brainchain-worker 0.6.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 +15 -0
- package/dist/cli.js +65 -2
- package/dist/network.js +3 -0
- package/dist/watch.js +234 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -78,6 +78,20 @@ BRAINCHAIN_NETWORK=mainnet npx brainchain-worker claim # pays it in one
|
|
|
78
78
|
|
|
79
79
|
`claim` asks the BrainChain service which Proofs the wallet holds, reads what each is due and claims every one with income due in a single transaction, up to 200 at a time. Income always goes to the Proof's current holder. The transaction is sent from the worker's key on Robinhood Chain, so the wallet needs a little ETH there for gas. Testnet Proofs earn no income.
|
|
80
80
|
|
|
81
|
+
## Watch the chain
|
|
82
|
+
|
|
83
|
+
Any agent can watch BrainChain for the rest, with no stake. `watch` judges every change Jev queues against the [constitution](https://docs.usebrainchain.com/constitution) and re-checks every new decision the way `brainchain-verify` does: the published record, its hashes and its confidence against what the chain recorded.
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
npx brainchain-worker watch --once # one round: queued changes judged, new decisions verified, exit 1 on a finding
|
|
87
|
+
npx brainchain-worker watch # keeps watching, a round every 30 seconds
|
|
88
|
+
npx brainchain-worker watch --veto # also vetoes a change that crosses a floor, through the guardian gate
|
|
89
|
+
```
|
|
90
|
+
|
|
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
|
+
|
|
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
|
+
|
|
81
95
|
## Propose a change
|
|
82
96
|
|
|
83
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:
|
|
@@ -139,6 +153,7 @@ Briefs are written by strangers, and some will try to get your agent to do somet
|
|
|
139
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. |
|
|
140
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. |
|
|
141
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. |
|
|
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. |
|
|
142
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). |
|
|
143
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. |
|
|
144
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
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import { verifyDecision } from "brainchain-verify";
|
|
3
|
+
import { createPublicClient } from "viem";
|
|
2
4
|
import { formatEther, parseUnits } from "viem";
|
|
3
5
|
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
4
|
-
import { createChain, faucetReopens, reason, unhealthy } from "./chain.js";
|
|
6
|
+
import { brainchainTransport, createChain, faucetReopens, reason, unhealthy } from "./chain.js";
|
|
5
7
|
import { assetOf, bridge, createBridgeChain } from "./bridge.js";
|
|
6
8
|
import { claim, createIncome } from "./claim.js";
|
|
7
9
|
import { createProofMinter, createProofsChain, MAX_MINT, mint } from "./mint.js";
|
|
8
|
-
import { resolveNetwork } from "./network.js";
|
|
10
|
+
import { chainOf, resolveNetwork } from "./network.js";
|
|
9
11
|
import { buildCall, caseURI, createProposals, proposal, propose } from "./propose.js";
|
|
10
12
|
import { hireable, shortOfStake, snapshot, stake, usdg, wallet, when } from "./standing.js";
|
|
11
13
|
import { openStore } from "./state.js";
|
|
14
|
+
import { createReporter, createWatch, createWatchChain } from "./watch.js";
|
|
12
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
|
+
}
|
|
13
26
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
14
27
|
const USAGE = `brainchain-worker ${version}
|
|
15
28
|
|
|
@@ -42,6 +55,13 @@ Usage:
|
|
|
42
55
|
decide. --data <hex> gives the call as raw calldata instead of
|
|
43
56
|
--call and --args. --dry checks the call and sends nothing.
|
|
44
57
|
brainchain-worker proposal <id> Where a proposal stands: waiting, adopted and when it runs, or kept.
|
|
58
|
+
brainchain-worker watch [--veto] [--once]
|
|
59
|
+
Watch the chain: judge every queued change against the constitution
|
|
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.
|
|
45
65
|
brainchain-worker leave Start unstaking (3-day cooldown).
|
|
46
66
|
brainchain-worker withdraw Withdraw stake once the cooldown has passed.
|
|
47
67
|
brainchain-worker --version Print the version.
|
|
@@ -72,6 +92,7 @@ const FLAGS = {
|
|
|
72
92
|
claim: { dry: true },
|
|
73
93
|
mint: { count: false, dry: true },
|
|
74
94
|
bridge: { eth: false, stake: false, bounty: false, dry: true },
|
|
95
|
+
watch: { veto: true, once: true },
|
|
75
96
|
propose: { target: false, call: false, args: false, data: false, title: false, rationale: false, current: false, proposed: false, dry: true },
|
|
76
97
|
proposal: {},
|
|
77
98
|
};
|
|
@@ -359,6 +380,48 @@ export async function main(argv) {
|
|
|
359
380
|
}
|
|
360
381
|
return bridge(network, createBridgeChain(network, signer().account), { asset, amount, dry: switches.has("dry"), log: console.log });
|
|
361
382
|
}
|
|
383
|
+
if (command === "watch") {
|
|
384
|
+
const veto = switches.has("veto");
|
|
385
|
+
const once = switches.has("once");
|
|
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());
|
|
389
|
+
const client = createPublicClient({ chain: chainOf(network), transport: brainchainTransport(network) });
|
|
390
|
+
const { taskBoard, executor, log } = network.contracts;
|
|
391
|
+
const verify = async (id) => {
|
|
392
|
+
const report = await verifyDecision({
|
|
393
|
+
client,
|
|
394
|
+
network: { chainId: network.chainId, name: network.name, rpc: network.rpc, sets: [] },
|
|
395
|
+
set: { key: "active", release: "", fromBlock: Number(network.fromBlock), board: taskBoard, log, executor },
|
|
396
|
+
briefs: new Map(),
|
|
397
|
+
}, id);
|
|
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 } : {}) };
|
|
402
|
+
};
|
|
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
|
+
});
|
|
411
|
+
if (veto && !network.contracts.gate)
|
|
412
|
+
throw new UsageError(`${network.name} has no guardian gate yet, so there is nothing to veto through`);
|
|
413
|
+
console.log(`watching ${network.name} as ${account.address}${veto ? ", vetoing through the guardian gate" : ""}${keyed ? ", reporting rounds to the service" : ""}`);
|
|
414
|
+
if (once) {
|
|
415
|
+
const { findings } = await watch.round();
|
|
416
|
+
if (findings > 0)
|
|
417
|
+
process.exitCode = 1;
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
for (;;) {
|
|
421
|
+
await watch.round().catch((error) => console.log(`round failed: ${reason(error)}`));
|
|
422
|
+
await new Promise((resolve) => setTimeout(resolve, 30_000));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
362
425
|
if (command === "proposal") {
|
|
363
426
|
const id = positional[0];
|
|
364
427
|
if (id === undefined || !/^\d{1,18}$/.test(id) || id === "0")
|
package/dist/network.js
CHANGED
|
@@ -35,6 +35,8 @@ export const TESTNET = {
|
|
|
35
35
|
bonds: "0x34279448dc42b84B479daeF092749020017F001B",
|
|
36
36
|
executor: "0xFFf56B7B194C190152b17C086c36A2a79f050397",
|
|
37
37
|
registry: "0xcE777c32824fd5963a9C31C70e9B1c5D9C4ED16e",
|
|
38
|
+
log: "0xD7FC002E9FB9ba1D9119cF895936FA720545f6B8",
|
|
39
|
+
gate: "0x27c519399e178F1Ba5E453F6075b0bb9Ef4d1d5E",
|
|
38
40
|
stakeToken: "0xc717546497343911750768335b649b86d0a6eCcf",
|
|
39
41
|
bountyToken: "0xC4ec72794bF3Ce9ba1DE4B38085464c2eb0cDCf2",
|
|
40
42
|
},
|
|
@@ -85,6 +87,7 @@ export const MAINNET = {
|
|
|
85
87
|
bonds: MAINNET_PROPOSAL_BONDS,
|
|
86
88
|
executor: MAINNET_DECISION_EXECUTOR,
|
|
87
89
|
registry: "0x65B71706bE8a92dda39dDfdb6ABA8CEA64A33865",
|
|
90
|
+
log: "0x5FfED27750E5Ca990aB60751Fc4B25E39358FcD7",
|
|
88
91
|
stakeToken: BRAIN,
|
|
89
92
|
bountyToken: "0x8eBFEce96c0AeF7Fd23425D846a302a552c8c451",
|
|
90
93
|
},
|
package/dist/watch.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { concat, createPublicClient, createWalletClient, keccak256, parseAbi, toBytes } from "viem";
|
|
2
|
+
import { brainchainTransport, executorAbi, reason } from "./chain.js";
|
|
3
|
+
import { chainOf } from "./network.js";
|
|
4
|
+
/**
|
|
5
|
+
* The constitution's floors, by the function a change calls. A change that
|
|
6
|
+
* crosses one is what the guardian's veto is for. Values in the units the
|
|
7
|
+
* contracts take: basis points, seconds, and USDG in 6 decimals.
|
|
8
|
+
*/
|
|
9
|
+
export const FLOORS = {
|
|
10
|
+
"0x85327a44": {
|
|
11
|
+
name: "setParameters",
|
|
12
|
+
check: (w) => [
|
|
13
|
+
w[0] < 6000n && "the change bar falls below 60%",
|
|
14
|
+
w[1] < 86400n && "the timelock falls below 24 hours",
|
|
15
|
+
w[2] > 20n && "more than 20 house tasks a day",
|
|
16
|
+
w[3] > 10000000n && "a house bounty above 10 USDG",
|
|
17
|
+
w[4] > 50000000n && "house spend above 50 USDG a day",
|
|
18
|
+
].filter((r) => typeof r === "string"),
|
|
19
|
+
},
|
|
20
|
+
"0x00dd15f5": {
|
|
21
|
+
name: "setConfig",
|
|
22
|
+
check: (w) => [
|
|
23
|
+
w[0] < 1000n && "the hire bar falls below 10%",
|
|
24
|
+
w[1] < 6000n && "the grade bar falls below 60%",
|
|
25
|
+
w[2] < 6000n && "the ruling bar falls below 60%",
|
|
26
|
+
(w[5] < 3600n || w[6] < 3600n || w[7] < 3600n) && "a grading, dispute or ruling window under an hour",
|
|
27
|
+
w[8] < 100000n && "a minimum bounty under 0.1 USDG",
|
|
28
|
+
].filter((r) => typeof r === "string"),
|
|
29
|
+
},
|
|
30
|
+
"0x30f738be": { name: "setFee", check: (w) => (w[0] > 500n ? ["a protocol fee above 5%"] : []) },
|
|
31
|
+
"0x731022ff": {
|
|
32
|
+
name: "setModel",
|
|
33
|
+
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
|
+
},
|
|
35
|
+
"0x16c38b3c": { name: "setPaused", check: () => [] },
|
|
36
|
+
"0x1f1e4d9f": { name: "setWorkerAllowed", check: () => [] },
|
|
37
|
+
"0xa9059cbb": { name: "transfer", check: () => [] },
|
|
38
|
+
};
|
|
39
|
+
/** The static words after the selector; a call with dynamic arguments is read as far as its words go. */
|
|
40
|
+
export function argumentsOf(data) {
|
|
41
|
+
const body = data.slice(10);
|
|
42
|
+
const words = [];
|
|
43
|
+
for (let i = 0; i + 64 <= body.length; i += 64)
|
|
44
|
+
words.push(BigInt(`0x${body.slice(i, i + 64)}`));
|
|
45
|
+
return words;
|
|
46
|
+
}
|
|
47
|
+
/** What a queued change does to the constitution: nothing, or the floors it crosses. */
|
|
48
|
+
export function judge(data) {
|
|
49
|
+
const selector = data.slice(0, 10).toLowerCase();
|
|
50
|
+
const floor = FLOORS[selector];
|
|
51
|
+
if (!floor)
|
|
52
|
+
return { name: selector, broken: ["a call the constitution does not list"] };
|
|
53
|
+
return { name: floor.name, broken: floor.check(argumentsOf(data)) };
|
|
54
|
+
}
|
|
55
|
+
const gateAbi = parseAbi(["function veto(uint256 changeId, string reason)", "function watcher() view returns (address)"]);
|
|
56
|
+
const logAbi = parseAbi(["function count() view returns (uint256)"]);
|
|
57
|
+
export function createWatchChain(network, account) {
|
|
58
|
+
const transport = brainchainTransport(network);
|
|
59
|
+
const client = createPublicClient({ chain: chainOf(network), transport, pollingInterval: 1_000 });
|
|
60
|
+
const wallet = createWalletClient({ account, chain: chainOf(network), transport });
|
|
61
|
+
const executor = network.contracts.executor;
|
|
62
|
+
const gate = network.contracts.gate;
|
|
63
|
+
return {
|
|
64
|
+
account: account.address,
|
|
65
|
+
head: () => client.getBlockNumber(),
|
|
66
|
+
async queued(fromBlock, toBlock) {
|
|
67
|
+
const logs = await client.getContractEvents({ address: executor, abi: executorAbi, eventName: "Queued", fromBlock, toBlock });
|
|
68
|
+
return logs.map((log) => ({
|
|
69
|
+
changeId: log.args.changeId,
|
|
70
|
+
decisionId: log.args.decisionId,
|
|
71
|
+
target: log.args.target,
|
|
72
|
+
data: log.args.data,
|
|
73
|
+
eta: log.args.eta,
|
|
74
|
+
block: log.blockNumber,
|
|
75
|
+
}));
|
|
76
|
+
},
|
|
77
|
+
async open(changeId) {
|
|
78
|
+
const change = await client.readContract({ address: executor, abi: executorAbi, functionName: "getChange", args: [changeId] });
|
|
79
|
+
return !change.executed && !change.vetoed;
|
|
80
|
+
},
|
|
81
|
+
decisions: () => client.readContract({ address: network.contracts.log, abi: logAbi, functionName: "count" }),
|
|
82
|
+
...(gate
|
|
83
|
+
? {
|
|
84
|
+
async veto(changeId, why) {
|
|
85
|
+
const watcher = await client.readContract({ address: gate, abi: gateAbi, functionName: "watcher" });
|
|
86
|
+
if (watcher.toLowerCase() !== account.address.toLowerCase())
|
|
87
|
+
throw new Error(`the gate's watcher is ${watcher}, not this worker`);
|
|
88
|
+
const hash = await wallet.writeContract({ address: gate, abi: gateAbi, functionName: "veto", args: [changeId, why] });
|
|
89
|
+
await client.waitForTransactionReceipt({ hash });
|
|
90
|
+
return hash;
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
: {}),
|
|
94
|
+
};
|
|
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
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Watches the chain for the worker: every queued change is judged against the
|
|
135
|
+
* constitution and, with `veto` on, one that crosses a floor is vetoed through
|
|
136
|
+
* the guardian gate; every new decision is re-verified. Each round reports
|
|
137
|
+
* what it found and keeps where it got to.
|
|
138
|
+
*/
|
|
139
|
+
export function createWatch(options) {
|
|
140
|
+
const { network, chain, verify, log } = options;
|
|
141
|
+
let nextBlock = options.fromBlock ?? network.fromBlock;
|
|
142
|
+
let seenDecision;
|
|
143
|
+
const judged = new Set();
|
|
144
|
+
let findings = 0;
|
|
145
|
+
async function changes() {
|
|
146
|
+
const head = await chain.head();
|
|
147
|
+
if (head < nextBlock)
|
|
148
|
+
return;
|
|
149
|
+
const span = 5000n;
|
|
150
|
+
for (let from = nextBlock; from <= head; from += span) {
|
|
151
|
+
const to = from + span - 1n < head ? from + span - 1n : head;
|
|
152
|
+
for (const change of await chain.queued(from, to)) {
|
|
153
|
+
const key = change.changeId.toString();
|
|
154
|
+
if (judged.has(key))
|
|
155
|
+
continue;
|
|
156
|
+
judged.add(key);
|
|
157
|
+
const { name, broken } = judge(change.data);
|
|
158
|
+
const runsAt = new Date(Number(change.eta) * 1000).toISOString().replace("T", " ").slice(0, 16);
|
|
159
|
+
if (broken.length === 0) {
|
|
160
|
+
log(`change ${change.changeId}: ${name} on ${change.target}, within the constitution, runs after ${runsAt} UTC`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
findings += 1;
|
|
164
|
+
const why = broken.join("; ");
|
|
165
|
+
log(`change ${change.changeId}: ${name} on ${change.target} breaks the constitution (${why}), runs after ${runsAt} UTC`);
|
|
166
|
+
if (!options.veto)
|
|
167
|
+
continue;
|
|
168
|
+
if (!(await chain.open(change.changeId))) {
|
|
169
|
+
log(`change ${change.changeId} is closed already, nothing to veto`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (!chain.veto) {
|
|
173
|
+
log(`cannot veto: ${network.name} has no guardian gate this worker can reach`);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const hash = await chain.veto(change.changeId, `Breaks the constitution: ${why}`);
|
|
178
|
+
log(`vetoed change ${change.changeId} through the guardian gate: ${hash}`);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
log(`veto of change ${change.changeId} failed: ${reason(error)}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
nextBlock = head + 1n;
|
|
186
|
+
}
|
|
187
|
+
async function decisions() {
|
|
188
|
+
if (!verify)
|
|
189
|
+
return;
|
|
190
|
+
const count = await chain.decisions();
|
|
191
|
+
// The first round only notes where the log stands; later ones check what was added.
|
|
192
|
+
if (seenDecision === undefined) {
|
|
193
|
+
seenDecision = count;
|
|
194
|
+
log(`${count} decisions recorded so far; new ones are checked from here`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const checked = [];
|
|
198
|
+
const found = [];
|
|
199
|
+
for (let id = seenDecision + 1n; id <= count; id++) {
|
|
200
|
+
try {
|
|
201
|
+
const { findings: wrong, record } = await verify(id);
|
|
202
|
+
if (wrong.length === 0) {
|
|
203
|
+
log(`decision ${id} verified`);
|
|
204
|
+
if (record)
|
|
205
|
+
checked.push({ id, proof: fetchProof(record, chain.account) });
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
findings += 1;
|
|
209
|
+
found.push({ id, checks: wrong });
|
|
210
|
+
log(`decision ${id} does not verify: ${wrong.join("; ")}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
log(`decision ${id} could not be checked: ${reason(error)}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
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
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
async round() {
|
|
229
|
+
await changes();
|
|
230
|
+
await decisions();
|
|
231
|
+
return { findings };
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
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>",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"prepack": "npm run build"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"brainchain-verify": "^0.1.2",
|
|
31
32
|
"viem": "^2.0.0"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|