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.
- package/networks/mycomesh-v11-sepolia.json +1 -1
- package/package.json +1 -1
- package/src/chain.mjs +15 -2
- package/src/probes.mjs +32 -0
- package/src/reputation.mjs +17 -11
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"stablecoin": "0xbce27efad4191277167fe76819581a97959048cf",
|
|
7
7
|
"registry": "0x0ff3210e8e8abf5f5bfa73a39e60db7416b6c8c7",
|
|
8
8
|
"relay_directory": "0xa0aaad9393f51cb5786c69f152133a174588bdab",
|
|
9
|
-
"probe_ledger": "
|
|
9
|
+
"probe_ledger": "0x1f26a2285eb366d0fd201bea6969abd1aaacb62b",
|
|
10
10
|
"emission": "0x85ee0e8f63400041f0c76010c590c330bc135a8d",
|
|
11
11
|
"token": "0x620d026ad854fb17c7b16534a0f28ef5499e2d4a",
|
|
12
12
|
"emission_block": 11815544,
|
package/package.json
CHANGED
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"]),
|
|
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
|
|
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";
|
package/src/reputation.mjs
CHANGED
|
@@ -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]),
|
|
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
|
-
|
|
65
|
-
if (!relay || verified >= MAX_VERIFIED) continue;
|
|
65
|
+
if (verified >= MAX_VERIFIED) continue;
|
|
66
66
|
verified += 1;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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] };
|