mycomesh-consumer 0.8.0 → 0.9.0

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.
@@ -6,7 +6,7 @@
6
6
  "stablecoin": "0xbce27efad4191277167fe76819581a97959048cf",
7
7
  "registry": "0x0ff3210e8e8abf5f5bfa73a39e60db7416b6c8c7",
8
8
  "relay_directory": "0xa0aaad9393f51cb5786c69f152133a174588bdab",
9
- "probe_ledger": "0x3bbc6fc971a6c4e528af9228b95d4b83ef188795",
9
+ "probe_ledger": "0x1f26a2285eb366d0fd201bea6969abd1aaacb62b",
10
10
  "emission": "0x85ee0e8f63400041f0c76010c590c330bc135a8d",
11
11
  "token": "0x620d026ad854fb17c7b16534a0f28ef5499e2d4a",
12
12
  "emission_block": 11815544,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mycomesh-consumer",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "MycoMesh V11 Consumer: a custodied deposit, relay-blind encrypted requests to any Provider, and a local OpenAI-compatible endpoint.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/chain.mjs CHANGED
@@ -30,14 +30,27 @@ export async function availableBalance(network, owner) {
30
30
  return ethCallWord(network.rpc_urls, network.settlement, encodeCall("availableBalance(address)", [["address", owner]]));
31
31
  }
32
32
 
