@genkaku/protocol 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/bridge.d.ts +46 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +609 -0
- package/dist/capabilities.d.ts +50 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +66 -0
- package/dist/capabilities.test.d.ts +2 -0
- package/dist/capabilities.test.d.ts.map +1 -0
- package/dist/capabilities.test.js +39 -0
- package/dist/chain.d.ts +496 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +763 -0
- package/dist/credit.d.ts +35 -0
- package/dist/credit.d.ts.map +1 -0
- package/dist/credit.js +112 -0
- package/dist/dial.d.ts +51 -0
- package/dist/dial.d.ts.map +1 -0
- package/dist/dial.js +287 -0
- package/dist/directory.d.ts +39 -0
- package/dist/directory.d.ts.map +1 -0
- package/dist/directory.js +149 -0
- package/dist/discovery.d.ts +20 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +58 -0
- package/dist/hello.d.ts +14 -0
- package/dist/hello.d.ts.map +1 -0
- package/dist/hello.js +42 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/keys.d.ts +30 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +126 -0
- package/dist/llmBridge.d.ts +100 -0
- package/dist/llmBridge.d.ts.map +1 -0
- package/dist/llmBridge.js +284 -0
- package/dist/lp.d.ts +31 -0
- package/dist/lp.d.ts.map +1 -0
- package/dist/lp.js +116 -0
- package/dist/modeManager.d.ts +30 -0
- package/dist/modeManager.d.ts.map +1 -0
- package/dist/modeManager.js +81 -0
- package/dist/netconfig.d.ts +11 -0
- package/dist/netconfig.d.ts.map +1 -0
- package/dist/netconfig.js +39 -0
- package/dist/offer.d.ts +89 -0
- package/dist/offer.d.ts.map +1 -0
- package/dist/offer.js +86 -0
- package/dist/offer.test.d.ts +2 -0
- package/dist/offer.test.d.ts.map +1 -0
- package/dist/offer.test.js +33 -0
- package/dist/ollama.d.ts +53 -0
- package/dist/ollama.d.ts.map +1 -0
- package/dist/ollama.js +162 -0
- package/dist/p2pNode.d.ts +52 -0
- package/dist/p2pNode.d.ts.map +1 -0
- package/dist/p2pNode.js +454 -0
- package/dist/paywall.d.ts +30 -0
- package/dist/paywall.d.ts.map +1 -0
- package/dist/paywall.js +84 -0
- package/dist/publishOptIn.d.ts +15 -0
- package/dist/publishOptIn.d.ts.map +1 -0
- package/dist/publishOptIn.js +34 -0
- package/dist/receipts.d.ts +61 -0
- package/dist/receipts.d.ts.map +1 -0
- package/dist/receipts.js +135 -0
- package/dist/registry.d.ts +19 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +50 -0
- package/dist/rpc.d.ts +24 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +143 -0
- package/dist/status.d.ts +56 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +354 -0
- package/dist/userSession.d.ts +77 -0
- package/dist/userSession.d.ts.map +1 -0
- package/dist/userSession.js +132 -0
- package/dist/workerSession.d.ts +87 -0
- package/dist/workerSession.d.ts.map +1 -0
- package/dist/workerSession.js +97 -0
- package/dist/workerSession.test.d.ts +2 -0
- package/dist/workerSession.test.d.ts.map +1 -0
- package/dist/workerSession.test.js +49 -0
- package/package.json +56 -3
- package/README.md +0 -3
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type Hex } from 'viem';
|
|
2
|
+
import type { NetEnvironment } from './netconfig.ts';
|
|
3
|
+
export type ReceiptPayloadV1 = {
|
|
4
|
+
chainId: 84532 | 8453;
|
|
5
|
+
escrow: `0x${string}`;
|
|
6
|
+
sessionId: `0x${string}`;
|
|
7
|
+
seq: number;
|
|
8
|
+
amount: bigint;
|
|
9
|
+
/** commit-challenge (P4.9f): keccak of the canonical REQUEST. Always set. */
|
|
10
|
+
inputHash: `0x${string}`;
|
|
11
|
+
/** keccak of the full answer. ZERO until the through-end receipt, which
|
|
12
|
+
* commits the exact text the worker was paid for. */
|
|
13
|
+
outputHash: `0x${string}`;
|
|
14
|
+
};
|
|
15
|
+
/** All-zero bytes32 — "not committed yet" (never a real keccak in practice). */
|
|
16
|
+
export declare const ZERO_HASH: "0x0000000000000000000000000000000000000000000000000000000000000000";
|
|
17
|
+
/** Canonical request commitment. NUL separators make the field boundaries
|
|
18
|
+
* unambiguous, so no two different requests can produce the same string. */
|
|
19
|
+
export declare function inputHashOf(req: {
|
|
20
|
+
model: string;
|
|
21
|
+
prompt: string;
|
|
22
|
+
system?: string;
|
|
23
|
+
maxTokens?: number;
|
|
24
|
+
}): `0x${string}`;
|
|
25
|
+
/** Commitment to the exact answer text (byte-exact, no normalisation). */
|
|
26
|
+
export declare function outputHashOf(text: string): `0x${string}`;
|
|
27
|
+
export declare function makeReceiptBody(env: NetEnvironment, escrow: string, sessionId: `0x${string}`, seq: number, amount: bigint, inputHash?: `0x${string}`, outputHash?: `0x${string}`): ReceiptPayloadV1 & {
|
|
28
|
+
_domain: 'genkaku.receipt.v1';
|
|
29
|
+
};
|
|
30
|
+
/** Cumulative-receipt digest: keccak256(abi.encodePacked(chainId, escrow,
|
|
31
|
+
* sessionId, seq, amount)) — the receiptHash JobEscrow.settle takes as an
|
|
32
|
+
* opaque bytes32; a receipt replayed on chain verifies bit-for-bit. */
|
|
33
|
+
export declare function digestFor(receipt: ReceiptPayloadV1): Uint8Array;
|
|
34
|
+
/** Hex form of digestFor — what the settle wire/tx carry as `receiptHash`. */
|
|
35
|
+
export declare function digestForHex(receipt: ReceiptPayloadV1): Hex;
|
|
36
|
+
/**
|
|
37
|
+
* 32-byte stream handle for a session. Priced sessions (real escrow id) use
|
|
38
|
+
* the numeric id as a bytes32 big-endian — the same integer JobEscrow keys
|
|
39
|
+
* `sessions[id]` with. Unpriced self-test sessions carry a deterministic
|
|
40
|
+
* keccak of the jobId so already-wired clients still bind receipts to a stream.
|
|
41
|
+
*/
|
|
42
|
+
export declare function sessionIdHex(jobId: string, escrowId: string): `0x${string}`;
|
|
43
|
+
/** EIP-191 personal_message signature over the 32-byte receipt digest —
|
|
44
|
+
* recovers in JobEscrow._verify's `_verify(digest, sig, expected)` shape. */
|
|
45
|
+
export declare function signDigest(key: `0x${string}`, digest: Uint8Array): Promise<string>;
|
|
46
|
+
/** Recover the signer address (EIP-191) — rejects malformed sigs with null. */
|
|
47
|
+
export declare function recoverSigner(digest: Uint8Array, sig: string): Promise<`0x${string}` | null>;
|
|
48
|
+
/** Worker-side ledger: keep max signed receipt per session (SQLite later). */
|
|
49
|
+
export declare class ReceiptLedger {
|
|
50
|
+
private max;
|
|
51
|
+
offer(sessionId: string, seq: number, amount: bigint): {
|
|
52
|
+
accepted: boolean;
|
|
53
|
+
reason?: string;
|
|
54
|
+
};
|
|
55
|
+
cumulative(sessionId: string): bigint;
|
|
56
|
+
settleDigest(sessionId: string): {
|
|
57
|
+
seq: number;
|
|
58
|
+
amount: bigint;
|
|
59
|
+
} | null;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=receipts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"receipts.d.ts","sourceRoot":"","sources":["../src/receipts.ts"],"names":[],"mappings":"AAUA,OAAO,EAAgB,KAAK,GAAG,EAAoD,MAAM,MAAM,CAAA;AAG/F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAGpD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,KAAK,GAAG,IAAI,CAAA;IACrB,MAAM,EAAE,KAAK,MAAM,EAAE,CAAA;IACrB,SAAS,EAAE,KAAK,MAAM,EAAE,CAAA;IACxB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,6EAA6E;IAC7E,SAAS,EAAE,KAAK,MAAM,EAAE,CAAA;IACxB;0DACsD;IACtD,UAAU,EAAE,KAAK,MAAM,EAAE,CAAA;CAC1B,CAAA;AAED,gFAAgF;AAChF,eAAO,MAAM,SAAS,EAAG,oEAA6E,CAAA;AAEtG;6EAC6E;AAC7E,wBAAgB,WAAW,CAAC,GAAG,EAAE;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,KAAK,MAAM,EAAE,CAUhB;AAED,0EAA0E;AAC1E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,MAAM,EAAE,CAExD;AAED,wBAAgB,eAAe,CAC7B,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,KAAK,MAAM,EAAE,EACxB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,SAAS,GAAE,KAAK,MAAM,EAAc,EACpC,UAAU,GAAE,KAAK,MAAM,EAAc,GACpC,gBAAgB,GAAG;IAAE,OAAO,EAAE,oBAAoB,CAAA;CAAE,CAatD;AAED;;wEAEwE;AACxE,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,UAAU,CAE/D;AAED,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,OAAO,EAAE,gBAAgB,GAAG,GAAG,CAkB3D;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,MAAM,EAAE,CAY3E;AAED;8EAC8E;AAC9E,wBAAsB,UAAU,CAAC,GAAG,EAAE,KAAK,MAAM,EAAE,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAWxF;AAED,+EAA+E;AAC/E,wBAAsB,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,MAAM,EAAE,GAAG,IAAI,CAAC,CAQlG;AAED,8EAA8E;AAC9E,qBAAa,aAAa;IACxB,OAAO,CAAC,GAAG,CAAqD;IAEhE,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;IAY7F,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAIrC,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;CAGxE"}
|
package/dist/receipts.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// GENKAKU: cumulative metered receipts (P2 §3, PDF-derived bounded-loss scheme).
|
|
2
|
+
// Client signs; worker holds; the LARGEST cumulative receipt is co-signed at
|
|
3
|
+
// settle. Zero gas — pure local crypto.
|
|
4
|
+
//
|
|
5
|
+
// P4.4 (cutover): digestFor is now REAL keccak256(abi.encodePacked(...)) over
|
|
6
|
+
// the receipt struct, and signatures are EIP-191 personal-message over the
|
|
7
|
+
// 32-byte digest — exactly the shape JobEscrow._verify recovers ("0x19Ethereum
|
|
8
|
+
// Signed Message:\n32" prefix), so a receipt presented at settle verifies on
|
|
9
|
+
// chain with no re-encoding.
|
|
10
|
+
import { encodePacked, keccak256, recoverMessageAddress, toBytes, toHex } from 'viem';
|
|
11
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
12
|
+
import { CHAIN_ID } from "./netconfig.js";
|
|
13
|
+
/** All-zero bytes32 — "not committed yet" (never a real keccak in practice). */
|
|
14
|
+
export const ZERO_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000';
|
|
15
|
+
/** Canonical request commitment. NUL separators make the field boundaries
|
|
16
|
+
* unambiguous, so no two different requests can produce the same string. */
|
|
17
|
+
export function inputHashOf(req) {
|
|
18
|
+
const canonical = [
|
|
19
|
+
'genkaku.commit.v1',
|
|
20
|
+
req.model,
|
|
21
|
+
req.system ?? '',
|
|
22
|
+
String(req.maxTokens ?? 0),
|
|
23
|
+
req.prompt,
|
|
24
|
+
].join('\u0000');
|
|
25
|
+
return keccak256(toBytes(canonical));
|
|
26
|
+
}
|
|
27
|
+
/** Commitment to the exact answer text (byte-exact, no normalisation). */
|
|
28
|
+
export function outputHashOf(text) {
|
|
29
|
+
return keccak256(toBytes(text));
|
|
30
|
+
}
|
|
31
|
+
export function makeReceiptBody(env, escrow, sessionId, seq, amount, inputHash = ZERO_HASH, outputHash = ZERO_HASH) {
|
|
32
|
+
if (seq < 1) {
|
|
33
|
+
throw new Error('[genkaku:receipt] seq starts at 1');
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
_domain: 'genkaku.receipt.v1',
|
|
37
|
+
chainId: CHAIN_ID[env],
|
|
38
|
+
escrow: escrow,
|
|
39
|
+
sessionId,
|
|
40
|
+
seq,
|
|
41
|
+
amount,
|
|
42
|
+
inputHash,
|
|
43
|
+
outputHash,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Cumulative-receipt digest: keccak256(abi.encodePacked(chainId, escrow,
|
|
47
|
+
* sessionId, seq, amount)) — the receiptHash JobEscrow.settle takes as an
|
|
48
|
+
* opaque bytes32; a receipt replayed on chain verifies bit-for-bit. */
|
|
49
|
+
export function digestFor(receipt) {
|
|
50
|
+
return toBytes(digestForHex(receipt));
|
|
51
|
+
}
|
|
52
|
+
/** Hex form of digestFor — what the settle wire/tx carry as `receiptHash`. */
|
|
53
|
+
export function digestForHex(receipt) {
|
|
54
|
+
const fields = [
|
|
55
|
+
// uint256 / address / bytes32 / uint256 / uint256 / bytes32 / bytes32 —
|
|
56
|
+
// packed layout (each numeric field big-endian full-width; all sizes are
|
|
57
|
+
// multiples of 8 bits so nothing is right-padded).
|
|
58
|
+
BigInt(receipt.chainId),
|
|
59
|
+
receipt.escrow.toLowerCase(),
|
|
60
|
+
receipt.sessionId.toLowerCase(),
|
|
61
|
+
BigInt(receipt.seq),
|
|
62
|
+
receipt.amount,
|
|
63
|
+
receipt.inputHash,
|
|
64
|
+
receipt.outputHash,
|
|
65
|
+
];
|
|
66
|
+
return keccak256(encodePacked(['uint256', 'address', 'bytes32', 'uint256', 'uint256', 'bytes32', 'bytes32'], fields));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 32-byte stream handle for a session. Priced sessions (real escrow id) use
|
|
70
|
+
* the numeric id as a bytes32 big-endian — the same integer JobEscrow keys
|
|
71
|
+
* `sessions[id]` with. Unpriced self-test sessions carry a deterministic
|
|
72
|
+
* keccak of the jobId so already-wired clients still bind receipts to a stream.
|
|
73
|
+
*/
|
|
74
|
+
export function sessionIdHex(jobId, escrowId) {
|
|
75
|
+
let id = 0n;
|
|
76
|
+
try {
|
|
77
|
+
id = BigInt(escrowId === '' ? '0' : escrowId);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
id = 0n;
|
|
81
|
+
}
|
|
82
|
+
if (id > 0n) {
|
|
83
|
+
return toHex(id, { size: 32 });
|
|
84
|
+
}
|
|
85
|
+
return keccak256(toBytes(`genkaku.job:${jobId}`));
|
|
86
|
+
}
|
|
87
|
+
/** EIP-191 personal_message signature over the 32-byte receipt digest —
|
|
88
|
+
* recovers in JobEscrow._verify's `_verify(digest, sig, expected)` shape. */
|
|
89
|
+
export async function signDigest(key, digest) {
|
|
90
|
+
if (digest.length !== 32) {
|
|
91
|
+
throw new Error('[genkaku:receipt] digest must be 32 bytes');
|
|
92
|
+
}
|
|
93
|
+
const account = privateKeyToAccount(key);
|
|
94
|
+
const sig = await account.signMessage({ message: { raw: digest } });
|
|
95
|
+
// viem personal signatures are 65 bytes r||s||v with v in 27|28 — the exact
|
|
96
|
+
// split JobEscrow._verify calldata-loads.
|
|
97
|
+
if (toBytes(sig).length !== 65) {
|
|
98
|
+
throw new Error('[genkaku:receipt] signature must be 65 bytes');
|
|
99
|
+
}
|
|
100
|
+
return sig;
|
|
101
|
+
}
|
|
102
|
+
/** Recover the signer address (EIP-191) — rejects malformed sigs with null. */
|
|
103
|
+
export async function recoverSigner(digest, sig) {
|
|
104
|
+
try {
|
|
105
|
+
if (toBytes(sig).length !== 65) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return await recoverMessageAddress({ message: { raw: digest }, signature: sig });
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Worker-side ledger: keep max signed receipt per session (SQLite later). */
|
|
115
|
+
export class ReceiptLedger {
|
|
116
|
+
max = new Map();
|
|
117
|
+
offer(sessionId, seq, amount) {
|
|
118
|
+
const m = this.max.get(sessionId);
|
|
119
|
+
if (m && seq <= m.seq) {
|
|
120
|
+
return { accepted: false, reason: 'stale-seq' };
|
|
121
|
+
}
|
|
122
|
+
if (m && amount <= m.amount) {
|
|
123
|
+
return { accepted: false, reason: 'non-increasing' };
|
|
124
|
+
}
|
|
125
|
+
this.max.set(sessionId, { seq, amount });
|
|
126
|
+
return { accepted: true };
|
|
127
|
+
}
|
|
128
|
+
cumulative(sessionId) {
|
|
129
|
+
return this.max.get(sessionId)?.amount ?? 0n;
|
|
130
|
+
}
|
|
131
|
+
settleDigest(sessionId) {
|
|
132
|
+
return this.max.get(sessionId) ?? null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=receipts.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** v0 content-policy version the sidecar serves and pins on registration. */
|
|
2
|
+
export declare const POLICY_VERSION = "smoke-v1";
|
|
3
|
+
/** bytes32 the worker pins into WorkerRegistry for its PeerId. */
|
|
4
|
+
export declare function peerIdDigestOf(peerId: string): `0x${string}`;
|
|
5
|
+
/** The content-policy code the worker declares with its registration. */
|
|
6
|
+
export declare function policyHashOf(policy: string): `0x${string}`;
|
|
7
|
+
/** The libp2p PeerId encoded in a worker multiaddr (the grain of the compare). */
|
|
8
|
+
export declare function peerIdOf(addr: string): string | null;
|
|
9
|
+
export type RegistryWorker = {
|
|
10
|
+
active: boolean;
|
|
11
|
+
peerId: `0x${string}`;
|
|
12
|
+
policyHash: `0x${string}`;
|
|
13
|
+
};
|
|
14
|
+
/** The data-plane half of the binding: does the on-chain record for `wallet`
|
|
15
|
+
* pin EXACTLY this PeerId? A record that is missing is `absent` (testnet
|
|
16
|
+
* grace — the wallet may simply be unfunded); a PRESENT record that pins a
|
|
17
|
+
* different PeerId is an active spoofing attempt and fails hard. */
|
|
18
|
+
export declare function bindingOf(reg: RegistryWorker | null, peerId: string): 'bound' | 'absent' | 'mismatch';
|
|
19
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAoBA,6EAA6E;AAC7E,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC,kEAAkE;AAClE,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,MAAM,EAAE,CAM5D;AAED,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,MAAM,EAAE,CAE1D;AAED,kFAAkF;AAClF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASpD;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,KAAK,MAAM,EAAE,CAAA;IACrB,UAAU,EAAE,KAAK,MAAM,EAAE,CAAA;CAC1B,CAAA;AAED;;;qEAGqE;AACrE,wBAAgB,SAAS,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,UAAU,CAMrG"}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// GENKAKU: WorkerRegistry binding (P4.5) — wallet ⇄ libp2p PeerId.
|
|
2
|
+
// The on-chain half (WorkerRegistry.register, msg.sender = wallet) stores a
|
|
3
|
+
// bytes32 digest of the worker's libp2p PeerId; EVERY data-plane participant
|
|
4
|
+
// recomputes the digest from the PeerId it just dialed and compares — the two
|
|
5
|
+
// halves only match if the same operator controls both keys.
|
|
6
|
+
//
|
|
7
|
+
// Digest scheme (v0, deterministic on both sides): keccak256 over a typed
|
|
8
|
+
// UTF-8 tag + the PeerId STRING (its base58btc data-plane form, the same form
|
|
9
|
+
// every libp2p address carries). Tagging binds the scheme, not the address —
|
|
10
|
+
// a collision with other hashes is structurally impossible:
|
|
11
|
+
// peerIdDigestOf = keccak("genkaku.peer.v0:<peerId string>")
|
|
12
|
+
// policyHashOf = keccak("genkaku.policy.v0:<policy code>")
|
|
13
|
+
// (WorkerRegistry stores the digest opaquely — bytes32 — so the algorithm is
|
|
14
|
+
// an off-chain agreement; the contract comment's "blake2s" was aspirational.)
|
|
15
|
+
import { keccak256, toBytes } from 'viem';
|
|
16
|
+
const PEER_TAG = 'genkaku.peer.v0:';
|
|
17
|
+
const POLICY_TAG = 'genkaku.policy.v0:';
|
|
18
|
+
/** v0 content-policy version the sidecar serves and pins on registration. */
|
|
19
|
+
export const POLICY_VERSION = 'smoke-v1';
|
|
20
|
+
/** bytes32 the worker pins into WorkerRegistry for its PeerId. */
|
|
21
|
+
export function peerIdDigestOf(peerId) {
|
|
22
|
+
if (peerId.includes('/') || peerId === '') {
|
|
23
|
+
throw new Error('[genkaku:registry] expected a bare PeerId string (not a multiaddr)');
|
|
24
|
+
}
|
|
25
|
+
return keccak256(toBytes(PEER_TAG + peerId));
|
|
26
|
+
}
|
|
27
|
+
/** The content-policy code the worker declares with its registration. */
|
|
28
|
+
export function policyHashOf(policy) {
|
|
29
|
+
return keccak256(toBytes(POLICY_TAG + policy));
|
|
30
|
+
}
|
|
31
|
+
/** The libp2p PeerId encoded in a worker multiaddr (the grain of the compare). */
|
|
32
|
+
export function peerIdOf(addr) {
|
|
33
|
+
const tail = addr.split('/p2p/')[1];
|
|
34
|
+
if (tail != null && tail !== '') {
|
|
35
|
+
// trim any stray suffix (addresses end at the peer id in our dialer)
|
|
36
|
+
return tail.split('/')[0] || null;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/** The data-plane half of the binding: does the on-chain record for `wallet`
|
|
41
|
+
* pin EXACTLY this PeerId? A record that is missing is `absent` (testnet
|
|
42
|
+
* grace — the wallet may simply be unfunded); a PRESENT record that pins a
|
|
43
|
+
* different PeerId is an active spoofing attempt and fails hard. */
|
|
44
|
+
export function bindingOf(reg, peerId) {
|
|
45
|
+
if (reg == null || !reg.active) {
|
|
46
|
+
return 'absent';
|
|
47
|
+
}
|
|
48
|
+
return reg.peerId === peerIdDigestOf(peerId) ? 'bound' : 'mismatch';
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=registry.js.map
|
package/dist/rpc.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Transport } from 'viem';
|
|
2
|
+
import type { NetEnvironment } from './netconfig.ts';
|
|
3
|
+
/** Public endpoints, measured reachable 2026-11. Order is a last-resort tie
|
|
4
|
+
* break only — the probe reorders per machine, and `fallback` handles failures. */
|
|
5
|
+
export declare const PUBLIC_RPC_ENDPOINTS: Record<NetEnvironment, readonly string[]>;
|
|
6
|
+
/** Explicit override, then last-good endpoint, then the rest.
|
|
7
|
+
* `GENKAKU_RPC_URL` is kept FIRST and not removed: if the operator's endpoint
|
|
8
|
+
* dies mid-session we still want to keep serving from a public one. */
|
|
9
|
+
export declare function rpcUrls(env: NetEnvironment): string[];
|
|
10
|
+
/** Transport with automatic failover — the part the user never sees. */
|
|
11
|
+
export declare function rpcTransport(env: NetEnvironment): Transport;
|
|
12
|
+
/** One `eth_blockNumber` per endpoint, keep the fastest. Fire-and-forget at
|
|
13
|
+
* boot; a failure leaves the previous preference untouched. */
|
|
14
|
+
export declare function probeRpc(env: NetEnvironment, log?: {
|
|
15
|
+
info: (msg: string, ...args: unknown[]) => void;
|
|
16
|
+
warn: (msg: string, ...args: unknown[]) => void;
|
|
17
|
+
}): Promise<string | null>;
|
|
18
|
+
/** Human-readable network state for the dashboard (no input field anywhere). */
|
|
19
|
+
export declare function rpcSummary(env: NetEnvironment): {
|
|
20
|
+
url: string;
|
|
21
|
+
cached: boolean;
|
|
22
|
+
override: boolean;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=rpc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,MAAM,CAAA;AAErD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAEpD;oFACoF;AACpF,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAe1E,CAAA;AAyCD;;wEAEwE;AACxE,wBAAgB,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,EAAE,CAgBrD;AAED,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,GAAG,SAAS,CAO3D;AAED;gEACgE;AAChE,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,cAAc,EACnB,GAAG,CAAC,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;CAAE,GACzG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA0CxB;AAED,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CASnG"}
|
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// GENKAKU: zero-config Base RPC (P4.9b).
|
|
2
|
+
//
|
|
3
|
+
// The product promise is "no fields to fill": a user or worker never sees an
|
|
4
|
+
// RPC URL unless every endpoint is down. That rules out shipping a metered key
|
|
5
|
+
// (an Electron app's asar is readable — a bundled unlimited key becomes someone
|
|
6
|
+
// else's free tier) and rules out asking a non-technical operator to sign up.
|
|
7
|
+
//
|
|
8
|
+
// So: a small list of public endpoints, probed once at boot to find the fastest
|
|
9
|
+
// healthy one, cached on disk, and wrapped in viem's `fallback` transport so a
|
|
10
|
+
// 429 / 5xx / timeout mid-session rolls to the next endpoint without user
|
|
11
|
+
// involvement. `GENKAKU_RPC_URL` still wins when set — that is the escape hatch
|
|
12
|
+
// for development and for a power user who wants privacy, never a product
|
|
13
|
+
// requirement.
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { fallback, http } from 'viem';
|
|
18
|
+
/** Public endpoints, measured reachable 2026-11. Order is a last-resort tie
|
|
19
|
+
* break only — the probe reorders per machine, and `fallback` handles failures. */
|
|
20
|
+
export const PUBLIC_RPC_ENDPOINTS = {
|
|
21
|
+
'base-sepolia': [
|
|
22
|
+
'https://sepolia.base.org',
|
|
23
|
+
'https://base-sepolia-rpc.publicnode.com',
|
|
24
|
+
'https://base-sepolia.drpc.org',
|
|
25
|
+
'https://base-sepolia.gateway.tenderly.co',
|
|
26
|
+
'https://base-sepolia.public.blastapi.io',
|
|
27
|
+
],
|
|
28
|
+
base: [
|
|
29
|
+
'https://mainnet.base.org',
|
|
30
|
+
'https://base-rpc.publicnode.com',
|
|
31
|
+
'https://base.drpc.org',
|
|
32
|
+
'https://gateway.tenderly.co/public/base',
|
|
33
|
+
'https://base-pokt.nodies.app',
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
const PROBE_TIMEOUT_MS = 4_000;
|
|
37
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
38
|
+
function keyDir() {
|
|
39
|
+
const dir = process.env.GENKAKU_KEY_DIR?.trim();
|
|
40
|
+
return dir != null && dir !== '' ? dir : join(homedir(), '.dsh', 'genkaku');
|
|
41
|
+
}
|
|
42
|
+
function cachePath() {
|
|
43
|
+
return join(keyDir(), 'rpc.json');
|
|
44
|
+
}
|
|
45
|
+
function readCache() {
|
|
46
|
+
try {
|
|
47
|
+
if (!existsSync(cachePath()))
|
|
48
|
+
return { version: 1 };
|
|
49
|
+
const raw = JSON.parse(readFileSync(cachePath(), 'utf8'));
|
|
50
|
+
return raw?.version === 1 ? raw : { version: 1 };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return { version: 1 };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function writeCache(cache) {
|
|
57
|
+
try {
|
|
58
|
+
mkdirSync(keyDir(), { recursive: true });
|
|
59
|
+
writeFileSync(cachePath(), JSON.stringify(cache, null, 2), { mode: 0o600 });
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// a cache miss only costs one probe next boot
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Explicit override, then last-good endpoint, then the rest.
|
|
66
|
+
* `GENKAKU_RPC_URL` is kept FIRST and not removed: if the operator's endpoint
|
|
67
|
+
* dies mid-session we still want to keep serving from a public one. */
|
|
68
|
+
export function rpcUrls(env) {
|
|
69
|
+
const override = (process.env.GENKAKU_RPC_URL ?? '')
|
|
70
|
+
.split(',')
|
|
71
|
+
.map(s => s.trim())
|
|
72
|
+
.filter(s => s.startsWith('http'));
|
|
73
|
+
const preferred = readCache().preferred?.[env];
|
|
74
|
+
const publicList = PUBLIC_RPC_ENDPOINTS[env];
|
|
75
|
+
const ordered = [
|
|
76
|
+
...override,
|
|
77
|
+
...(preferred != null ? [preferred] : []),
|
|
78
|
+
...publicList,
|
|
79
|
+
];
|
|
80
|
+
return [...new Set(ordered)];
|
|
81
|
+
}
|
|
82
|
+
/** Transport with automatic failover — the part the user never sees. */
|
|
83
|
+
export function rpcTransport(env) {
|
|
84
|
+
return fallback(rpcUrls(env).map(url => http(url, { timeout: REQUEST_TIMEOUT_MS, retryCount: 1 })),
|
|
85
|
+
// Keep the configured order: ranking samples would re-probe on every
|
|
86
|
+
// failure burst, which is exactly when the endpoints are least healthy.
|
|
87
|
+
{ rank: false });
|
|
88
|
+
}
|
|
89
|
+
/** One `eth_blockNumber` per endpoint, keep the fastest. Fire-and-forget at
|
|
90
|
+
* boot; a failure leaves the previous preference untouched. */
|
|
91
|
+
export async function probeRpc(env, log) {
|
|
92
|
+
const urls = rpcUrls(env);
|
|
93
|
+
const results = await Promise.all(urls.map(async (url) => {
|
|
94
|
+
const started = Date.now();
|
|
95
|
+
try {
|
|
96
|
+
const response = await fetch(url, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: { 'content-type': 'application/json' },
|
|
99
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] }),
|
|
100
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
101
|
+
});
|
|
102
|
+
if (!response.ok)
|
|
103
|
+
return null;
|
|
104
|
+
const body = await response.json();
|
|
105
|
+
if (typeof body.result !== 'string')
|
|
106
|
+
return null;
|
|
107
|
+
return { url, ms: Date.now() - started, block: body.result };
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}));
|
|
113
|
+
const healthy = results.filter((r) => r != null);
|
|
114
|
+
if (healthy.length === 0) {
|
|
115
|
+
log?.warn('rpc: no endpoint answered a probe; requests will still try all of them');
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
healthy.sort((a, b) => a.ms - b.ms);
|
|
119
|
+
const best = healthy[0];
|
|
120
|
+
if (best == null)
|
|
121
|
+
return null;
|
|
122
|
+
const cache = readCache();
|
|
123
|
+
cache.preferred = { ...cache.preferred, [env]: best.url };
|
|
124
|
+
cache.latency = {
|
|
125
|
+
...cache.latency,
|
|
126
|
+
[env]: Object.fromEntries(healthy.map(h => [h.url, h.ms])),
|
|
127
|
+
};
|
|
128
|
+
writeCache(cache);
|
|
129
|
+
log?.info('rpc: %s (%dms, block %s) — %d/%d endpoints healthy', best.url, best.ms, BigInt(best.block).toString(), healthy.length, urls.length);
|
|
130
|
+
return best.url;
|
|
131
|
+
}
|
|
132
|
+
/** Human-readable network state for the dashboard (no input field anywhere). */
|
|
133
|
+
export function rpcSummary(env) {
|
|
134
|
+
const override = (process.env.GENKAKU_RPC_URL ?? '').trim() !== '';
|
|
135
|
+
const preferred = readCache().preferred?.[env];
|
|
136
|
+
const first = rpcUrls(env)[0] ?? '';
|
|
137
|
+
return {
|
|
138
|
+
url: override ? first : (preferred ?? first),
|
|
139
|
+
cached: preferred != null,
|
|
140
|
+
override,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=rpc.js.map
|
package/dist/status.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { RoleMode } from './modeManager.ts';
|
|
2
|
+
import type { OfferWire } from './offer.ts';
|
|
3
|
+
/** One offer row rendered by the dashboard. */
|
|
4
|
+
export type StatusOffer = {
|
|
5
|
+
/** peer multiaddr serving this row; omitted for a purely local catalog */
|
|
6
|
+
addr?: string;
|
|
7
|
+
offer: OfferWire;
|
|
8
|
+
};
|
|
9
|
+
/** Live facts the status server renders; called once per HTTP request. */
|
|
10
|
+
export type StatusSnapshot = {
|
|
11
|
+
role: string;
|
|
12
|
+
peerId: string;
|
|
13
|
+
peerAddrs: string[];
|
|
14
|
+
wallet: string;
|
|
15
|
+
offers: StatusOffer[];
|
|
16
|
+
activeSessions: number;
|
|
17
|
+
/** true when at least one announced address is globally dialable (P4.9i) */
|
|
18
|
+
reachable: boolean;
|
|
19
|
+
/** config.publishAddress: may a public address be written to the chain? */
|
|
20
|
+
publishAddress: boolean;
|
|
21
|
+
};
|
|
22
|
+
export type StatusHandle = {
|
|
23
|
+
url: string;
|
|
24
|
+
close: () => Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
/** One validated catalog row accepted by POST /genkaku/catalog. */
|
|
27
|
+
export type CatalogEntryInput = {
|
|
28
|
+
model: string;
|
|
29
|
+
pricePerHour: number;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
};
|
|
32
|
+
/** Applies a validated catalog replacement; may be async. */
|
|
33
|
+
export type CatalogUpdateHandler = (catalog: CatalogEntryInput[]) => void | Promise<void>;
|
|
34
|
+
/** Applies a validated role-mode switch; may be async. */
|
|
35
|
+
export type ModeUpdateHandler = (mode: RoleMode) => void | Promise<void>;
|
|
36
|
+
/** Applies a directory-publishing opt-in; may be async. */
|
|
37
|
+
export type PublishUpdateHandler = (publish: boolean) => void | Promise<void>;
|
|
38
|
+
/** Worker-side Ollama control the onboarding panel drives. */
|
|
39
|
+
export type OllamaControl = {
|
|
40
|
+
/** Detect the install, probe the service and list local models. */
|
|
41
|
+
state: () => Promise<unknown>;
|
|
42
|
+
/** Start a stopped service; idempotent. */
|
|
43
|
+
ensureRunning: () => Promise<unknown>;
|
|
44
|
+
/** Download a registry model, forwarding NDJSON-friendly progress. */
|
|
45
|
+
pull: (model: string, onProgress: (progress: {
|
|
46
|
+
status: string;
|
|
47
|
+
percent?: number;
|
|
48
|
+
}) => void) => Promise<{
|
|
49
|
+
status: string;
|
|
50
|
+
}>;
|
|
51
|
+
/** Delete one locally installed model. */
|
|
52
|
+
remove: (model: string) => Promise<void>;
|
|
53
|
+
};
|
|
54
|
+
/** Serve the dashboard and live status JSON on 127.0.0.1. */
|
|
55
|
+
export declare function startStatusServer(port: number, snapshot: () => StatusSnapshot | Promise<StatusSnapshot>, onCatalog?: CatalogUpdateHandler, onMode?: ModeUpdateHandler, ollama?: OllamaControl, onPublish?: PublishUpdateHandler): Promise<StatusHandle>;
|
|
56
|
+
//# sourceMappingURL=status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE3C,+CAA+C;AAC/C,MAAM,MAAM,WAAW,GAAG;IACxB,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,SAAS,CAAA;CACjB,CAAA;AAED,0EAA0E;AAC1E,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,4EAA4E;IAC5E,SAAS,EAAE,OAAO,CAAA;IAClB,2EAA2E;IAC3E,cAAc,EAAE,OAAO,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B,CAAA;AAED,mEAAmE;AACnE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB,CAAA;AAED,6DAA6D;AAC7D,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,EAAE,iBAAiB,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAEzF,0DAA0D;AAC1D,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAExE,2DAA2D;AAC3D,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAE7E,8DAA8D;AAC9D,MAAM,MAAM,aAAa,GAAG;IAC1B,mEAAmE;IACnE,KAAK,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;IAC7B,2CAA2C;IAC3C,aAAa,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;IACrC,sEAAsE;IACtE,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,QAAQ,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,KAAK,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC1H,0CAA0C;IAC1C,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACzC,CAAA;AAsKD,6DAA6D;AAC7D,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,EACxD,SAAS,CAAC,EAAE,oBAAoB,EAChC,MAAM,CAAC,EAAE,iBAAiB,EAC1B,MAAM,CAAC,EAAE,aAAa,EACtB,SAAS,CAAC,EAAE,oBAAoB,GAC/B,OAAO,CAAC,YAAY,CAAC,CA+LvB"}
|