mycomesh-consumer 0.8.0 → 0.9.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.
@@ -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.1",
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
@@ -22,6 +22,17 @@ export async function rpcCall(urls, method, params, { timeoutMs = 15000 } = {})
22
22
 
23
23
  export const quantity = (value) => BigInt(value);
24
24
 
25
+ /** eth_getLogs that tolerates a load-balanced RPC answering from a node a block or two behind the head. */
26
+ export async function getLogs(urls, filter) {
27
+ try {
28
+ return await rpcCall(urls, "eth_getLogs", [filter]);
29
+ } catch (error) {
30
+ if (!/beyond current head/.test(error.message)) throw error;
31
+ const to = BigInt(filter.toBlock) - 3n;
32
+ return to < BigInt(filter.fromBlock) ? [] : rpcCall(urls, "eth_getLogs", [{ ...filter, toBlock: `0x${to.toString(16)}` }]);
33
+ }
34
+ }
35
+
25
36
  export async function ethCallWord(urls, to, data) {
26
37
  return BigInt(await rpcCall(urls, "eth_call", [{ to, data }, "latest"]));
27
38
  }
@@ -30,14 +41,27 @@ export async function availableBalance(network, owner) {
30
41
  return ethCallWord(network.rpc_urls, network.settlement, encodeCall("availableBalance(address)", [["address", owner]]));
31
42
  }
32
43
 
44
+ /**
45
+ * Twice the latest base fee plus the suggested tip, as EIP-1559 wallets cap a fee. Some nodes (anvil)
46
+ * derive eth_gasPrice from recent transactions, so pricing above it compounds block after block.
47
+ */
48
+ export async function suggestedGasPrice(urls) {
49
+ try {
50
+ const [block, tip] = await Promise.all([rpcCall(urls, "eth_getBlockByNumber", ["latest", false]),
51
+ rpcCall(urls, "eth_maxPriorityFeePerGas", [])]);
52
+ if (block?.baseFeePerGas) return quantity(block.baseFeePerGas) * 2n + quantity(tip);
53
+ } catch {}
54
+ return quantity(await rpcCall(urls, "eth_gasPrice", [])) * 12n / 10n;
55
+ }
56
+
33
57
  export async function sendTransaction(urls, privateKey, { to, data, gasLimit }) {
34
58
  const from = addressOf(privateKey);
35
59
  const [chainId, nonce, gasPrice] = await Promise.all([
36
- rpcCall(urls, "eth_chainId", []), rpcCall(urls, "eth_getTransactionCount", [from, "pending"]), rpcCall(urls, "eth_gasPrice", []),
60
+ rpcCall(urls, "eth_chainId", []), rpcCall(urls, "eth_getTransactionCount", [from, "pending"]), suggestedGasPrice(urls),
37
61
  ]);
38
62
  const limit = gasLimit ?? (quantity(await rpcCall(urls, "eth_estimateGas", [{ from, to, data }])) * 12n / 10n + 10000n);
39
63
  const raw = signLegacyTransaction(privateKey, {
40
- nonce: quantity(nonce), gasPrice: quantity(gasPrice) * 12n / 10n, gasLimit: limit, to, data, chainId: quantity(chainId),
64
+ nonce: quantity(nonce), gasPrice, gasLimit: limit, to, data, chainId: quantity(chainId),
41
65
  });
42
66
  const hash = await rpcCall(urls, "eth_sendRawTransaction", [raw]);
43
67
  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";
@@ -1,9 +1,10 @@
1
1
  // Provider ranking from what the chain and the probe ledger prove, not from what anyone claims.
2
2
  import { encodeCall, keccak, hex } from "./eip712.mjs";
3
- import { rpcCall } from "./chain.mjs";
3
+ import { getLogs, 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;
@@ -31,9 +32,9 @@ async function ledgerEvents(network) {
31
32
  const events = [];
32
33
  for (let from = head > LOOKBACK_BLOCKS ? head - LOOKBACK_BLOCKS : 0n; from <= head; from += LOG_CHUNK) {
33
34
  const to = from + LOG_CHUNK - 1n < head ? from + LOG_CHUNK - 1n : head;
34
- for (const log of await rpcCall(network.rpc_urls, "eth_getLogs", [{ address: network.probe_ledger, topics: [PROBE_RECORDED],
35
- 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],
35
+ for (const log of await getLogs(network.rpc_urls, { address: network.probe_ledger, topics: [PROBE_RECORDED],
36
+ fromBlock: `0x${from.toString(16)}`, toBlock: `0x${to.toString(16)}` })) {
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] };
package/src/rewards.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // MYCO rewards: every hour's fees mint MYCO for the Consumers who paid them (80%), their Providers (10%,
2
2
  // 48 hours later, by success rate), Relays (7%) and keepers (3%). Find, show and claim an account's share.
3
3
  import { encodeCall, encodeWords, hex, keccak } from "./eip712.mjs";
4
- import { rpcCall, sendTransaction } from "./chain.mjs";
4
+ import { getLogs, rpcCall, sendTransaction } from "./chain.mjs";
5
5
 
6
6
  export const ROLES = ["consumer", "provider", "relay", "bridge"];
7
7
  const POINTS = hex(keccak(new TextEncoder().encode("Points(uint64,uint8,address,uint256)")));
@@ -18,8 +18,8 @@ export async function earnedBlocks(network, account) {
18
18
  const topic = `0x${account.toLowerCase().replace(/^0x/, "").padStart(64, "0")}`;
19
19
  const found = new Map();
20
20
  for (let start = Number(network.emission_block || 0); start <= head; start += LOG_CHUNK) {
21
- const logs = await rpcCall(network.rpc_urls, "eth_getLogs", [{ address: network.emission, topics: [POINTS, null, null, topic],
22
- fromBlock: `0x${start.toString(16)}`, toBlock: `0x${Math.min(head, start + LOG_CHUNK - 1).toString(16)}` }]);
21
+ const logs = await getLogs(network.rpc_urls, { address: network.emission, topics: [POINTS, null, null, topic],
22
+ fromBlock: `0x${start.toString(16)}`, toBlock: `0x${Math.min(head, start + LOG_CHUNK - 1).toString(16)}` });
23
23
  for (const log of logs) {
24
24
  const role = Number(BigInt(log.topics[2]));
25
25
  if (!found.has(role)) found.set(role, new Set());