33
+ /**
34
+ * Twice the latest base fee plus the suggested tip, as EIP-1559 wallets cap a fee. Some nodes (anvil)
35
+ * derive eth_gasPrice from recent transactions, so pricing above it compounds block after block.
36
+ */
37
+ export async function suggestedGasPrice(urls) {
38
+ try {
39
+ const [block, tip] = await Promise.all([rpcCall(urls, "eth_getBlockByNumber", ["latest", false]),
40
+ rpcCall(urls, "eth_maxPriorityFeePerGas", [])]);
41
+ if (block?.baseFeePerGas) return quantity(block.baseFeePerGas) * 2n + quantity(tip);
42
+ } catch {}
43
+ return quantity(await rpcCall(urls, "eth_gasPrice", [])) * 12n / 10n;
44
+ }
45
+
33
46
  export async function sendTransaction(urls, privateKey, { to, data, gasLimit }) {
34
47
  const from = addressOf(privateKey);
35
48
  const [chainId, nonce, gasPrice] = await Promise.all([
36
- rpcCall(urls, "eth_chainId", []), rpcCall(urls, "eth_getTransactionCount", [from, "pending"]), rpcCall(urls, "eth_gasPrice", []),
49
+ rpcCall(urls, "eth_chainId", []), rpcCall(urls, "eth_getTransactionCount", [from, "pending"]), suggestedGasPrice(urls),
37
50
  ]);
38
51
  const limit = gasLimit ?? (quantity(await rpcCall(urls, "eth_estimateGas", [{ from, to, data }])) * 12n / 10n + 10000n);
39
52
  const raw = signLegacyTransaction(privateKey, {
40
- nonce: quantity(nonce), gasPrice: quantity(gasPrice) * 12n / 10n, gasLimit: limit, to, data, chainId: quantity(chainId),
53
+ nonce: quantity(nonce), gasPrice, gasLimit: limit, to, data, chainId: quantity(chainId),
41
54
  });
42
55
  const hash = await rpcCall(urls, "eth_sendRawTransaction", [raw]);
43
56
  for (let i = 0; i < 180; i += 1) {
package/src/probes.mjs CHANGED
@@ -80,6 +80,36 @@ function capabilityTask(kind, p) {
80
80
  throw new Error(`unknown capability kind ${kind}`);
81
81
  }
82
82
 
83
+ // Hunters' own questions: {question, reference, grader} graded "number", "choice" or "text".
84
+ function customTask(p) {
85
+ const question = String(p.question); const reference = String(p.reference).trim(); const grader = String(p.grader);
86
+ if (!["number", "choice", "text"].includes(grader) || !question || !reference || question.length > 20_000 || reference.length > 200) {
87
+ throw new Error("invalid custom task");
88
+ }
89
+ if ((grader === "number" && !/^[0-9]+$/.test(reference)) || (grader === "choice" && !/^[A-J]$/.test(reference))) {
90
+ throw new Error("invalid custom task reference");
91
+ }
92
+ return { question, reference, grader };
93
+ }
94
+
95
+ const normal = (text) => text.toLowerCase().split(/\s+/).filter(Boolean).join(" ");
96
+
97
+ function gradeCustom(task, answer) {
98
+ const text = String(answer);
99
+ if (task.grader === "number") return gradeCapability({ ...task, kind: "custom-number" }, text);
100
+ if (task.grader === "choice") {
101
+ const pick = (part) => [...part.matchAll(/\b([A-J])\b/g)].map((m) => m[1]);
102
+ const boxed = [...text.matchAll(/\\boxed\{([^{}]*)\}/g)];
103
+ let stated = [];
104
+ if (boxed.length) stated = pick(boxed[boxed.length - 1][1]);
105
+ else for (const line of text.split(LINE_BREAKS).reverse()) { const found = pick(line); if (found.length) { stated = found; break; } }
106
+ return stated.length === 1 && stated[0] === task.reference ? "pass" : "wrong";
107
+ }
108
+ const lines = text.split(LINE_BREAKS).filter((line) => line.trim());
109
+ const final = lines.length ? normal(lines[lines.length - 1]) : "";
110
+ return final.includes(normal(task.reference)) || (text.length <= 200 && normal(text).includes(normal(task.reference))) ? "pass" : "wrong";
111
+ }
112
+
83
113
  function gradeCapability(task, answer) {
84
114
  // The answer finally stated decides: the last \boxed{...}, else the last line that states a value.
85
115
  // A handful of values at most: listing candidates is not answering.
@@ -106,6 +136,7 @@ export const capabilityFlagged = (passes, total, floor, minimum = 20) => total >
106
136
 
107
137
  export function buildTask(kind, params) {
108
138
  if (CAPABILITY_KINDS.includes(kind)) return { kind, capability: true, ...capabilityTask(kind, params) };
139
+ if (kind === "custom") return { kind, capability: true, ...customTask(params) };
109
140
  if (kind === "multiply") return { kind, question: `What is ${params.a} multiplied by ${params.b}?`, reference: String(BigInt(params.a) * BigInt(params.b)), numeric: true };
110
141
  if (kind === "sum") {
111
142
  return { kind, question: `What is the sum of ${params.values.join(", ")}?`, reference: String(params.values.reduce((a, b) => a + Number(b), 0)), numeric: true };
@@ -125,6 +156,7 @@ export function buildTask(kind, params) {
125
156
  }
126
157
 
127
158
  export function grade(task, answer) {
159
+ if (task.kind === "custom") return gradeCustom(task, answer);
128
160
  if (task.capability) return gradeCapability(task, answer);
129
161
  const text = String(answer).trim();
130
162
  if (!text) return "unrelated";
@@ -4,6 +4,7 @@ import { rpcCall } from "./chain.mjs";
4
4
  import { evidenceHash } from "./disputes.mjs";
5
5
  import { CAPABILITY_FLOORS, capabilityFlagged, verifyProbeEvidence } from "./probes.mjs";
6
6
 
7
+ // Recorded by the hunter that voided the probe: a Relay or any third party (open probing).
7
8
  const PROBE_RECORDED = hex(keccak(Buffer.from("ProbeRecorded(address,address,bytes32,bytes32,uint8)")));
8
9
  const LOOKBACK_BLOCKS = 50_400n; // about 7 days of 12-second blocks
9
10
  const LOG_CHUNK = 10_000n;
@@ -33,7 +34,7 @@ async function ledgerEvents(network) {
33
34
  const to = from + LOG_CHUNK - 1n < head ? from + LOG_CHUNK - 1n : head;
34
35
  for (const log of await rpcCall(network.rpc_urls, "eth_getLogs", [{ address: network.probe_ledger, topics: [PROBE_RECORDED],
35
36
  fromBlock: `0x${from.toString(16)}`, toBlock: `0x${to.toString(16)}` }])) {
36
- events.push({ provider: address(log.topics[1]), relay: address(log.topics[2]), key: log.topics[3],
37
+ events.push({ provider: address(log.topics[1]), hunter: address(log.topics[2]), key: log.topics[3],
37
38
  evidence: `0x${log.data.slice(2, 66)}`, ...VERDICT_CODES[Number(BigInt(`0x${log.data.slice(66, 130)}`))] });
38
39
  }
39
40
  }
@@ -61,17 +62,22 @@ export async function reputation(consumer, descriptors, relaysByOwner) {
61
62
  const entry = byOwner[event.provider];
62
63
  if (!entry || !event.verdict) continue;
63
64
  if (event.verdict === "pass") { entry[event.capability ? "capability_passed" : "probes_passed"] += 1; continue; }
64
- const relay = relaysByOwner[event.relay];
65
- if (!relay || verified >= MAX_VERIFIED) continue;
65
+ if (verified >= MAX_VERIFIED) continue;
66
66
  verified += 1;
67
- try {
68
- const { status, body } = await consumer.fetchJson(`${relay.url}/v11/evidence/${event.evidence}`,
69
- { ca: network.tls_ca, pin: relay.pin, timeoutMs: 8_000 });
70
- const checked = status === 200 && evidenceHash(body) === event.evidence ? verifyProbeEvidence(body, consumer.deployment) : null;
71
- if (checked?.verdict === "wrong" && checked.capability === event.capability) {
72
- entry[event.capability ? "capability_failed_verified" : "probes_failed_verified"] += 1;
73
- }
74
- } catch {}
67
+ // Hunters publish their evidence to every Relay's desk; a hunter that is a Relay serves its own first.
68
+ const relays = [relaysByOwner[event.hunter], ...Object.values(relaysByOwner)].filter(Boolean);
69
+ for (const relay of [...new Set(relays)].slice(0, 3)) {
70
+ try {
71
+ const { status, body } = await consumer.fetchJson(`${relay.url}/v11/evidence/${event.evidence}`,
72
+ { ca: network.tls_ca, pin: relay.pin, timeoutMs: 8_000 });
73
+ if (status !== 200 || evidenceHash(body) !== event.evidence) continue;
74
+ const checked = verifyProbeEvidence(body, consumer.deployment);
75
+ if (checked.verdict === "wrong" && checked.capability === event.capability) {
76
+ entry[event.capability ? "capability_failed_verified" : "probes_failed_verified"] += 1;
77
+ }
78
+ break;
79
+ } catch {}
80
+ }
75
81
  }
76
82
  return Object.fromEntries(descriptors.filter((d) => records[d.provider_signer]).map((descriptor) => {
77
83
  const entry = { ...byOwner[records[descriptor.provider_signer].owner] };