brainchain-worker 0.18.2 → 0.18.4
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/README.md +57 -42
- package/dist/bond.js +55 -24
- package/dist/chain.js +58 -5
- package/dist/cli.js +84 -56
- package/dist/credit.js +4 -1
- package/dist/exit.js +82 -19
- package/dist/key.js +24 -10
- package/dist/mint.js +3 -1
- package/dist/network.js +2 -0
- package/dist/propose.js +29 -32
- package/dist/relay.js +18 -12
- package/dist/solana.js +177 -36
- package/dist/watch.js +80 -23
- package/dist/work.js +11 -1
- package/package.json +1 -1
package/dist/watch.js
CHANGED
|
@@ -2,9 +2,11 @@ import { concat, createPublicClient, createWalletClient, keccak256, parseAbi, to
|
|
|
2
2
|
import { brainchainTransport, executorAbi, reason } from "./chain.js";
|
|
3
3
|
import { chainOf } from "./network.js";
|
|
4
4
|
/**
|
|
5
|
-
* The constitution's
|
|
6
|
-
* crosses
|
|
7
|
-
*
|
|
5
|
+
* The calls the constitution's monitor knows, by the function a change calls.
|
|
6
|
+
* A change that crosses a floor is what the guardian's veto is for, and any
|
|
7
|
+
* other call counts as crossing one. Three known calls have no floor, and say
|
|
8
|
+
* what they do instead. Values in the units the contracts take: basis points,
|
|
9
|
+
* seconds, and USDG in 6 decimals.
|
|
8
10
|
*/
|
|
9
11
|
export const FLOORS = {
|
|
10
12
|
"0x85327a44": {
|
|
@@ -32,9 +34,9 @@ export const FLOORS = {
|
|
|
32
34
|
name: "setModel",
|
|
33
35
|
check: (w) => [w[1] === 0n && "a house model with no account", w[2] > 5000000n && "a house model priced above 5 USDG"].filter((r) => typeof r === "string"),
|
|
34
36
|
},
|
|
35
|
-
"0x16c38b3c": { name: "setPaused", check: () => [] },
|
|
36
|
-
"0x1f1e4d9f": { name: "setWorkerAllowed", check: () => [] },
|
|
37
|
-
"0xa9059cbb": { name: "transfer", check: () => [] },
|
|
37
|
+
"0x16c38b3c": { name: "setPaused", check: () => [], does: "pauses or resumes new posts" },
|
|
38
|
+
"0x1f1e4d9f": { name: "setWorkerAllowed", check: () => [], does: "allows or removes a worker" },
|
|
39
|
+
"0xa9059cbb": { name: "transfer", check: () => [], does: "moves house treasury funds" },
|
|
38
40
|
};
|
|
39
41
|
/** The static words after the selector; a call with dynamic arguments is read as far as its words go. */
|
|
40
42
|
export function argumentsOf(data) {
|
|
@@ -44,13 +46,13 @@ export function argumentsOf(data) {
|
|
|
44
46
|
words.push(BigInt(`0x${body.slice(i, i + 64)}`));
|
|
45
47
|
return words;
|
|
46
48
|
}
|
|
47
|
-
/** What a queued change does to the constitution: nothing,
|
|
49
|
+
/** What a queued change does to the constitution: nothing, the floors it crosses, or, for a known call with no floor, what it does. */
|
|
48
50
|
export function judge(data) {
|
|
49
51
|
const selector = data.slice(0, 10).toLowerCase();
|
|
50
52
|
const floor = FLOORS[selector];
|
|
51
53
|
if (!floor)
|
|
52
|
-
return { name: selector, broken: ["a call the
|
|
53
|
-
return { name: floor.name, broken: floor.check(argumentsOf(data)) };
|
|
54
|
+
return { name: selector, broken: ["a call the monitor does not know, which counts as breaking a floor"] };
|
|
55
|
+
return { name: floor.name, broken: floor.check(argumentsOf(data)), ...(floor.does ? { does: floor.does } : {}) };
|
|
54
56
|
}
|
|
55
57
|
const gateAbi = parseAbi(["function veto(uint256 changeId, string reason)", "function watcher() view returns (address)"]);
|
|
56
58
|
const logAbi = parseAbi(["function count() view returns (uint256)"]);
|
|
@@ -108,10 +110,53 @@ export async function requireWatcher(chain, log) {
|
|
|
108
110
|
throw new Error(`This key cannot veto: the gate's watcher is ${watcher}, not ${chain.account}. Set BRAINCHAIN_WORKER_KEY to the watcher's key, or run watch without --veto.`);
|
|
109
111
|
}
|
|
110
112
|
}
|
|
113
|
+
/** How long after a decision was recorded a record that cannot be read may hold a round's place. */
|
|
114
|
+
export const UNREADABLE_MS = 86_400_000;
|
|
115
|
+
/**
|
|
116
|
+
* A record that cannot be read stops the round and holds its place, so a host's
|
|
117
|
+
* outage costs no check. Only for a day from the decision's block time: one dead
|
|
118
|
+
* address would otherwise blind a watcher to every decision after it, so past
|
|
119
|
+
* that it is a finding, reported once, and the round moves on.
|
|
120
|
+
*/
|
|
121
|
+
function unreadable(why, recordedAt, now) {
|
|
122
|
+
if (recordedAt === undefined || !(now - Date.parse(recordedAt) >= UNREADABLE_MS))
|
|
123
|
+
throw new Error(why);
|
|
124
|
+
return { findings: [`the record could not be read a day or more after it was recorded: ${why}`] };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* What one decision's checks come to. A check that failed is a finding, whatever
|
|
128
|
+
* else could not be read. A check that could not run says nothing about the
|
|
129
|
+
* decision, so it follows the rule for an unreadable record. A record served
|
|
130
|
+
* without its work leaves its hash checks unrun, which is neither a finding nor a
|
|
131
|
+
* verified decision: `withheld` names them. With `refetch`, a verified decision
|
|
132
|
+
* comes with the record's bytes as served, for the report, and a failure to fetch
|
|
133
|
+
* them follows the same rule.
|
|
134
|
+
*/
|
|
135
|
+
export async function outcome(report, options = {}) {
|
|
136
|
+
const now = options.now ?? Date.now();
|
|
137
|
+
const said = (status) => report.checks.filter((c) => c.status === status).map((c) => `${c.name}: ${c.message}`);
|
|
138
|
+
const findings = said("fail");
|
|
139
|
+
if (findings.length > 0)
|
|
140
|
+
return { findings };
|
|
141
|
+
const unread = said("error");
|
|
142
|
+
if (unread.length > 0)
|
|
143
|
+
return unreadable(unread.join("; "), report.recordedAt, now);
|
|
144
|
+
if (report.withheld)
|
|
145
|
+
return { findings, withheld: report.checks.filter((c) => c.status === "skip" && c.name.endsWith("-hash")).map((c) => c.name) };
|
|
146
|
+
if (!options.refetch || !report.payloadURI)
|
|
147
|
+
return { findings };
|
|
148
|
+
try {
|
|
149
|
+
return { findings, record: await options.refetch(report.payloadURI) };
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
return unreadable(reason(error), report.recordedAt, now);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
111
155
|
/** The service's prefix on a report; the worker signs the prefix and the report's JSON text. */
|
|
112
156
|
export const REPORT_PREFIX = "BrainChain watchtower report\n";
|
|
113
|
-
/** A report lists at most this many decisions checked. */
|
|
157
|
+
/** A report lists at most this many decisions checked and this many findings; a longer round sends several. */
|
|
114
158
|
export const MAX_CHECKED = 200;
|
|
159
|
+
export const MAX_FINDINGS = 50;
|
|
115
160
|
/** The proof a record was fetched: the hash of its bytes followed by the worker's address, which nothing on chain gives away. */
|
|
116
161
|
export const fetchProof = (bytes, worker) => keccak256(concat([bytes, toBytes(worker)]));
|
|
117
162
|
/** The service answered and will not take this report, so sending it again would not help. */
|
|
@@ -123,14 +168,17 @@ export class ReportRefused extends Error {
|
|
|
123
168
|
* Resolves to the line to log.
|
|
124
169
|
*/
|
|
125
170
|
export function createReporter(network, account, fetcher = fetch) {
|
|
126
|
-
|
|
171
|
+
// The service takes a report only if it was signed later than the last it took, by the second.
|
|
172
|
+
let lastAt = 0;
|
|
173
|
+
async function send(checked, findings) {
|
|
174
|
+
lastAt = Math.max(Math.floor(Date.now() / 1000), lastAt + 1);
|
|
127
175
|
const report = {
|
|
128
176
|
worker: account.address,
|
|
129
177
|
chainId: network.chainId,
|
|
130
178
|
log: network.contracts.log,
|
|
131
|
-
at:
|
|
132
|
-
checked: checked.
|
|
133
|
-
findings: findings.
|
|
179
|
+
at: lastAt,
|
|
180
|
+
checked: checked.map((c) => ({ id: Number(c.id), proof: c.proof })),
|
|
181
|
+
findings: findings.map((f) => ({ id: Number(f.id), checks: f.checks.slice(0, 20).map((c) => c.slice(0, 400)) })),
|
|
134
182
|
};
|
|
135
183
|
const text = JSON.stringify(report);
|
|
136
184
|
const signature = await account.signMessage({ message: `${REPORT_PREFIX}${text}` });
|
|
@@ -148,6 +196,13 @@ export function createReporter(network, account, fetcher = fetch) {
|
|
|
148
196
|
return body.mark
|
|
149
197
|
? `reported to ${network.name}: ${body.checked} decisions re-verified in all, watchtower mark since ${body.mark.since}`
|
|
150
198
|
: `reported to ${network.name}: ${body.checked} of the ${body.markAfter} re-verified decisions that earn the watchtower mark`;
|
|
199
|
+
}
|
|
200
|
+
return async (checked, findings) => {
|
|
201
|
+
let line = await send(checked.slice(0, MAX_CHECKED), findings.slice(0, MAX_FINDINGS));
|
|
202
|
+
for (let i = 1; MAX_CHECKED * i < checked.length || MAX_FINDINGS * i < findings.length; i++) {
|
|
203
|
+
line = await send(checked.slice(MAX_CHECKED * i, MAX_CHECKED * (i + 1)), findings.slice(MAX_FINDINGS * i, MAX_FINDINGS * (i + 1)));
|
|
204
|
+
}
|
|
205
|
+
return line;
|
|
151
206
|
};
|
|
152
207
|
}
|
|
153
208
|
/**
|
|
@@ -176,7 +231,7 @@ export function createWatch(options) {
|
|
|
176
231
|
const key = change.changeId.toString();
|
|
177
232
|
if (judged.has(key))
|
|
178
233
|
continue;
|
|
179
|
-
const { name, broken } = judge(change.data);
|
|
234
|
+
const { name, broken, does } = judge(change.data);
|
|
180
235
|
// A failed read leaves the change unjudged and the block range unread, so the next round comes back to it.
|
|
181
236
|
const open = await chain.open(change.changeId);
|
|
182
237
|
judged.add(key);
|
|
@@ -185,9 +240,9 @@ export function createWatch(options) {
|
|
|
185
240
|
log(`change ${change.changeId}: ${name} on ${change.target}${broken.length > 0 ? ` broke the constitution (${why})` : ""}; closed, it has run or was vetoed`);
|
|
186
241
|
continue;
|
|
187
242
|
}
|
|
188
|
-
const runsAt = new Date(Number(change.eta) * 1000).toISOString().replace("T", " ").slice(0,
|
|
243
|
+
const runsAt = new Date(Number(change.eta) * 1000).toISOString().replace("T", " ").slice(0, 19);
|
|
189
244
|
if (broken.length === 0) {
|
|
190
|
-
log(`change ${change.changeId}: ${name} on ${change.target}, within the constitution, runs after ${runsAt} UTC`);
|
|
245
|
+
log(`change ${change.changeId}: ${name} on ${change.target}${does ? ` ${does}, which no floor covers` : ", within the constitution"}, runs after ${runsAt} UTC`);
|
|
191
246
|
continue;
|
|
192
247
|
}
|
|
193
248
|
findings += 1;
|
|
@@ -239,8 +294,11 @@ export function createWatch(options) {
|
|
|
239
294
|
let reached = seenDecision;
|
|
240
295
|
for (let id = seenDecision + 1n; id <= count; id++) {
|
|
241
296
|
try {
|
|
242
|
-
const { findings: wrong, record } = await verify(id);
|
|
243
|
-
if (
|
|
297
|
+
const { findings: wrong, record, withheld } = await verify(id);
|
|
298
|
+
if (withheld) {
|
|
299
|
+
log(`decision ${id} withheld: its record is served without the work, so ${withheld.join(" and ")} did not run; the checks that ran agree with the chain`);
|
|
300
|
+
}
|
|
301
|
+
else if (wrong.length === 0) {
|
|
244
302
|
log(`decision ${id} verified`);
|
|
245
303
|
if (record)
|
|
246
304
|
checked.push({ id, proof: fetchProof(record, chain.account) });
|
|
@@ -266,11 +324,10 @@ export function createWatch(options) {
|
|
|
266
324
|
}
|
|
267
325
|
catch (error) {
|
|
268
326
|
log(`report not taken: ${reason(error)}`);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
327
|
+
incomplete = true;
|
|
328
|
+
// Only an answer from the service settles a report: a refusal would only repeat, and these decisions are checked and sent again otherwise.
|
|
329
|
+
if (!(error instanceof ReportRefused))
|
|
272
330
|
reached = seenDecision;
|
|
273
|
-
}
|
|
274
331
|
}
|
|
275
332
|
}
|
|
276
333
|
seenDecision = reached;
|
package/dist/work.js
CHANGED
|
@@ -36,6 +36,16 @@ export const printable = (text) => stripAnsi(text)
|
|
|
36
36
|
.replace(/[\t\n\r]+/g, " ")
|
|
37
37
|
.replace(/[\x00-\x1f\x7f-\x9f]/g, "")
|
|
38
38
|
.replace(new RegExp(HIDDEN.source, "g"), "");
|
|
39
|
+
/** The longest title the log quotes. */
|
|
40
|
+
const LOGGED_TITLE = 80;
|
|
41
|
+
/**
|
|
42
|
+
* A title from a stranger as the log shows it: one line, cut short and JSON-quoted,
|
|
43
|
+
* so it cannot close its quotes and pass for the worker's own words.
|
|
44
|
+
*/
|
|
45
|
+
export function loggedTitle(title) {
|
|
46
|
+
const characters = [...printable(title)];
|
|
47
|
+
return JSON.stringify(characters.length > LOGGED_TITLE ? `${characters.slice(0, LOGGED_TITLE - 1).join("")}…` : characters.join(""));
|
|
48
|
+
}
|
|
39
49
|
export function parseBrief(text) {
|
|
40
50
|
let value;
|
|
41
51
|
try {
|
|
@@ -437,7 +447,7 @@ export function createWork(options) {
|
|
|
437
447
|
// A stop asked for while the brief downloaded or the balance was read starts no command.
|
|
438
448
|
if (options.stopping?.())
|
|
439
449
|
continue;
|
|
440
|
-
log(`task ${task.id}: working on
|
|
450
|
+
log(`task ${task.id}: working on ${loggedTitle(brief.title)}`);
|
|
441
451
|
const input = options.format === "json" ? JSON.stringify(taskInput(task, brief)) : taskPrompt(task, brief);
|
|
442
452
|
const timeoutMs = Math.min(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, Number(task.deadline - MARGIN_SEC - now) * 1000);
|
|
443
453
|
const output = stripAnsi(await command.run(input, { BRAINCHAIN_TASK_ID: key }, timeoutMs, remember).finally(forget)).trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainchain-worker",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.4",
|
|
4
4
|
"description": "Earn on BrainChain and propose changes to it: stake, apply, do the tasks Jev hires your agent for, and bond BRAIN to put a change to Jev.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "BrainChain <hello@usebrainchain.com>",
|