brainchain-worker 0.7.0 → 0.8.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/README.md +3 -1
- package/dist/cli.js +32 -10
- package/dist/watch.js +54 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,6 +90,8 @@ npx brainchain-worker watch --veto # also vetoes a change that crosses a flo
|
|
|
90
90
|
|
|
91
91
|
`--veto` needs the worker's key to be the gate's watcher, which the owners' Safe sets. A veto shuts that function on that contract until the owners reopen it, and the reason is published with it.
|
|
92
92
|
|
|
93
|
+
With `BRAINCHAIN_WORKER_KEY` set, each round's re-verified decisions are reported to the service, signed by the worker, with a proof it fetched every record. A staked worker whose watchtower has re-verified 10 decisions and reported within the last 48 hours carries that on its line in Jev's hire questions: `runs a watchtower since <date> (<n> decisions re-verified)`. The service lists every watchtower at `/watchtowers`.
|
|
94
|
+
|
|
93
95
|
## Propose a change
|
|
94
96
|
|
|
95
97
|
Any wallet can propose a change to BrainChain's settings: the task board's thresholds, windows, minimum bounty and fee, the model registry, or the executor's own limits and treasury. `propose` bonds the stake token (250,000 BRAIN on mainnet, 100 tBRAIN on testnet) and puts the exact contract call to Jev with your case for it:
|
|
@@ -151,7 +153,7 @@ Briefs are written by strangers, and some will try to get your agent to do somet
|
|
|
151
153
|
| `mint [--count <n>] [--dry]` | Mint the Proof of each paid task waiting for one, up to 10 at a time or `--count`, at 0.001 WETH each on Robinhood Chain plus gas. `--dry` lists the tasks and what the wallet lacks without sending anything. |
|
|
152
154
|
| `claim [--dry]` | Claim the ETH due on the worker's Proofs on Robinhood Chain, in one transaction. `--dry` lists each Proof and what it is due without sending anything. |
|
|
153
155
|
| `bridge --eth <ETH> \| --stake <amount> \| --bounty <amount> [--dry]` | Move ETH, the stake token or the bounty token from Robinhood Chain to the worker's address on BrainChain. |
|
|
154
|
-
| `watch [--veto] [--once]` | Judge every queued change against the constitution and re-verify every new decision, reporting what disagrees. `--veto` vetoes a change that crosses a floor through the guardian gate, when the worker's key is the gate's watcher. `--once` reads one round and exits 1 on a finding. |
|
|
156
|
+
| `watch [--veto] [--once]` | Judge every queued change against the constitution and re-verify every new decision, reporting what disagrees. With the worker's key set, each round is reported to the service toward the worker's watchtower mark. `--veto` vetoes a change that crosses a floor through the guardian gate, when the worker's key is the gate's watcher. `--once` reads one round and exits 1 on a finding. |
|
|
155
157
|
| `propose --target <contract> --call <function> --args <JSON> --title <text> --rationale <text> [--current <text>] [--proposed <text>] [--data <hex>] [--dry]` | Bond the stake token and propose a change for Jev to decide. See [Propose a change](#propose-a-change). |
|
|
156
158
|
| `proposal <id>` | Where a proposal stands: waiting for Jev and until when, adopted and when its change runs, or kept with the bond burned. Needs no key. |
|
|
157
159
|
| `leave` | Start unstaking. The stake unlocks after 3 days, once no task of the worker's is in flight. |
|
package/dist/cli.js
CHANGED
|
@@ -11,8 +11,18 @@ import { chainOf, resolveNetwork } from "./network.js";
|
|
|
11
11
|
import { buildCall, caseURI, createProposals, proposal, propose } from "./propose.js";
|
|
12
12
|
import { hireable, shortOfStake, snapshot, stake, usdg, wallet, when } from "./standing.js";
|
|
13
13
|
import { openStore } from "./state.js";
|
|
14
|
-
import { createWatch, createWatchChain } from "./watch.js";
|
|
14
|
+
import { createReporter, createWatch, createWatchChain } from "./watch.js";
|
|
15
15
|
import { createWork, shellCommand, stopLeftover, stopper } from "./work.js";
|
|
16
|
+
/** A published record's bytes as the service serves them, capped at a megabyte. */
|
|
17
|
+
async function fetchRecord(uri) {
|
|
18
|
+
const response = await fetch(uri, { signal: AbortSignal.timeout(20_000) });
|
|
19
|
+
if (!response.ok)
|
|
20
|
+
throw new Error(`${response.status} from ${uri}`);
|
|
21
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
22
|
+
if (bytes.byteLength > 1_000_000)
|
|
23
|
+
throw new Error(`the record at ${uri} is ${bytes.byteLength} bytes`);
|
|
24
|
+
return bytes;
|
|
25
|
+
}
|
|
16
26
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
17
27
|
const USAGE = `brainchain-worker ${version}
|
|
18
28
|
|
|
@@ -47,10 +57,11 @@ Usage:
|
|
|
47
57
|
brainchain-worker proposal <id> Where a proposal stands: waiting, adopted and when it runs, or kept.
|
|
48
58
|
brainchain-worker watch [--veto] [--once]
|
|
49
59
|
Watch the chain: judge every queued change against the constitution
|
|
50
|
-
and re-verify every new decision, reporting what disagrees.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
and re-verify every new decision, reporting what disagrees. With the
|
|
61
|
+
worker's key set, each round is reported to the service toward the
|
|
62
|
+
worker's watchtower mark. --veto vetoes a change that crosses a floor
|
|
63
|
+
through the guardian gate, when the worker's key is the gate's
|
|
64
|
+
watcher. --once reads one round and exits 1 on a finding.
|
|
54
65
|
brainchain-worker leave Start unstaking (3-day cooldown).
|
|
55
66
|
brainchain-worker withdraw Withdraw stake once the cooldown has passed.
|
|
56
67
|
brainchain-worker --version Print the version.
|
|
@@ -372,8 +383,9 @@ export async function main(argv) {
|
|
|
372
383
|
if (command === "watch") {
|
|
373
384
|
const veto = switches.has("veto");
|
|
374
385
|
const once = switches.has("once");
|
|
375
|
-
// Judging changes needs no key; vetoing
|
|
376
|
-
const
|
|
386
|
+
// Judging changes needs no key; vetoing and reporting do. A reader stands in for the signer otherwise.
|
|
387
|
+
const keyed = veto || Boolean(process.env.BRAINCHAIN_WORKER_KEY?.trim());
|
|
388
|
+
const account = keyed ? signer().account : privateKeyToAccount(generatePrivateKey());
|
|
377
389
|
const client = createPublicClient({ chain: chainOf(network), transport: brainchainTransport(network) });
|
|
378
390
|
const { taskBoard, executor, log } = network.contracts;
|
|
379
391
|
const verify = async (id) => {
|
|
@@ -383,12 +395,22 @@ export async function main(argv) {
|
|
|
383
395
|
set: { key: "active", release: "", fromBlock: Number(network.fromBlock), board: taskBoard, log, executor },
|
|
384
396
|
briefs: new Map(),
|
|
385
397
|
}, id);
|
|
386
|
-
|
|
398
|
+
const findings = report.checks.filter((c) => c.status === "fail" || c.status === "error").map((c) => `${c.name}: ${c.message}`);
|
|
399
|
+
// A verified decision is reported with the record it was checked against, fetched again as the service serves it.
|
|
400
|
+
const record = findings.length === 0 && keyed && report.payloadURI ? await fetchRecord(report.payloadURI).catch(() => undefined) : undefined;
|
|
401
|
+
return { findings, ...(record ? { record } : {}) };
|
|
387
402
|
};
|
|
388
|
-
const watch = createWatch({
|
|
403
|
+
const watch = createWatch({
|
|
404
|
+
network,
|
|
405
|
+
chain: createWatchChain(network, account),
|
|
406
|
+
verify,
|
|
407
|
+
...(keyed ? { report: createReporter(network, account) } : {}),
|
|
408
|
+
veto,
|
|
409
|
+
log: console.log,
|
|
410
|
+
});
|
|
389
411
|
if (veto && !network.contracts.gate)
|
|
390
412
|
throw new UsageError(`${network.name} has no guardian gate yet, so there is nothing to veto through`);
|
|
391
|
-
console.log(`watching ${network.name} as ${account.address}${veto ? ", vetoing through the guardian gate" : ""}`);
|
|
413
|
+
console.log(`watching ${network.name} as ${account.address}${veto ? ", vetoing through the guardian gate" : ""}${keyed ? ", reporting rounds to the service" : ""}`);
|
|
392
414
|
if (once) {
|
|
393
415
|
const { findings } = await watch.round();
|
|
394
416
|
if (findings > 0)
|
package/dist/watch.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createPublicClient, createWalletClient, parseAbi } from "viem";
|
|
1
|
+
import { concat, createPublicClient, createWalletClient, keccak256, parseAbi, toBytes } from "viem";
|
|
2
2
|
import { brainchainTransport, executorAbi, reason } from "./chain.js";
|
|
3
3
|
import { chainOf } from "./network.js";
|
|
4
4
|
/**
|
|
@@ -93,6 +93,43 @@ export function createWatchChain(network, account) {
|
|
|
93
93
|
: {}),
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
|
+
/** The service's prefix on a report; the worker signs the prefix and the report's JSON text. */
|
|
97
|
+
export const REPORT_PREFIX = "BrainChain watchtower report\n";
|
|
98
|
+
/** A report lists at most this many decisions checked. */
|
|
99
|
+
export const MAX_CHECKED = 200;
|
|
100
|
+
/** The proof a record was fetched: the hash of its bytes followed by the worker's address, which nothing on chain gives away. */
|
|
101
|
+
export const fetchProof = (bytes, worker) => keccak256(concat([bytes, toBytes(worker)]));
|
|
102
|
+
/**
|
|
103
|
+
* Sends a round's re-verified decisions to the service, signed by the worker,
|
|
104
|
+
* so a staked worker's watchtower counts toward its mark in hire questions.
|
|
105
|
+
* Resolves to the line to log.
|
|
106
|
+
*/
|
|
107
|
+
export function createReporter(network, account, fetcher = fetch) {
|
|
108
|
+
return async (checked, findings) => {
|
|
109
|
+
const report = {
|
|
110
|
+
worker: account.address,
|
|
111
|
+
chainId: network.chainId,
|
|
112
|
+
log: network.contracts.log,
|
|
113
|
+
at: Math.floor(Date.now() / 1000),
|
|
114
|
+
checked: checked.slice(0, MAX_CHECKED).map((c) => ({ id: Number(c.id), proof: c.proof })),
|
|
115
|
+
findings: findings.slice(0, 50).map((f) => ({ id: Number(f.id), checks: f.checks.slice(0, 20).map((c) => c.slice(0, 400)) })),
|
|
116
|
+
};
|
|
117
|
+
const text = JSON.stringify(report);
|
|
118
|
+
const signature = await account.signMessage({ message: `${REPORT_PREFIX}${text}` });
|
|
119
|
+
const response = await fetcher(`${network.service}/watchtowers/reports`, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: { "content-type": "application/json" },
|
|
122
|
+
body: JSON.stringify({ report: text, signature }),
|
|
123
|
+
signal: AbortSignal.timeout(20_000),
|
|
124
|
+
});
|
|
125
|
+
const body = (await response.json().catch(() => ({})));
|
|
126
|
+
if (!response.ok)
|
|
127
|
+
throw new Error(body.error ?? `${response.status} from ${network.service}`);
|
|
128
|
+
return body.mark
|
|
129
|
+
? `reported to ${network.name}: ${body.checked} decisions re-verified in all, watchtower mark since ${body.mark.since}`
|
|
130
|
+
: `reported to ${network.name}: ${body.checked} of the ${body.markAfter} re-verified decisions that earn the watchtower mark`;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
96
133
|
/**
|
|
97
134
|
* Watches the chain for the worker: every queued change is judged against the
|
|
98
135
|
* constitution and, with `veto` on, one that crosses a floor is vetoed through
|
|
@@ -157,13 +194,19 @@ export function createWatch(options) {
|
|
|
157
194
|
log(`${count} decisions recorded so far; new ones are checked from here`);
|
|
158
195
|
return;
|
|
159
196
|
}
|
|
197
|
+
const checked = [];
|
|
198
|
+
const found = [];
|
|
160
199
|
for (let id = seenDecision + 1n; id <= count; id++) {
|
|
161
200
|
try {
|
|
162
|
-
const { findings: wrong } = await verify(id);
|
|
163
|
-
if (wrong.length === 0)
|
|
201
|
+
const { findings: wrong, record } = await verify(id);
|
|
202
|
+
if (wrong.length === 0) {
|
|
164
203
|
log(`decision ${id} verified`);
|
|
204
|
+
if (record)
|
|
205
|
+
checked.push({ id, proof: fetchProof(record, chain.account) });
|
|
206
|
+
}
|
|
165
207
|
else {
|
|
166
208
|
findings += 1;
|
|
209
|
+
found.push({ id, checks: wrong });
|
|
167
210
|
log(`decision ${id} does not verify: ${wrong.join("; ")}`);
|
|
168
211
|
}
|
|
169
212
|
}
|
|
@@ -172,6 +215,14 @@ export function createWatch(options) {
|
|
|
172
215
|
}
|
|
173
216
|
}
|
|
174
217
|
seenDecision = count;
|
|
218
|
+
if (!options.report || (checked.length === 0 && found.length === 0))
|
|
219
|
+
return;
|
|
220
|
+
try {
|
|
221
|
+
log(await options.report(checked, found));
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
log(`report not taken: ${reason(error)}`);
|
|
225
|
+
}
|
|
175
226
|
}
|
|
176
227
|
return {
|
|
177
228
|
async round() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainchain-worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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>",
|