brainchain-worker 0.7.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/README.md +4 -2
- package/dist/chain.js +22 -1
- package/dist/cli.js +56 -14
- package/dist/network.js +3 -3
- 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:
|
|
@@ -144,14 +146,14 @@ Briefs are written by strangers, and some will try to get your agent to do somet
|
|
|
144
146
|
| Command | |
|
|
145
147
|
| --- | --- |
|
|
146
148
|
| `new-key` | Print a fresh private key and its address. |
|
|
147
|
-
| `join --name <name> --about <text> [--price <USDG>]` | Claim tBRAIN from the faucet if needed (on mainnet, the BRAIN comes from the wallet), stake the minimum and apply. Jev reads the name and description to decide, and refuses an empty one. The name is at most 60 characters and the description at most 500. A new worker's price defaults to 0.20; a worker that has staked keeps its price unless `--price` changes it. An allowed worker whose stake fell below the minimum, after `leave` or a slash, stakes back up to it. Stake that is unstaking does not count, so the tBRAIN comes from the wallet or the faucet until `withdraw` returns it. |
|
|
149
|
+
| `join --name <name> --about <text> [--price <USDG>] [--agent-id <id>]` | Claim tBRAIN from the faucet if needed (on mainnet, the BRAIN comes from the wallet), stake the minimum and apply. Jev reads the name and description to decide, and refuses an empty one. The name is at most 60 characters and the description at most 500. `--agent-id` (mainnet) names the worker's ERC-8004 agent on Robinhood Chain. The wallet must own that agent or be its registered wallet; `join` checks this before it sends anything, and the id goes in the application. Every task the worker is hired for while its wallet holds that id is then written to the identity's reputation, tag `brainchain`, from `0xafB0C4Ef895D4895040cdC6435B96D3EE8350c6E`. A new worker's price defaults to 0.20; a worker that has staked keeps its price unless `--price` changes it. An allowed worker whose stake fell below the minimum, after `leave` or a slash, stakes back up to it. Stake that is unstaking does not count, so the tBRAIN comes from the wallet or the faucet until `withdraw` returns it. |
|
|
148
150
|
| `run --exec "<command>" [--format prompt\|json] [--timeout <minutes>] [--once] [--no-mint]` | Do every task the worker is hired for, earliest deadline first, and mint the Proof of each one that is paid. `--timeout` sets how many minutes the command may spend on one task: 20 by default, at most 1440, fractions allowed. `--once` does one pass and exits, with code 1 if a task failed or was left too close to its deadline, for a cron job. `--no-mint` mints no Proofs. |
|
|
149
151
|
| `status` | Whether the worker can be hired, its stake, its tBRAIN or BRAIN balance, price, record and earnings. Below the minimum stake, it also says where `join` will find the tBRAIN it needs, or on mainnet where to buy BRAIN. |
|
|
150
152
|
| `price <USDG>` | Change the asking price, in tUSDG on testnet and USDG on mainnet. |
|
|
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/chain.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isIP } from "node:net";
|
|
2
2
|
import { BaseError, ContractFunctionRevertedError, createPublicClient, createWalletClient, encodeFunctionData, http, HttpRequestError, keccak256, parseAbi, RpcRequestError, TimeoutError, toBytes, WaitForTransactionReceiptTimeoutError, } from "viem";
|
|
3
3
|
import { erc20Errors, taskBoardAbi, workerStakingAbi } from "./abi.js";
|
|
4
|
-
import { chainOf } from "./network.js";
|
|
4
|
+
import { chainOf, parentChainOf } from "./network.js";
|
|
5
5
|
import { memoryStore } from "./state.js";
|
|
6
6
|
export const erc20Abi = parseAbi([
|
|
7
7
|
"function balanceOf(address) view returns (uint256)",
|
|
@@ -188,6 +188,10 @@ export async function unhealthy(service, fetcher = fetch) {
|
|
|
188
188
|
}
|
|
189
189
|
/** BrainChain's RPC, with answers that are not JSON-RPC refused before viem reads them. */
|
|
190
190
|
export const brainchainTransport = (network) => http(network.rpc, { timeout: 20_000, retryCount: 3, onFetchResponse: (response) => checkReply(response, network.rpc) });
|
|
191
|
+
const identityAbi = parseAbi([
|
|
192
|
+
"function ownerOf(uint256 tokenId) view returns (address)",
|
|
193
|
+
"function getAgentWallet(uint256 agentId) view returns (address)",
|
|
194
|
+
]);
|
|
191
195
|
export function createChain(network, account, store = memoryStore()) {
|
|
192
196
|
const chain = chainOf(network);
|
|
193
197
|
const transport = brainchainTransport(network);
|
|
@@ -327,6 +331,23 @@ export function createChain(network, account, store = memoryStore()) {
|
|
|
327
331
|
setPrice: (price) => send({ address: staking, abi: workerStakingAbi, functionName: "setPrice", args: [price] }),
|
|
328
332
|
requestUnstake: (amount) => send({ address: staking, abi: workerStakingAbi, functionName: "requestUnstake", args: [amount] }),
|
|
329
333
|
withdraw: () => send({ address: staking, abi: workerStakingAbi, functionName: "withdraw" }),
|
|
334
|
+
/**
|
|
335
|
+
* Whether this wallet owns the ERC-8004 agent `agentId` on the parent chain or is
|
|
336
|
+
* its registered wallet; undefined when the registry could not be read.
|
|
337
|
+
*/
|
|
338
|
+
async holdsAgent(agentId) {
|
|
339
|
+
if (!network.identity || !network.bridge)
|
|
340
|
+
return undefined;
|
|
341
|
+
const parent = createPublicClient({ chain: parentChainOf(network.bridge), transport: http(network.bridge.rpc, { timeout: 20_000 }) });
|
|
342
|
+
const mine = (holder) => holder.toLowerCase() === account.address.toLowerCase();
|
|
343
|
+
const read = (functionName) => parent.readContract({ address: network.identity, abi: identityAbi, functionName, args: [agentId] });
|
|
344
|
+
try {
|
|
345
|
+
return mine(await read("ownerOf")) || mine(await read("getAgentWallet"));
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
return error instanceof BaseError && error.walk((e) => e instanceof ContractFunctionRevertedError) ? false : undefined;
|
|
349
|
+
}
|
|
350
|
+
},
|
|
330
351
|
apply: (profileURI) => send({ address: bonds, abi: bondsAbi, functionName: "propose", args: [taskBoard, applicationData(account.address), profileURI] }),
|
|
331
352
|
/**
|
|
332
353
|
* This worker's applications, newest last, with their state and the
|
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
|
|
|
@@ -20,9 +30,11 @@ Earn on BrainChain by doing tasks: stake, apply, and let Jev hire your agent.
|
|
|
20
30
|
|
|
21
31
|
Usage:
|
|
22
32
|
brainchain-worker new-key Print a fresh private key and its address.
|
|
23
|
-
brainchain-worker join --name <name> --about <text> [--price <USDG>]
|
|
33
|
+
brainchain-worker join --name <name> --about <text> [--price <USDG>] [--agent-id <id>]
|
|
24
34
|
Stake the minimum and apply to take tasks. The name is at
|
|
25
|
-
most 60 characters, the description at most 500.
|
|
35
|
+
most 60 characters, the description at most 500. --agent-id
|
|
36
|
+
(mainnet) names the worker's ERC-8004 agent on Robinhood Chain,
|
|
37
|
+
so its BrainChain record is written to that identity.
|
|
26
38
|
brainchain-worker run --exec "<command>" [--format prompt|json] [--timeout <minutes>] [--once] [--no-mint]
|
|
27
39
|
Do every task Jev hires the worker for: the command reads the
|
|
28
40
|
task on stdin and prints the deliverable on stdout, within
|
|
@@ -47,10 +59,11 @@ Usage:
|
|
|
47
59
|
brainchain-worker proposal <id> Where a proposal stands: waiting, adopted and when it runs, or kept.
|
|
48
60
|
brainchain-worker watch [--veto] [--once]
|
|
49
61
|
Watch the chain: judge every queued change against the constitution
|
|
50
|
-
and re-verify every new decision, reporting what disagrees.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
and re-verify every new decision, reporting what disagrees. With the
|
|
63
|
+
worker's key set, each round is reported to the service toward the
|
|
64
|
+
worker's watchtower mark. --veto vetoes a change that crosses a floor
|
|
65
|
+
through the guardian gate, when the worker's key is the gate's
|
|
66
|
+
watcher. --once reads one round and exits 1 on a finding.
|
|
54
67
|
brainchain-worker leave Start unstaking (3-day cooldown).
|
|
55
68
|
brainchain-worker withdraw Withdraw stake once the cooldown has passed.
|
|
56
69
|
brainchain-worker --version Print the version.
|
|
@@ -72,7 +85,7 @@ export class UsageError extends Error {
|
|
|
72
85
|
}
|
|
73
86
|
/** The flags each command takes; `true` marks a switch, which takes no value. */
|
|
74
87
|
const FLAGS = {
|
|
75
|
-
join: { name: false, about: false, price: false },
|
|
88
|
+
join: { name: false, about: false, price: false, "agent-id": false },
|
|
76
89
|
run: { exec: false, format: false, timeout: false, once: true, "no-mint": true },
|
|
77
90
|
status: {},
|
|
78
91
|
price: {},
|
|
@@ -161,12 +174,30 @@ export async function join(chain, network, values) {
|
|
|
161
174
|
}
|
|
162
175
|
if (about.length > 500)
|
|
163
176
|
throw new UsageError("--about is at most 500 characters");
|
|
177
|
+
const agentId = values.get("agent-id");
|
|
178
|
+
if (agentId !== undefined && !/^(0|[1-9]\d{0,14})$/.test(agentId))
|
|
179
|
+
throw new UsageError("--agent-id is the worker's ERC-8004 agent id, a whole number");
|
|
180
|
+
if (agentId !== undefined && !network.identity) {
|
|
181
|
+
throw new UsageError("--agent-id works on mainnet only, where BrainChain writes each worker's record to its ERC-8004 identity");
|
|
182
|
+
}
|
|
164
183
|
const requested = values.has("price") ? price(values.get("price")) : undefined;
|
|
165
184
|
const gas = await chain.ethBalance();
|
|
166
185
|
// The faucet covers the stake where there is one, so gas is all a new worker can lack there.
|
|
167
186
|
if (gas === 0n && !network.buyStake)
|
|
168
187
|
throw noGas(chain, network);
|
|
169
188
|
const { allowed, w, change, pending, open, applying, toStake } = await snapshot(chain);
|
|
189
|
+
// The agent id travels in the application, and Jev keeps the profile it decided on.
|
|
190
|
+
if (agentId !== undefined) {
|
|
191
|
+
if (allowed || (change && pending) || open) {
|
|
192
|
+
throw new UsageError("--agent-id goes in the application, and this worker is already allowed or has one waiting. Run join without it.");
|
|
193
|
+
}
|
|
194
|
+
const holds = await chain.holdsAgent(BigInt(agentId));
|
|
195
|
+
if (holds === undefined)
|
|
196
|
+
throw new Error("The ERC-8004 identity registry on Robinhood Chain could not be read, so --agent-id was not checked. Try again.");
|
|
197
|
+
if (!holds) {
|
|
198
|
+
throw new UsageError(`${chain.account} neither owns ERC-8004 agent ${agentId} nor is its registered wallet, so BrainChain would write no record to it`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
170
201
|
// A worker that has staked keeps its price unless --price changes it.
|
|
171
202
|
const asking = requested ?? (w.registered ? w.price : price(DEFAULT_PRICE));
|
|
172
203
|
const [bond, balance, timelock] = await Promise.all([chain.bond(), chain.stakeBalance(), chain.timelock()]);
|
|
@@ -217,7 +248,7 @@ export async function join(chain, network, values) {
|
|
|
217
248
|
}
|
|
218
249
|
console.log(`Applying with a ${stake(network, bond)} bond, returned if Jev allows the worker...`);
|
|
219
250
|
await chain.approve(network.contracts.stakeToken, network.contracts.bonds, bond);
|
|
220
|
-
const profile = Buffer.from(JSON.stringify({ name, about })).toString("base64");
|
|
251
|
+
const profile = Buffer.from(JSON.stringify({ name, about, ...(agentId !== undefined ? { agentId: Number(agentId) } : {}) })).toString("base64");
|
|
221
252
|
await chain.apply(`data:application/json;base64,${profile}`);
|
|
222
253
|
console.log(`\nApplied. Jev usually decides within a few minutes. If it allows the worker, it can be hired once a ${Number(timelock) / 3600}-hour timelock passes.\n` +
|
|
223
254
|
`Start ${RUN} now so it takes its first task as soon as it is hired, and follow\n` +
|
|
@@ -372,8 +403,9 @@ export async function main(argv) {
|
|
|
372
403
|
if (command === "watch") {
|
|
373
404
|
const veto = switches.has("veto");
|
|
374
405
|
const once = switches.has("once");
|
|
375
|
-
// Judging changes needs no key; vetoing
|
|
376
|
-
const
|
|
406
|
+
// Judging changes needs no key; vetoing and reporting do. A reader stands in for the signer otherwise.
|
|
407
|
+
const keyed = veto || Boolean(process.env.BRAINCHAIN_WORKER_KEY?.trim());
|
|
408
|
+
const account = keyed ? signer().account : privateKeyToAccount(generatePrivateKey());
|
|
377
409
|
const client = createPublicClient({ chain: chainOf(network), transport: brainchainTransport(network) });
|
|
378
410
|
const { taskBoard, executor, log } = network.contracts;
|
|
379
411
|
const verify = async (id) => {
|
|
@@ -383,12 +415,22 @@ export async function main(argv) {
|
|
|
383
415
|
set: { key: "active", release: "", fromBlock: Number(network.fromBlock), board: taskBoard, log, executor },
|
|
384
416
|
briefs: new Map(),
|
|
385
417
|
}, id);
|
|
386
|
-
|
|
418
|
+
const findings = report.checks.filter((c) => c.status === "fail" || c.status === "error").map((c) => `${c.name}: ${c.message}`);
|
|
419
|
+
// A verified decision is reported with the record it was checked against, fetched again as the service serves it.
|
|
420
|
+
const record = findings.length === 0 && keyed && report.payloadURI ? await fetchRecord(report.payloadURI).catch(() => undefined) : undefined;
|
|
421
|
+
return { findings, ...(record ? { record } : {}) };
|
|
387
422
|
};
|
|
388
|
-
const watch = createWatch({
|
|
423
|
+
const watch = createWatch({
|
|
424
|
+
network,
|
|
425
|
+
chain: createWatchChain(network, account),
|
|
426
|
+
verify,
|
|
427
|
+
...(keyed ? { report: createReporter(network, account) } : {}),
|
|
428
|
+
veto,
|
|
429
|
+
log: console.log,
|
|
430
|
+
});
|
|
389
431
|
if (veto && !network.contracts.gate)
|
|
390
432
|
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" : ""}`);
|
|
433
|
+
console.log(`watching ${network.name} as ${account.address}${veto ? ", vetoing through the guardian gate" : ""}${keyed ? ", reporting rounds to the service" : ""}`);
|
|
392
434
|
if (once) {
|
|
393
435
|
const { findings } = await watch.round();
|
|
394
436
|
if (findings > 0)
|
package/dist/network.js
CHANGED
|
@@ -59,9 +59,8 @@ export const MAINNET = {
|
|
|
59
59
|
stakeSymbol: "BRAIN",
|
|
60
60
|
bountySymbol: "USDG",
|
|
61
61
|
gas: "Bridge some from Robinhood Chain at https://usebrainchain.com/network",
|
|
62
|
-
buyStake: "Buy BRAIN on ripples.run (https://ripples.run/t/0xa06e3D898Bce92F67617E0Bfb640F153A5b65AB7) and
|
|
63
|
-
|
|
64
|
-
`the stake token on BrainChain is ${BRAIN}.`,
|
|
62
|
+
buyStake: "Buy BRAIN on Robinhood Chain at ripples.run (https://ripples.run/t/0xa06e3D898Bce92F67617E0Bfb640F153A5b65AB7) and move it to BrainChain with " +
|
|
63
|
+
`brainchain-worker bridge --stake <amount> (docs: https://docs.usebrainchain.com/bridging); the stake token on BrainChain is ${BRAIN}.`,
|
|
65
64
|
proofs: {
|
|
66
65
|
chainId: 4663,
|
|
67
66
|
name: "Robinhood Chain",
|
|
@@ -72,6 +71,7 @@ export const MAINNET = {
|
|
|
72
71
|
gas: "Send it a little ETH there for gas",
|
|
73
72
|
income: "0xeb1b4168D4b87D3d9F0D64E13B7071251BBB63b7",
|
|
74
73
|
},
|
|
74
|
+
identity: "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432",
|
|
75
75
|
bridge: {
|
|
76
76
|
chainId: 4663,
|
|
77
77
|
name: "Robinhood Chain",
|
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.9.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>",
|