@finchip.ai/foga 0.0.0-stage → 0.6.0-alpha.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/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +216 -2
- package/artifacts/bundle.json +74250 -0
- package/artifacts/bundle.sha256 +1 -0
- package/bin/foga.js +14 -0
- package/build/known-interface-ids.json +18 -0
- package/build/profiles.json +120 -0
- package/dist/build/bundle.js +417 -0
- package/dist/build/vendor.js +74 -0
- package/dist/bundle/load.js +24 -0
- package/dist/bundle/types.js +1 -0
- package/dist/cli-cell.js +395 -0
- package/dist/cli.js +505 -0
- package/dist/core/acdf.js +162 -0
- package/dist/core/adrf.js +273 -0
- package/dist/core/aid.js +83 -0
- package/dist/core/ard.js +146 -0
- package/dist/core/asf.js +157 -0
- package/dist/core/canonical.js +79 -0
- package/dist/core/erc8004.js +52 -0
- package/dist/core/kya.js +225 -0
- package/dist/core/measure.js +96 -0
- package/dist/core/pack/cbor.js +130 -0
- package/dist/core/pack/skillpack.js +142 -0
- package/dist/core/skills.js +40 -0
- package/dist/core/swarm.js +206 -0
- package/dist/core/tenders.js +121 -0
- package/dist/core/units.js +122 -0
- package/dist/core/world.js +104 -0
- package/dist/demo/demander.js +76 -0
- package/dist/demo/evidence.js +1 -0
- package/dist/demo/loop-d.js +449 -0
- package/dist/doctor/checks.js +51 -0
- package/dist/doctor/doctor.js +385 -0
- package/dist/doctor/loop-d.js +103 -0
- package/dist/doctor/probe.js +97 -0
- package/dist/doctor/semantic.js +83 -0
- package/dist/genesis/extract.js +64 -0
- package/dist/genesis/funding.js +126 -0
- package/dist/genesis/operator-scope.js +90 -0
- package/dist/genesis/preflight.js +54 -0
- package/dist/genesis/probe.js +164 -0
- package/dist/genesis/roles.js +96 -0
- package/dist/genesis/run.js +71 -0
- package/dist/genesis/scenario.js +765 -0
- package/dist/genesis/skill-runner.js +103 -0
- package/dist/genesis/skill-worker.js +41 -0
- package/dist/genesis/t1-zk.js +62 -0
- package/dist/genesis/t3-zk.js +105 -0
- package/dist/genesis/t4.js +259 -0
- package/dist/genesis/t5.js +168 -0
- package/dist/genesis/t6.js +222 -0
- package/dist/genesis/t7.js +198 -0
- package/dist/genesis/t8.js +189 -0
- package/dist/genesis/t9.js +188 -0
- package/dist/genesis/zk.js +128 -0
- package/dist/http/server.js +276 -0
- package/dist/kernel/cell.js +104 -0
- package/dist/kernel/errors.js +78 -0
- package/dist/kernel/kernel.js +251 -0
- package/dist/kernel/provider.js +125 -0
- package/dist/kernel/store.js +470 -0
- package/dist/kernel/tx.js +385 -0
- package/dist/mcp/server.js +170 -0
- package/dist/node/config.js +195 -0
- package/dist/node/init.js +200 -0
- package/dist/node/keys.js +95 -0
- package/dist/node/migrate.js +173 -0
- package/dist/node/model/anthropic.js +97 -0
- package/dist/node/model/stub.js +129 -0
- package/dist/node/model/types.js +9 -0
- package/dist/node/node.js +198 -0
- package/dist/node/policy.js +95 -0
- package/dist/node/runner.js +256 -0
- package/dist/node/watchers.js +88 -0
- package/dist/tools/admin.js +184 -0
- package/dist/tools/catalog.js +261 -0
- package/dist/tools/common.js +143 -0
- package/dist/tools/identity.js +107 -0
- package/dist/tools/index.js +14 -0
- package/dist/tools/read.js +471 -0
- package/dist/tools/tenders.js +400 -0
- package/dist/tools/types.js +4 -0
- package/dist/util/bytecode.js +123 -0
- package/dist/util/fs.js +58 -0
- package/dist/util/hash.js +4 -0
- package/dist/util/log.js +11 -0
- package/dist/util/version.js +12 -0
- package/dist/world/deploy.js +277 -0
- package/dist/world/plan.js +93 -0
- package/dist/world/types.js +1 -0
- package/docs/build.md +45 -0
- package/docs/cell-account.md +58 -0
- package/docs/compatibility-matrix.md +38 -0
- package/docs/design-memo.md +113 -0
- package/docs/doctor.md +53 -0
- package/docs/examples/README.md +11 -0
- package/docs/examples/claude-code-mcp.json +13 -0
- package/docs/examples/claude_desktop_config.json +12 -0
- package/docs/examples/cursor-mcp.json +11 -0
- package/docs/genesis.md +312 -0
- package/docs/kernel.md +101 -0
- package/docs/loop-d.md +90 -0
- package/docs/mcp.md +105 -0
- package/docs/node.md +66 -0
- package/docs/version-lock.md +67 -0
- package/docs/world.md +50 -0
- package/docs/zk.md +48 -0
- package/package.json +82 -4
- package/prover/README.md +26 -0
- package/prover/build-predicate.mjs +77 -0
- package/prover/package-lock.json +1837 -0
- package/prover/package.json +15 -0
- package/prover/run.cjs +199 -0
- package/scenarios/fixtures/sample-filing.txt +16 -0
- package/scenarios/skills/finch-extract-verify/LICENSE +2 -0
- package/scenarios/skills/finch-extract-verify/SKILL.md +34 -0
- package/scenarios/skills/finch-extract-verify/fields.json +56 -0
- package/scenarios/skills/finch-extract-verify/manifest.json +16 -0
- package/scenarios/skills/foga-probe/LICENSE +2 -0
- package/scenarios/skills/foga-probe/SKILL.md +35 -0
- package/scenarios/skills/foga-probe/manifest.json +16 -0
- package/scenarios/skills/foga-probe/probe.schema.json +40 -0
- package/server.json +79 -0
- package/vendor/LOCK.json +107 -0
- package/vendor/README.md +13 -0
- package/worlds/local.json +34 -0
- package/worlds/sepolia.json +314 -0
- package/zk/build/verification_key.json +174 -0
- package/zk/circuits/kya_predicate_count_v1.circom +231 -0
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { ZeroHash, getAddress, parseEther } from "ethers";
|
|
3
|
+
import { AID } from "../core/aid.js";
|
|
4
|
+
import { agentDescriptor, kindId, skillDescriptor, taskDescriptor } from "../core/ard.js";
|
|
5
|
+
import { Enforcement, Rights, accountSubjectKey, fogaCellSpecification, oneClause, requirementIdOf, scopeKeyOf } from "../core/asf.js";
|
|
6
|
+
import { accountRef, dataUri, dataUriJcs, keccakJcs, sha256Hex } from "../core/canonical.js";
|
|
7
|
+
import { accountSubject, fogaAttestedSchemeDescriptor, fogaPredicateSchemeDescriptor, fogaProvedSchemeDescriptor } from "../core/kya.js";
|
|
8
|
+
import { buildSkillPackage, readSkillDir } from "../core/pack/skillpack.js";
|
|
9
|
+
import { hashlock, nativeTerms } from "../core/tenders.js";
|
|
10
|
+
import { cellUnitDefinition, skillUnitDefinition } from "../core/units.js";
|
|
11
|
+
import { councilDescriptor, councilPolicySpec } from "../core/acdf.js";
|
|
12
|
+
import { escrowBindingInput, serviceDescriptor, servicePolicySpec } from "../core/adrf.js";
|
|
13
|
+
import { Cell } from "../kernel/cell.js";
|
|
14
|
+
import { Kernel } from "../kernel/kernel.js";
|
|
15
|
+
import { EPOCH_GRACE, EPOCH_LENGTH, PREDICATE_VALIDITY, attestorKeyFrom, predicateAvailability, proverLibsAvailable, sourceKeyFrom, zk, zkAvailability } from "./zk.js";
|
|
16
|
+
import { CELLS, operatorRole, ownerRole, roleKeys } from "./roles.js";
|
|
17
|
+
import { CELL_CAPITAL, fundRoles, operatorGasFloor } from "./funding.js";
|
|
18
|
+
import { CELL_GROUPS, operatorScope } from "./operator-scope.js";
|
|
19
|
+
import { runProbe } from "./probe.js";
|
|
20
|
+
import { runProbeIsolated, verifiedPackage } from "./skill-runner.js";
|
|
21
|
+
export const STEPS = ["T0", "T1", "T2", "T3", "T4", "T5", "T6", "T7", "T8", "T9"];
|
|
22
|
+
/** Bumped whenever the operation keys or their inputs change; a store written by another version is refused. */
|
|
23
|
+
export const SCENARIO_VERSION = "5";
|
|
24
|
+
export const LOCAL_TIMING = {
|
|
25
|
+
councilWindow: 86_400,
|
|
26
|
+
appealWindow: 3_600,
|
|
27
|
+
governanceWindow: 86_400,
|
|
28
|
+
margin: 3_600,
|
|
29
|
+
judgmentWindow: 7 * 86_400,
|
|
30
|
+
noDecisionJudgmentWindow: 3 * 86_400,
|
|
31
|
+
service: { filingWindow: 7 * 86_400, evidenceWindow: 86_400, decisionWindow: 3 * 86_400, reviewWindow: 86_400, enactmentMargin: 86_400, disputePeriod: 10 * 86_400 },
|
|
32
|
+
};
|
|
33
|
+
export const PUBLIC_TIMING = {
|
|
34
|
+
councilWindow: 180,
|
|
35
|
+
appealWindow: 60,
|
|
36
|
+
governanceWindow: 180,
|
|
37
|
+
margin: 60,
|
|
38
|
+
judgmentWindow: 900,
|
|
39
|
+
noDecisionJudgmentWindow: 600,
|
|
40
|
+
service: { filingWindow: 1_800, evidenceWindow: 180, decisionWindow: 600, reviewWindow: 120, enactmentMargin: 120, disputePeriod: 1_800 },
|
|
41
|
+
};
|
|
42
|
+
/** The liveness window the cells shorten to in T7 so that STALE is reached by waiting, on every chain (seconds). */
|
|
43
|
+
export const LIVENESS_DEMO_WINDOW = 120;
|
|
44
|
+
const ZERO32 = "0x" + "00".repeat(32);
|
|
45
|
+
const SKILL_NAMES = ["foga-probe", "finch-extract-verify"];
|
|
46
|
+
/**
|
|
47
|
+
* The genesis scenario (design memo, section 4), T0 to T3 in this phase. Every chain write is a keyed operation,
|
|
48
|
+
* every value a later step needs is a fact, every post-condition of the memo is a check that re-runs on resume.
|
|
49
|
+
*/
|
|
50
|
+
export class Genesis {
|
|
51
|
+
kernel;
|
|
52
|
+
cfg;
|
|
53
|
+
checks = [];
|
|
54
|
+
cells = new Map();
|
|
55
|
+
timing;
|
|
56
|
+
constructor(kernel, cfg) {
|
|
57
|
+
this.kernel = kernel;
|
|
58
|
+
this.cfg = cfg;
|
|
59
|
+
this.timing = kernel.isLocalChain ? LOCAL_TIMING : PUBLIC_TIMING;
|
|
60
|
+
}
|
|
61
|
+
static async open(cfg) {
|
|
62
|
+
// The keys are decided by the world file, so load it first through a throw-away World handle.
|
|
63
|
+
const { World } = await import("../core/world.js");
|
|
64
|
+
const w = World.load(cfg.worldPath, cfg.rpc, cfg.bundlePath);
|
|
65
|
+
const keys = roleKeys(w.file);
|
|
66
|
+
w.provider.destroy();
|
|
67
|
+
const kernel = await Kernel.open({
|
|
68
|
+
worldPath: cfg.worldPath,
|
|
69
|
+
rpc: cfg.rpc,
|
|
70
|
+
bundlePath: cfg.bundlePath,
|
|
71
|
+
storePath: cfg.storePath,
|
|
72
|
+
keys,
|
|
73
|
+
crashBeforeBroadcast: cfg.crashBeforeBroadcast,
|
|
74
|
+
crashAfterBroadcast: cfg.crashAfterBroadcast,
|
|
75
|
+
// The genesis is a script that resumes: an operation that failed on a transient condition (an estimate made
|
|
76
|
+
// against a lagging node, say) is sent anew when the run is resumed. That is the script's explicit choice;
|
|
77
|
+
// the kernel's default answers a failed key with its failure (docs/kernel.md).
|
|
78
|
+
retryFailed: true,
|
|
79
|
+
log: cfg.log,
|
|
80
|
+
});
|
|
81
|
+
const have = kernel.store.getMeta("scenarioVersion");
|
|
82
|
+
if (have && have !== SCENARIO_VERSION) {
|
|
83
|
+
kernel.close();
|
|
84
|
+
throw new Error(`operation store ${cfg.storePath} was written by scenario version ${have}; this build runs version ${SCENARIO_VERSION}. Start from a fresh store (delete it or pass --store).`);
|
|
85
|
+
}
|
|
86
|
+
if (!have)
|
|
87
|
+
kernel.store.setMeta("scenarioVersion", SCENARIO_VERSION);
|
|
88
|
+
return new Genesis(kernel, cfg);
|
|
89
|
+
}
|
|
90
|
+
close() {
|
|
91
|
+
this.kernel.close();
|
|
92
|
+
this.kernel.world.provider.destroy();
|
|
93
|
+
}
|
|
94
|
+
get world() {
|
|
95
|
+
return this.kernel.world;
|
|
96
|
+
}
|
|
97
|
+
get chainId() {
|
|
98
|
+
return this.kernel.chainId;
|
|
99
|
+
}
|
|
100
|
+
log(line) {
|
|
101
|
+
this.cfg.log(line);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Record a check. `until` names operations of later steps that change the state the check is about; when one of
|
|
105
|
+
* them is already confirmed in the store (a replayed or resumed run), the check cannot be remade and is recorded
|
|
106
|
+
* as skipped with the reason, never as passed or failed.
|
|
107
|
+
*/
|
|
108
|
+
check(step, name, pass, detail, until) {
|
|
109
|
+
const keys = typeof until === "string" ? [until] : (until ?? []);
|
|
110
|
+
const superseded = keys.find((k) => this.kernel.store.get(k)?.status === "confirmed");
|
|
111
|
+
if (superseded)
|
|
112
|
+
return this.skipped(step, name, `replayed run: the state was changed later by ${superseded}`);
|
|
113
|
+
this.checks.push({ step, name, pass, detail });
|
|
114
|
+
this.log(` ${pass ? "ok " : "FAIL"} ${step} ${name}: ${detail}`);
|
|
115
|
+
}
|
|
116
|
+
/** A check that could not be made on this machine; recorded as passed with the reason, never silently dropped. */
|
|
117
|
+
skipped(step, name, reason) {
|
|
118
|
+
this.checks.push({ step, name, pass: true, detail: `skipped: ${reason}` });
|
|
119
|
+
this.log(` skip ${step} ${name}: ${reason}`);
|
|
120
|
+
}
|
|
121
|
+
selector(iface, fn) {
|
|
122
|
+
const f = iface.getFunction(fn);
|
|
123
|
+
if (!f)
|
|
124
|
+
throw new Error(`no function ${fn}`);
|
|
125
|
+
return f.selector;
|
|
126
|
+
}
|
|
127
|
+
// ------------------------------------------------------------------------------------------------ driver
|
|
128
|
+
async run(until) {
|
|
129
|
+
const stop = STEPS.indexOf(until);
|
|
130
|
+
for (const step of STEPS.slice(0, stop + 1)) {
|
|
131
|
+
this.log(`${step}: start`);
|
|
132
|
+
await fundRoles(this.kernel, step, (line) => this.log(line));
|
|
133
|
+
if (step === "T0")
|
|
134
|
+
await this.t0();
|
|
135
|
+
if (step === "T1")
|
|
136
|
+
await this.t1();
|
|
137
|
+
if (step === "T2")
|
|
138
|
+
await this.t2();
|
|
139
|
+
if (step === "T3")
|
|
140
|
+
await this.t3();
|
|
141
|
+
if (step === "T4")
|
|
142
|
+
await (await import("./t4.js")).t4(this);
|
|
143
|
+
if (step === "T5")
|
|
144
|
+
await (await import("./t5.js")).t5(this);
|
|
145
|
+
if (step === "T6")
|
|
146
|
+
await (await import("./t6.js")).t6(this);
|
|
147
|
+
if (step === "T7")
|
|
148
|
+
await (await import("./t7.js")).t7(this);
|
|
149
|
+
if (step === "T8")
|
|
150
|
+
await (await import("./t8.js")).t8(this);
|
|
151
|
+
if (step === "T9")
|
|
152
|
+
await (await import("./t9.js")).t9(this);
|
|
153
|
+
this.kernel.setFact(`step.${step}.done`, true);
|
|
154
|
+
this.log(`${step}: done`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// ------------------------------------------------------------------------------------------------ T0 world
|
|
158
|
+
/** T0: the world exists (deployed by `foga world deploy`); W registers what the cells will adopt. */
|
|
159
|
+
async t0() {
|
|
160
|
+
const k = this.kernel;
|
|
161
|
+
const W = "W";
|
|
162
|
+
// KYA scheme S_att: attested, ordered levels 0..3, account subjects.
|
|
163
|
+
const attDescriptor = fogaAttestedSchemeDescriptor();
|
|
164
|
+
const attInput = k.kya.attestedSchemeInput(attDescriptor);
|
|
165
|
+
const att = await k.call("T0.kya.S_att.register", W, k.kya.schemeRegistryAddress, k.kya.schemes().interface, "registerScheme", [attInput]);
|
|
166
|
+
const sAtt = k.kya.schemeIdFromReceipt(att.receipt);
|
|
167
|
+
k.setFact("T0.kya.S_att", { schemeId: sAtt, schemeHash: attInput.schemeHash, levelMask: attInput.levelMask.toString() });
|
|
168
|
+
// The credential-pattern ZK path: FOGA's own issuer set (one attestor key held by W) and its own instance of
|
|
169
|
+
// the upstream Groth16 adapter pinned to that set; S_cred admits proofs through it. Decided once per store.
|
|
170
|
+
const zkMode = await k.memo("T0.zk", async () => {
|
|
171
|
+
if (this.cfg.zk === "skip")
|
|
172
|
+
return "off";
|
|
173
|
+
const a = zkAvailability();
|
|
174
|
+
if (a.ok)
|
|
175
|
+
return "on";
|
|
176
|
+
if (this.cfg.zk === "require")
|
|
177
|
+
throw new Error(`the ZK path was required but is not available:\n ${a.missing.join("\n ")}`);
|
|
178
|
+
this.log(` zk: proving material not available, the credential-pattern path is left out of this store (${a.missing.join("; ")})`);
|
|
179
|
+
return "off";
|
|
180
|
+
});
|
|
181
|
+
let credVerifier = this.world.address("8419/Groth16KYAVerifierAdapter");
|
|
182
|
+
if (zkMode === "on") {
|
|
183
|
+
const attestor = attestorKeyFrom(k.signer(W).privateKey, "W");
|
|
184
|
+
const set = await k.memo("T0.kya.issuerSet", async () => {
|
|
185
|
+
const r = await zk.issuerSet([attestor]);
|
|
186
|
+
return { root: r.root, attestors: r.attestors };
|
|
187
|
+
});
|
|
188
|
+
const adapter = await k.deploy("T0.kya.adapter.deploy", W, "kya-standard:Groth16KYAVerifierAdapter", [this.world.address("8419/Groth16Verifier"), set.root, EPOCH_LENGTH, EPOCH_GRACE]);
|
|
189
|
+
k.setFact("T0.kya.adapter", adapter.address);
|
|
190
|
+
this.world.attach("foga/Groth16KYAVerifierAdapter", adapter.address, "kya-standard:Groth16KYAVerifierAdapter");
|
|
191
|
+
credVerifier = adapter.address;
|
|
192
|
+
const pinned = await this.world.contract("foga/Groth16KYAVerifierAdapter").requiredIssuerSetRoot();
|
|
193
|
+
this.check("T0", "zk.adapter", pinned === set.root, `FOGA's Groth16 adapter ${adapter.address} pins issuer set root ${set.root.slice(0, 10)}…`);
|
|
194
|
+
}
|
|
195
|
+
const credDescriptor = fogaProvedSchemeDescriptor();
|
|
196
|
+
const credInput = k.kya.provedSchemeInput(credDescriptor, credVerifier);
|
|
197
|
+
const cred = await k.call("T0.kya.S_cred.register", W, k.kya.schemeRegistryAddress, k.kya.schemes().interface, "registerScheme", [credInput]);
|
|
198
|
+
const sCred = k.kya.schemeIdFromReceipt(cred.receipt);
|
|
199
|
+
k.setFact("T0.kya.S_cred", { schemeId: sCred, schemeHash: credInput.schemeHash, verifier: credInput.verifier });
|
|
200
|
+
// The predicate-pattern ZK path: FOGA's own circuit (kya-predicate-count-v1), its generated Groth16 verifier
|
|
201
|
+
// and its adapter, pinned to a source set holding one source key (held by W: the node that indexes task
|
|
202
|
+
// completions and signs records of them). The scheme is registered on every machine; the proof (T3) needs the
|
|
203
|
+
// proving material built by prover/build-predicate.mjs. Decided once per store.
|
|
204
|
+
const predMode = await k.memo("T0.zk.predicate", async () => {
|
|
205
|
+
if (this.cfg.zk === "skip")
|
|
206
|
+
return "off";
|
|
207
|
+
const a = predicateAvailability();
|
|
208
|
+
if (a.ok)
|
|
209
|
+
return "on";
|
|
210
|
+
if (this.cfg.zk === "require")
|
|
211
|
+
throw new Error(`the predicate ZK path was required but is not available:\n ${a.missing.join("\n ")}`);
|
|
212
|
+
this.log(` zk: predicate proving material not available, the predicate proof is left out of this store (${a.missing.join("; ")})`);
|
|
213
|
+
return "off";
|
|
214
|
+
});
|
|
215
|
+
const sourceSet = await k.memo("T0.kya.sourceSet", async () => {
|
|
216
|
+
if (!proverLibsAvailable()) {
|
|
217
|
+
this.log(" zk: the proving libraries are not installed, so no source set root can be computed; the predicate adapter is deployed unpinned");
|
|
218
|
+
return { root: ZeroHash, sources: [], pinned: false };
|
|
219
|
+
}
|
|
220
|
+
const r = await zk.sourceSet([sourceKeyFrom(k.signer(W).privateKey, "completions")]);
|
|
221
|
+
return { root: r.root, sources: r.attestors, pinned: true };
|
|
222
|
+
});
|
|
223
|
+
const groth16Pred = await k.deploy("T0.kya.predicate.verifier.deploy", W, "foga:Groth16PredicateVerifier", []);
|
|
224
|
+
this.world.attach("foga/Groth16PredicateVerifier", groth16Pred.address, "foga:Groth16PredicateVerifier");
|
|
225
|
+
const predAdapter = await k.deploy("T0.kya.predicate.adapter.deploy", W, "foga:Groth16PredicateKYAVerifierAdapter", [groth16Pred.address, sourceSet.root, EPOCH_LENGTH, EPOCH_GRACE, PREDICATE_VALIDITY]);
|
|
226
|
+
this.world.attach("foga/Groth16PredicateKYAVerifierAdapter", predAdapter.address, "foga:Groth16PredicateKYAVerifierAdapter");
|
|
227
|
+
const predInput = k.kya.provedSchemeInput(fogaPredicateSchemeDescriptor(), predAdapter.address);
|
|
228
|
+
const pred = await k.call("T0.kya.S_pred.register", W, k.kya.schemeRegistryAddress, k.kya.schemes().interface, "registerScheme", [predInput]);
|
|
229
|
+
const sPred = k.kya.schemeIdFromReceipt(pred.receipt);
|
|
230
|
+
k.setFact("T0.kya.S_pred", { schemeId: sPred, schemeHash: predInput.schemeHash, verifier: predAdapter.address, groth16: groth16Pred.address, sourceSetRoot: sourceSet.root, pinned: sourceSet.pinned, zk: predMode });
|
|
231
|
+
// ASF specification FOGA-Cell-v1.
|
|
232
|
+
const spec = fogaCellSpecification();
|
|
233
|
+
const salt = keccakJcs({ spec: "FOGA-Cell-v1" });
|
|
234
|
+
const commitment = k.asf.specificationCommitment(spec);
|
|
235
|
+
const reg = await k.call("T0.asf.spec.register", W, k.asf.specRegistryAddress, k.asf.specs().interface, "registerSpecification", [salt, commitment.contentHash, commitment.locator, []]);
|
|
236
|
+
const specId = k.asf.specIdFromReceipt(reg.receipt);
|
|
237
|
+
k.setFact("T0.asf.spec", { specId, version: 1, contentHash: commitment.contentHash, salt });
|
|
238
|
+
// The ACDF policy FOGA-Council (judges J1-J3, two approvals decide) and FOGA's instance of the upstream
|
|
239
|
+
// ACDFTaskTenderAdapter bound to it: the judged acceptance authority of T5.
|
|
240
|
+
const judges = ["J1", "J2", "J3"].map((j) => k.address(j));
|
|
241
|
+
const descriptor = councilDescriptor({ judges, k: 2, window: this.timing.councilWindow, appealWindow: this.timing.appealWindow });
|
|
242
|
+
const policy = councilPolicySpec({ judges, k: 2, window: this.timing.councilWindow, appealWindow: this.timing.appealWindow, updateAuthority: k.address(W), descriptor });
|
|
243
|
+
const pol = await k.call("T0.acdf.policy.register", W, k.acdf.policyRegistryAddress, k.acdf.policies().interface, "registerPolicy", [policy]);
|
|
244
|
+
const policyId = k.acdf.policyIdFromReceipt(pol.receipt);
|
|
245
|
+
k.setFact("T0.acdf.policy", { policyId, judges, k: 2, window: this.timing.councilWindow, appealWindow: this.timing.appealWindow, maxTotalDuration: policy.maxTotalDuration, descriptorHash: policy.descriptorHash });
|
|
246
|
+
const tenderAdapter = await k.deploy("T0.acdf.tenderAdapter.deploy", W, "acd-framework:ACDFTaskTenderAdapter", [k.acdf.registryAddress, k.tenders.address, policyId, this.timing.margin]);
|
|
247
|
+
k.setFact("T0.acdf.tenderAdapter", tenderAdapter.address);
|
|
248
|
+
this.world.attach("foga/ACDFTaskTenderAdapter", tenderAdapter.address, "acd-framework:ACDFTaskTenderAdapter");
|
|
249
|
+
// The ADRF policy FOGA-Service-v1 (judgment tier = FOGA-Council through the bridge) and the world's service
|
|
250
|
+
// escrow bound to it by its admin, W: the dispute path of T6 exists before any call is opened.
|
|
251
|
+
const escrowAdmin = await k.adrf.escrow().admin();
|
|
252
|
+
if (getAddress(escrowAdmin) !== getAddress(k.address(W)))
|
|
253
|
+
throw new Error(`the world's ADRFServiceEscrow ${k.adrf.escrowAddress} is administered by ${escrowAdmin}, not by W ${k.address(W)}: the escrow must be deployed by W for the genesis to bind it (deploy the world with W's key: FOGA_MNEMONIC makes W the deployer, or set FOGA_DEPLOYER_PK to W's key)`);
|
|
254
|
+
const serviceParams = { escrow: k.adrf.escrowAddress, versionVerifier: k.adrf.versionVerifierAddress, bridge: k.adrf.bridgeAddress, councilPolicyId: policyId, timing: this.timing.service };
|
|
255
|
+
const servicePolicy = servicePolicySpec({ ...serviceParams, updateAuthority: k.address(W), descriptor: serviceDescriptor(serviceParams) });
|
|
256
|
+
const spol = await k.call("T0.adrf.policy.register", W, k.adrf.policyRegistryAddress, k.adrf.policies().interface, "registerPolicy", [servicePolicy]);
|
|
257
|
+
const servicePolicyId = k.adrf.policyIdFromReceipt(spol.receipt);
|
|
258
|
+
const bindingTerms = { kind: "binding-terms", escrow: k.adrf.escrowAddress.toLowerCase(), policy: "FOGA-Service-v1", funds: "the escrow enacts a refund only out of the settlement it holds for the disputed call", thirdParties: "none" };
|
|
259
|
+
const bindInput = escrowBindingInput(servicePolicyId, keccakJcs(bindingTerms));
|
|
260
|
+
const bound = await k.call("T0.adrf.escrow.bind", W, k.adrf.escrowAddress, k.adrf.escrow().interface, "bindTo", [bindInput]);
|
|
261
|
+
const bindingId = k.adrf.bindingIdFromReceipt(bound.receipt);
|
|
262
|
+
k.setFact("T0.adrf", { policyId: servicePolicyId, bindingId, escrow: k.adrf.escrowAddress, maxTotalDuration: servicePolicy.track.maxTotalDuration, timing: this.timing.service, descriptorHash: servicePolicy.descriptorHash, bindingTermsHash: bindInput.termsHash });
|
|
263
|
+
k.evidence("T0", "registrations", {
|
|
264
|
+
W: k.address(W),
|
|
265
|
+
schemes: { S_att: sAtt, S_cred: sCred, S_credVerifier: credVerifier, zk: zkMode, S_pred: sPred, S_predVerifier: predAdapter.address, S_predGroth16: groth16Pred.address, sourceSetRoot: sourceSet.root, zkPredicate: predMode },
|
|
266
|
+
specification: { specId, version: 1, contentHash: commitment.contentHash, locatorBytes: commitment.locator.length },
|
|
267
|
+
council: { policyId, judges, k: 2, window: this.timing.councilWindow, appealWindow: this.timing.appealWindow, tenderAdapter: tenderAdapter.address, margin: this.timing.margin },
|
|
268
|
+
service: { policyId: servicePolicyId, bindingId, escrow: k.adrf.escrowAddress, versionVerifier: k.adrf.versionVerifierAddress, bridge: k.adrf.bridgeAddress, timing: this.timing.service, maxTotalDuration: servicePolicy.track.maxTotalDuration },
|
|
269
|
+
txs: { S_att: att.txHash, S_cred: cred.txHash, S_pred: pred.txHash, spec: reg.txHash, policy: pol.txHash, servicePolicy: spol.txHash, escrowBind: bound.txHash },
|
|
270
|
+
});
|
|
271
|
+
// checks
|
|
272
|
+
const got = await k.asf.versionOf(specId, 1);
|
|
273
|
+
this.check("T0", "spec.pinned", got.contentHash === commitment.contentHash, `versionOf(${specId.slice(0, 10)}…, 1).contentHash matches keccak(JCS(spec))`);
|
|
274
|
+
const schemeOk = await k.kya.schemes().schemeExists(sAtt);
|
|
275
|
+
this.check("T0", "scheme.S_att", Boolean(schemeOk), `S_att ${sAtt.slice(0, 10)}… exists`);
|
|
276
|
+
const credOk = await k.kya.schemes().schemeExists(sCred);
|
|
277
|
+
this.check("T0", "scheme.S_cred", Boolean(credOk), `S_cred ${sCred.slice(0, 10)}… exists (verifier ${credVerifier})`);
|
|
278
|
+
const predOk = await k.kya.schemes().schemeExists(sPred);
|
|
279
|
+
const pinnedRoot = await this.world.contract("foga/Groth16PredicateKYAVerifierAdapter").requiredSourceSetRoot();
|
|
280
|
+
this.check("T0", "scheme.S_pred", Boolean(predOk) && pinnedRoot === sourceSet.root, `S_pred ${sPred.slice(0, 10)}… exists; FOGA's predicate adapter ${predAdapter.address} over verifier ${groth16Pred.address} pins source set root ${sourceSet.root.slice(0, 10)}…${sourceSet.pinned ? "" : " (unpinned: no proving libraries)"}`);
|
|
281
|
+
const policyOk = await k.acdf.policies().policyExists(policyId);
|
|
282
|
+
this.check("T0", "council.policy", Boolean(policyOk), `FOGA-Council ${policyId.slice(0, 10)}… registered: ${judges.length} judges, k = 2, window ${this.timing.councilWindow} s`);
|
|
283
|
+
const adapterPolicy = await this.world.contract("foga/ACDFTaskTenderAdapter").policyId();
|
|
284
|
+
const judged = !(await this.world.contract("foga/ACDFTaskTenderAdapter").supportsInterface("0x9977db15"));
|
|
285
|
+
this.check("T0", "council.tenderAdapter", adapterPolicy === policyId && judged, `ACDFTaskTenderAdapter ${tenderAdapter.address} bound to the policy; does not declare ITaskVerifier (judged authority)`);
|
|
286
|
+
const servicePolicyOk = await k.adrf.policies().policyExists(servicePolicyId);
|
|
287
|
+
const binding = await k.adrf.registry().getBinding(bindingId);
|
|
288
|
+
const escrowBindingId = await k.adrf.escrow().bindingId();
|
|
289
|
+
this.check("T0", "service.policy", Boolean(servicePolicyOk) && getAddress(binding.source) === getAddress(k.adrf.escrowAddress) && binding.policyId === servicePolicyId && escrowBindingId === bindingId, `FOGA-Service-v1 ${servicePolicyId.slice(0, 10)}… registered (decision window ${this.timing.service.decisionWindow} s holds the council's ${policy.maxTotalDuration} s); the escrow ${k.adrf.escrowAddress} is bound to it (binding ${bindingId.slice(0, 10)}…, refund authorized)`);
|
|
290
|
+
}
|
|
291
|
+
// ------------------------------------------------------------------------------------------------ T1 birth
|
|
292
|
+
async t1() {
|
|
293
|
+
for (const X of CELLS)
|
|
294
|
+
await this.t1Cell(X);
|
|
295
|
+
}
|
|
296
|
+
cell(X) {
|
|
297
|
+
const c = this.cells.get(X);
|
|
298
|
+
if (c)
|
|
299
|
+
return c;
|
|
300
|
+
const address = this.kernel.fact(`T1.${X}.address`);
|
|
301
|
+
if (!address)
|
|
302
|
+
throw new Error(`cell ${X} has not been born (run T1)`);
|
|
303
|
+
const cell = new Cell(this.kernel, X, address, { owner: ownerRole(X), operator: operatorRole(X), guardian: this.kernel.address("G") });
|
|
304
|
+
this.cells.set(X, cell);
|
|
305
|
+
return cell;
|
|
306
|
+
}
|
|
307
|
+
async t1Cell(X) {
|
|
308
|
+
const k = this.kernel;
|
|
309
|
+
const spec = k.requireFact("T0.asf.spec");
|
|
310
|
+
const sAtt = k.requireFact("T0.kya.S_att").schemeId;
|
|
311
|
+
const G = k.address("G");
|
|
312
|
+
const roles = { owner: ownerRole(X), operator: operatorRole(X), guardian: G };
|
|
313
|
+
const chainId = this.chainId;
|
|
314
|
+
// 1. the body: deployed by the owner; the requirement names the account itself
|
|
315
|
+
const cell = await Cell.deploy(k, {
|
|
316
|
+
key: `T1.${X}.deploy`,
|
|
317
|
+
name: X,
|
|
318
|
+
roles,
|
|
319
|
+
requirement: (predicted) => oneClause({
|
|
320
|
+
specRegistry: k.asf.specRegistryAddress,
|
|
321
|
+
specId: spec.specId,
|
|
322
|
+
versions: [spec.version],
|
|
323
|
+
adopter: predicted,
|
|
324
|
+
subjectKey: accountSubjectKey(chainId, predicted),
|
|
325
|
+
scopeKey: scopeKeyOf({ ref: accountRef(chainId, predicted) }),
|
|
326
|
+
enforcement: Enforcement.account | Enforcement.contract,
|
|
327
|
+
interveners: [{ account: G, rights: Rights.suspend | Rights.resume }],
|
|
328
|
+
}),
|
|
329
|
+
});
|
|
330
|
+
this.cells.set(X, cell);
|
|
331
|
+
k.setFact(`T1.${X}.address`, cell.address);
|
|
332
|
+
this.log(` ${X}: account ${cell.address} (owner ${cell.ownerAddress}, operator ${cell.operatorAddress})`);
|
|
333
|
+
// 2. working capital: W funds the account (gas is the operator's own)
|
|
334
|
+
await k.transfer(`T1.${X}.fund`, "W", cell.address, k.isLocalChain ? CELL_CAPITAL.local : CELL_CAPITAL.public);
|
|
335
|
+
// 3. owner administration: operator scope, AID registry for the commitment gate, selector classes
|
|
336
|
+
await cell.admin(`T1.${X}.setOperator`, "setOperator", [cell.operatorAddress, 0, parseEther("0.01")]);
|
|
337
|
+
// the operator's allow-list (account revision 2): the groups this cell plays, resolved to target x selector;
|
|
338
|
+
// what the genesis deploys later and opens to one cell (the swarm's account, the decision adapter) is added then
|
|
339
|
+
const scope = operatorScope(k, CELL_GROUPS[X]);
|
|
340
|
+
await cell.admin(`T1.${X}.scope`, "setOperatorAllowedBatch", [scope.targets, scope.selectors, true]);
|
|
341
|
+
k.setFact(`T1.${X}.scope`, { groups: CELL_GROUPS[X], entries: scope.names });
|
|
342
|
+
await cell.admin(`T1.${X}.setAIDRegistry`, "setAIDRegistry", [k.aid.address]);
|
|
343
|
+
const tasks = k.tenders.contract().interface;
|
|
344
|
+
const swarms = this.world.iface("8447/SwarmRegistry");
|
|
345
|
+
const assertions = this.world.iface("8454/AUFAssertionRegistry");
|
|
346
|
+
const commitment = [
|
|
347
|
+
["8414.submitFulfillment", k.tenders.address, this.selector(tasks, "submitFulfillment")],
|
|
348
|
+
["8447.join", this.world.address("8447/SwarmRegistry"), this.selector(swarms, "join")],
|
|
349
|
+
["8447.applyToJoin", this.world.address("8447/SwarmRegistry"), this.selector(swarms, "applyToJoin")],
|
|
350
|
+
["8454.assertStatement", this.world.address("8454/AUFAssertionRegistry"), this.selector(assertions, "assertStatement")],
|
|
351
|
+
["8436.castBallot", k.acdf.registryAddress, this.selector(k.acdf.registry().interface, "castBallot")],
|
|
352
|
+
];
|
|
353
|
+
for (const [name, target, sel] of commitment)
|
|
354
|
+
await cell.admin(`T1.${X}.class.${name}`, "setCommitmentClass", [target, sel, true]);
|
|
355
|
+
const derisk = [
|
|
356
|
+
["8414.withdrawCredit", k.tenders.address, this.selector(tasks, "withdrawCredit")],
|
|
357
|
+
["8414.reclaimEscrow", k.tenders.address, this.selector(tasks, "reclaimEscrow")],
|
|
358
|
+
["8447.leave", this.world.address("8447/SwarmRegistry"), this.selector(swarms, "leave")],
|
|
359
|
+
["8471.withdrawBalance", k.adrf.escrowAddress, this.selector(k.adrf.escrow().interface, "withdrawBalance")],
|
|
360
|
+
];
|
|
361
|
+
for (const [name, target, sel] of derisk)
|
|
362
|
+
await cell.admin(`T1.${X}.derisk.${name}`, "setDeriskAllowed", [target, sel, true]);
|
|
363
|
+
// 4. the safety binding: adopted by the account, presented for clause 0
|
|
364
|
+
const params = { profile: "foga-cell/1", cell: X, operatorScope: CELL_GROUPS[X], deriskActions: derisk.map((d) => d[0]), commitmentActions: commitment.map((c) => c[0]) };
|
|
365
|
+
const authority = { kind: "owner", owner: accountRef(chainId, cell.ownerAddress), guardian: accountRef(chainId, G) };
|
|
366
|
+
const input = k.asf.cellBindingInput({ chainId, cell: cell.address, specId: spec.specId, version: spec.version, contentHash: spec.contentHash, guardian: G, params, authority });
|
|
367
|
+
const { bindingId } = await cell.adoptBinding(`T1.${X}.adopt`, input, 0);
|
|
368
|
+
k.setFact(`T1.${X}.bindingId`, bindingId);
|
|
369
|
+
// 5. identity: ERC-8004 registration by the account itself
|
|
370
|
+
const registration = {
|
|
371
|
+
type: "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
|
|
372
|
+
name: `FOGA cell ${X}`,
|
|
373
|
+
description: `Cell ${X} of the Finch OG Agent genesis: an address acting under the ten-ERC stack`,
|
|
374
|
+
services: [],
|
|
375
|
+
supportedTrust: ["reputation"],
|
|
376
|
+
"x-foga": { cell: X, anchor: accountRef(chainId, cell.address), binding: bindingId },
|
|
377
|
+
};
|
|
378
|
+
const reg = await cell.execute(`T1.${X}.8004.register`, k.identity.address, k.identity.encodeRegister(dataUri(registration)));
|
|
379
|
+
const agentId = k.identity.agentIdFromReceipt(reg.receipt);
|
|
380
|
+
k.setFact(`T1.${X}.agentId`, agentId.toString());
|
|
381
|
+
// 6. the anchor: bind, heartbeat; the document follows the attestation so it can cite it. Where another
|
|
382
|
+
// standard's adapter reads a second ERC-8434 instance (Sepolia: the ARD agent kind is anchored to ARD's own
|
|
383
|
+
// fixture registry), the cell keeps its liveness there as well, so its records resolve as backed.
|
|
384
|
+
await cell.execute(`T1.${X}.aid.bind`, k.aid.address, k.aid.encodeBind(k.identity.address, agentId));
|
|
385
|
+
await cell.execute(`T1.${X}.aid.heartbeat`, k.aid.address, k.aid.encodeHeartbeat());
|
|
386
|
+
if (k.aid.backing)
|
|
387
|
+
await cell.execute(`T1.${X}.aid.backing.bind`, k.aid.backing.address, k.aid.encodeBind(k.identity.address, agentId));
|
|
388
|
+
// 7. standing: W attests S_att level 2 ("bound") for the account subject
|
|
389
|
+
const claim = { scheme: "S_att", level: 2, subject: accountRef(chainId, cell.address), binding: bindingId, basis: "operates under FOGA-Cell-v1 with a guardian intervener" };
|
|
390
|
+
const claimDigest = keccakJcs(claim);
|
|
391
|
+
const expiresAt = await k.memo(`T1.${X}.kya.expiresAt`, async () => (await this.world.blockTimestamp()) + 365 * 86400);
|
|
392
|
+
const subject = accountSubject(chainId, cell.address);
|
|
393
|
+
const att = await k.call(`T1.${X}.kya.attest`, "W", k.kya.registryAddress, k.kya.registry().interface, "attest", [subject, sAtt, 2, claimDigest, expiresAt, dataUriJcs(claim), claimDigest]);
|
|
394
|
+
const assertionId = k.kya.assertionIdFromReceipt(att.receipt);
|
|
395
|
+
k.setFact(`T1.${X}.assertionId`, assertionId);
|
|
396
|
+
// 8. the AID document: identity facet (SELF) and standing facet (ATTESTED), committed by keccak(JCS)
|
|
397
|
+
const doc = {
|
|
398
|
+
version: "aid-document/v1",
|
|
399
|
+
aid: accountRef(chainId, cell.address),
|
|
400
|
+
did: `did:aid:${accountRef(chainId, cell.address)}`,
|
|
401
|
+
binding: { registry: k.identity.address, agentId: Number(agentId) },
|
|
402
|
+
livenessWindow: Number(this.world.file.params.aid.defaultLivenessWindow),
|
|
403
|
+
facets: [
|
|
404
|
+
{ facetType: "aid:core/identity/v1", provenance: "SELF", issuer: accountRef(chainId, cell.address), validUntil: 0, digest: keccakJcs(registration), access: { mode: "PUBLIC" }, resolver: { kind: "erc8004-identity", chainId, registry: k.identity.address, agentId: Number(agentId) } },
|
|
405
|
+
{ facetType: "aid:core/kya/v1", provenance: "ATTESTED", issuer: accountRef(chainId, k.address("W")), validUntil: expiresAt, digest: claimDigest, access: { mode: "PUBLIC" }, resolver: { kind: "erc8419-assertion", chainId, registry: k.kya.registryAddress, schemeId: sAtt, issuers: [k.address("W")], assertionId } },
|
|
406
|
+
],
|
|
407
|
+
};
|
|
408
|
+
const commitment2 = AID.documentCommitment(doc);
|
|
409
|
+
await cell.execute(`T1.${X}.aid.document`, k.aid.address, k.aid.encodeSetDocumentURI(commitment2.uri, commitment2.digest));
|
|
410
|
+
k.setFact(`T1.${X}.documentDigest`, commitment2.digest);
|
|
411
|
+
// 9. discovery: the ARD agent record, controlled by the anchor itself
|
|
412
|
+
const descriptor = agentDescriptor({
|
|
413
|
+
chainId,
|
|
414
|
+
anchor: cell.address,
|
|
415
|
+
slug: `cell-${X.toLowerCase()}`,
|
|
416
|
+
name: `FOGA cell ${X}`,
|
|
417
|
+
description: registration.description,
|
|
418
|
+
capabilities: ["erc8414:fulfiller", "erc8338:holder"],
|
|
419
|
+
registration: { registry: `eip155:${chainId}:${k.identity.address.toLowerCase()}`, agentId: agentId.toString() },
|
|
420
|
+
});
|
|
421
|
+
const ard = await cell.execute(`T1.${X}.ard.register`, k.ard.address, k.ard.encodeRegister(kindId("agent"), cell.address, 0n, descriptor));
|
|
422
|
+
const recordId = k.ard.recordIdFromReceipt(ard.receipt);
|
|
423
|
+
k.setFact(`T1.${X}.recordId`, recordId);
|
|
424
|
+
// 10. the economic boundary: the cell's unit (controller = the account)
|
|
425
|
+
const unitDef = cellUnitDefinition({ chainId, cell: cell.address, name: `FOGA cell ${X}` });
|
|
426
|
+
const unit = await cell.execute(`T1.${X}.auf.unit`, k.units.registryAddress, k.units.encodeRegisterUnit(cell.address, unitDef, []));
|
|
427
|
+
const unitId = k.units.unitIdFromReceipt(unit.receipt);
|
|
428
|
+
k.setFact(`T1.${X}.unitId`, unitId.toString());
|
|
429
|
+
k.evidence("T1", X, {
|
|
430
|
+
account: cell.address,
|
|
431
|
+
owner: cell.ownerAddress,
|
|
432
|
+
operator: cell.operatorAddress,
|
|
433
|
+
guardian: G,
|
|
434
|
+
requirementId: requirementIdOf(await this.requirementOf(cell)),
|
|
435
|
+
bindingId,
|
|
436
|
+
agentId: agentId.toString(),
|
|
437
|
+
assertionId,
|
|
438
|
+
documentDigest: commitment2.digest,
|
|
439
|
+
recordId,
|
|
440
|
+
unitId: unitId.toString(),
|
|
441
|
+
capital: k.store.get(`T1.${X}.fund`)?.txHash ? (k.isLocalChain ? CELL_CAPITAL.local : CELL_CAPITAL.public).toString() : null,
|
|
442
|
+
backingRegistry: k.aid.backing?.address ?? null,
|
|
443
|
+
txs: { deploy: k.store.get(`T1.${X}.deploy`)?.txHash, adopt: k.store.get(`T1.${X}.adopt`)?.txHash, register8004: reg.txHash, attest: att.txHash, ard: ard.txHash, unit: unit.txHash, backingBind: k.store.get(`T1.${X}.aid.backing.bind`)?.txHash ?? null },
|
|
444
|
+
});
|
|
445
|
+
// checks (memo T1 "expected")
|
|
446
|
+
const state = await k.aid.stateName(cell.address);
|
|
447
|
+
const untilA = X === "A" ? ["T9.A.8004.transfer", "T9.A.aid.retire"] : [];
|
|
448
|
+
this.check("T1", `${X}.aid.active`, state === "ACTIVE", `state(${cell.address}) = ${state}`, untilA);
|
|
449
|
+
const passes = await k.kya.check(subject, sAtt, 2, [k.address("W")]);
|
|
450
|
+
this.check("T1", `${X}.kya.level2`, passes === true, `check({account, cell}, S_att, 2, [W]) = ${passes}`);
|
|
451
|
+
const status = await cell.status();
|
|
452
|
+
this.check("T1", `${X}.asf.satisfied`, status.verdict === 1, `status() = ${status.verdictName} [${status.reasonNames.join(",")}]`, X === "A" ? "T7.G.suspend" : []);
|
|
453
|
+
if (k.aid.backing) {
|
|
454
|
+
const backingState = await k.aid.stateName(cell.address, k.aid.backing.address);
|
|
455
|
+
this.check("T1", `${X}.aid.backing`, backingState === "ACTIVE", `state(${cell.address}) in the backing registry ${k.aid.backing.address} (${k.aid.backing.id}) = ${backingState}`, untilA);
|
|
456
|
+
}
|
|
457
|
+
const backed = await k.ard.backed(recordId);
|
|
458
|
+
this.check("T1", `${X}.ard.backed`, backed === true, `backed(${recordId.slice(0, 10)}…) = ${backed}${k.aid.backing ? " (the ARD agent adapter reads the backing registry)" : ""}`, untilA);
|
|
459
|
+
const controller = await k.units.controllerOf(unitId);
|
|
460
|
+
this.check("T1", `${X}.auf.unit`, getAddress(controller) === getAddress(cell.address), `controllerOf(unit ${unitId}) = ${controller}`);
|
|
461
|
+
const owner8004 = await k.identity.ownerOf(agentId);
|
|
462
|
+
this.check("T1", `${X}.8004.owner`, getAddress(owner8004) === getAddress(cell.address), `ownerOf(agent ${agentId}) = ${owner8004}`, X === "A" ? "T9.A.8004.transfer" : []);
|
|
463
|
+
if (X === "A")
|
|
464
|
+
await (await import("./t1-zk.js")).credentialProof(this, cell);
|
|
465
|
+
const reqOnchain = await cell.contract().requirementId();
|
|
466
|
+
const reqLocal = requirementIdOf(await this.requirementOf(cell));
|
|
467
|
+
this.check("T1", `${X}.requirementId`, reqOnchain === reqLocal, `account.requirementId() ${reqOnchain.slice(0, 10)}… equals keccak(abi.encode(requirement)) computed off-chain`);
|
|
468
|
+
const bindingOnchain = await k.asf.computeBindingId(cell.address, input);
|
|
469
|
+
this.check("T1", `${X}.bindingId`, bindingOnchain === bindingId, `computeBindingId(account, input) matches the adopted binding ${bindingId.slice(0, 10)}…`);
|
|
470
|
+
}
|
|
471
|
+
async requirementOf(cell) {
|
|
472
|
+
const r = await cell.contract().requirement();
|
|
473
|
+
return {
|
|
474
|
+
combinator: Number(r.combinator),
|
|
475
|
+
k: Number(r.k),
|
|
476
|
+
clauses: Array.from(r.clauses, (c) => ({
|
|
477
|
+
specRegistry: String(c.specRegistry),
|
|
478
|
+
specId: String(c.specId),
|
|
479
|
+
versions: Array.from(c.versions, (v) => Number(v)),
|
|
480
|
+
adopter: String(c.adopter),
|
|
481
|
+
subjectKey: String(c.subjectKey),
|
|
482
|
+
scopeKey: String(c.scopeKey),
|
|
483
|
+
enforcement: Number(c.enforcement),
|
|
484
|
+
interveners: Array.from(c.interveners, (i) => ({ account: String(i.account), rights: Number(i.rights) })),
|
|
485
|
+
})),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
// ------------------------------------------------------------------------------------------------ T2 capability
|
|
489
|
+
/** T2: A packages and mints its two skills, publishes their records and units, and updates its document. */
|
|
490
|
+
async t2() {
|
|
491
|
+
const k = this.kernel;
|
|
492
|
+
const A = this.cell("A");
|
|
493
|
+
const chainId = this.chainId;
|
|
494
|
+
const packages = {};
|
|
495
|
+
const minted = {};
|
|
496
|
+
for (const name of SKILL_NAMES) {
|
|
497
|
+
const dir = join(this.cfg.skillsDir, name);
|
|
498
|
+
const pkg = buildSkillPackage(readSkillDir(dir));
|
|
499
|
+
packages[name] = pkg;
|
|
500
|
+
const packageURI = dataUriJcs({ skill: name, version: pkg.version, mdHash: pkg.mdHash, packageHash: pkg.packageHash, rootCid: pkg.rootCid, source: `scenarios/skills/${name}` });
|
|
501
|
+
const mint = await A.execute(`T2.A.skill.${name}.mint`, k.skills.address, k.skills.encodeMint(A.address, A.address, pkg, packageURI));
|
|
502
|
+
const tokenId = k.skills.tokenIdFromReceipt(mint.receipt);
|
|
503
|
+
const descriptor = skillDescriptor({ chainId, controller: A.address, slug: name, name, description: String(pkg.manifest.summary), packageHash: pkg.packageHash, packageURI, price: { amount: name === "foga-probe" ? "0" : "0.001", currency: "ETH", asset: `eip155:${chainId}/slip44:60` } });
|
|
504
|
+
const rec = await A.execute(`T2.A.skill.${name}.ard`, k.ard.address, k.ard.encodeRegister(kindId("skill"), k.skills.address, tokenId, descriptor));
|
|
505
|
+
const recordId = k.ard.recordIdFromReceipt(rec.receipt);
|
|
506
|
+
const unitDef = skillUnitDefinition({ chainId, skillToken: k.skills.address, tokenId, holder: A.address, name });
|
|
507
|
+
const unit = await A.execute(`T2.A.skill.${name}.unit`, k.units.registryAddress, k.units.encodeRegisterUnit(A.address, unitDef, []));
|
|
508
|
+
const unitId = k.units.unitIdFromReceipt(unit.receipt);
|
|
509
|
+
minted[name] = { tokenId: tokenId.toString(), recordId, unitId: unitId.toString() };
|
|
510
|
+
k.setFact(`T2.skill.${name}`, { tokenId: tokenId.toString(), mdHash: pkg.mdHash, packageHash: pkg.packageHash, recordId, unitId: unitId.toString() });
|
|
511
|
+
const onchain = await k.skills.skillOf(tokenId);
|
|
512
|
+
this.check("T2", `${name}.commitment`, onchain.packageHash === pkg.packageHash && onchain.mdHash === pkg.mdHash && onchain.version === 1n, `skillOf(${tokenId}) = {mdHash ${pkg.mdHash.slice(0, 10)}…, packageHash ${pkg.packageHash.slice(0, 10)}…, v1}`, name === "finch-extract-verify" ? "T6.A.skill.v2.update" : []);
|
|
513
|
+
const owner = await k.skills.ownerOf(tokenId);
|
|
514
|
+
this.check("T2", `${name}.owner`, getAddress(owner) === getAddress(A.address), `ownerOf(${tokenId}) = ${owner}`, name === "finch-extract-verify" ? "T9.A.skill.transfer" : []);
|
|
515
|
+
const backed = await k.ard.backed(recordId);
|
|
516
|
+
this.check("T2", `${name}.ard.backed`, backed, `backed(skill record) = ${backed}`);
|
|
517
|
+
}
|
|
518
|
+
// the tampered-package case: one changed byte in the primary document changes both commitments
|
|
519
|
+
const tampered = { ...readSkillDir(join(this.cfg.skillsDir, "foga-probe")) };
|
|
520
|
+
tampered["SKILL.md"] = new Uint8Array([...tampered["SKILL.md"], 0x0a]);
|
|
521
|
+
const tp = buildSkillPackage(tampered);
|
|
522
|
+
this.check("T2", "tamper.detected", tp.packageHash !== packages["foga-probe"].packageHash && tp.mdHash !== packages["foga-probe"].mdHash, `appending one byte to SKILL.md: packageHash ${tp.packageHash.slice(0, 10)}… != ${packages["foga-probe"].packageHash.slice(0, 10)}…`);
|
|
523
|
+
// the document now lists the skills (facet aid:skills/erc8338/v1)
|
|
524
|
+
const prior = await k.aid.documentURI(A.address);
|
|
525
|
+
const expectedPrior = k.store.get("T2.A.aid.document")?.status === "confirmed" ? k.requireFact("T2.A.documentDigest") : k.requireFact("T1.A.documentDigest");
|
|
526
|
+
this.check("T2", "A.document.prior", prior.digest === expectedPrior, `documentURI(A).digest matches the kernel's latest commitment ${expectedPrior.slice(0, 10)}…`);
|
|
527
|
+
const skillsFacet = {
|
|
528
|
+
facetType: "aid:skills/erc8338/v1",
|
|
529
|
+
provenance: "SELF",
|
|
530
|
+
issuer: accountRef(chainId, A.address),
|
|
531
|
+
validUntil: 0,
|
|
532
|
+
digest: keccakJcs(minted),
|
|
533
|
+
access: { mode: "PUBLIC" },
|
|
534
|
+
resolver: { kind: "erc8338-skill", chainId, registry: k.skills.address, tokenIds: Object.values(minted).map((m) => m.tokenId) },
|
|
535
|
+
};
|
|
536
|
+
const doc = await this.documentOf(A);
|
|
537
|
+
doc.facets = [...doc.facets.filter((f) => f.facetType !== "aid:skills/erc8338/v1"), skillsFacet];
|
|
538
|
+
const c2 = AID.documentCommitment(doc);
|
|
539
|
+
await A.execute("T2.A.aid.document", k.aid.address, k.aid.encodeSetDocumentURI(c2.uri, c2.digest));
|
|
540
|
+
k.setFact("T2.A.documentDigest", c2.digest);
|
|
541
|
+
const now = await k.aid.documentURI(A.address);
|
|
542
|
+
this.check("T2", "A.document.updated", now.digest === c2.digest, `documentURI(A).digest = ${c2.digest.slice(0, 10)}… (skills facet added)`);
|
|
543
|
+
k.evidence("T2", "skills", {
|
|
544
|
+
holder: A.address,
|
|
545
|
+
skills: Object.fromEntries(Object.entries(minted).map(([n, m]) => [n, { ...m, mdHash: packages[n].mdHash, packageHash: packages[n].packageHash, rootCid: packages[n].rootCid }])),
|
|
546
|
+
documentDigest: c2.digest,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
/** Re-read a cell's AID document from the chain (data: URI) so an update builds on what is published. */
|
|
550
|
+
async documentOf(cell) {
|
|
551
|
+
const { parseDataUri } = await import("../core/canonical.js");
|
|
552
|
+
const { uri } = await this.kernel.aid.documentURI(cell.address);
|
|
553
|
+
return parseDataUri(uri);
|
|
554
|
+
}
|
|
555
|
+
// ------------------------------------------------------------------------------------------------ T3 labour
|
|
556
|
+
/**
|
|
557
|
+
* T3: B tenders "the foga-probe report of C at block N" with a hashlock; A discovers it, verifies its skill
|
|
558
|
+
* package against the chain, runs the probe in an isolated worker, submits, settles and is paid.
|
|
559
|
+
*/
|
|
560
|
+
async t3() {
|
|
561
|
+
const k = this.kernel;
|
|
562
|
+
const A = this.cell("A");
|
|
563
|
+
const B = this.cell("B");
|
|
564
|
+
const C = this.cell("C");
|
|
565
|
+
const chainId = this.chainId;
|
|
566
|
+
const sAtt = k.requireFact("T0.kya.S_att").schemeId;
|
|
567
|
+
const probeSkill = k.requireFact("T2.skill.foga-probe");
|
|
568
|
+
const reward = parseEther("0.001");
|
|
569
|
+
// --- B, as demander: the known answer is the probe of C at a fixed block
|
|
570
|
+
const blockN = await k.memo("T3.block", () => k.blockNumber());
|
|
571
|
+
// the log-based sections scan the 9 000 blocks before N (about 30 hours on a 12-second chain): public endpoints cap
|
|
572
|
+
// the range of eth_getLogs, and the range is part of the committed input so both runs read the same one
|
|
573
|
+
const input = { subject: C.address, block: blockN, lookback: 9_000, kya: { schemeId: sAtt, issuers: [k.address("W")] } };
|
|
574
|
+
const demanderRun = await runProbe(this.world, input);
|
|
575
|
+
const answerHash = hashlock.answerHash(demanderRun.answer);
|
|
576
|
+
k.setFact("T3.answerHash", answerHash);
|
|
577
|
+
const task = {
|
|
578
|
+
schema: "foga.task/1",
|
|
579
|
+
skill: "foga-probe",
|
|
580
|
+
packageHash: probeSkill.packageHash,
|
|
581
|
+
input,
|
|
582
|
+
acceptance: { mode: "verifier", profile: "x-hashlock-v1", verifier: k.tenders.hashlockAddress, answerHash },
|
|
583
|
+
deliverable: "the canonical foga.probe/1 report; resultHash = sha256(report || fulfiller)",
|
|
584
|
+
};
|
|
585
|
+
const taskHash = sha256Hex(JSON.stringify(task));
|
|
586
|
+
const taskURI = dataUri(task);
|
|
587
|
+
const tdHash = keccakJcs({ td: "foga.task/1", skill: "foga-probe" });
|
|
588
|
+
const settleBy = await k.memo("T3.settleBy", async () => (await this.world.blockTimestamp()) + 7 * 86400);
|
|
589
|
+
const terms = nativeTerms(reward, { submitBy: BigInt(settleBy - 86400), settleBy: BigInt(settleBy) });
|
|
590
|
+
const mint = await B.execute("T3.B.mint", k.tenders.address, k.tenders.encodeMint({ to: B.address, updateAuthority: B.address, acceptanceAuthority: k.tenders.hashlockAddress, tdHash, taskHash, taskURI, terms }));
|
|
591
|
+
const tokenId = k.tenders.tokenIdFromReceipt(mint.receipt);
|
|
592
|
+
k.setFact("T3.tokenId", tokenId.toString());
|
|
593
|
+
await B.execute("T3.B.fund", k.tenders.address, k.tenders.encodeFund(tokenId, reward), { value: reward });
|
|
594
|
+
await B.execute("T3.B.commit", k.tenders.hashlockAddress, k.tenders.encodeCommitAnswer(tokenId, answerHash));
|
|
595
|
+
const taskDesc = taskDescriptor({
|
|
596
|
+
chainId,
|
|
597
|
+
demander: B.address,
|
|
598
|
+
slug: `probe-c-at-${blockN}`,
|
|
599
|
+
name: `foga-probe of cell C at block ${blockN}`,
|
|
600
|
+
wants: "the canonical foga.probe/1 report of the subject at the stated block",
|
|
601
|
+
taskHash,
|
|
602
|
+
taskURI,
|
|
603
|
+
bounty: { amount: "0.001", currency: "ETH", asset: `eip155:${chainId}/slip44:60` },
|
|
604
|
+
acceptance: { mode: "verifier", ref: accountRef(chainId, k.tenders.hashlockAddress) },
|
|
605
|
+
settleBy,
|
|
606
|
+
judgmentWindow: "P7D",
|
|
607
|
+
});
|
|
608
|
+
const rec = await B.execute("T3.B.ard", k.ard.address, k.ard.encodeRegister(kindId("task"), k.tenders.address, tokenId, taskDesc));
|
|
609
|
+
const taskRecordId = k.ard.recordIdFromReceipt(rec.receipt);
|
|
610
|
+
k.setFact("T3.recordId", taskRecordId);
|
|
611
|
+
// Checks on states a later operation changes are made only while that operation is still ahead of us: on a
|
|
612
|
+
// resumed run the chain already shows the end state, which the end-state checks below cover.
|
|
613
|
+
const submitted = k.store.get("T3.A.submit")?.status === "confirmed";
|
|
614
|
+
const settled = k.store.get("T3.A.settle")?.status === "confirmed";
|
|
615
|
+
if (!settled) {
|
|
616
|
+
const escrow = await k.tenders.escrowBalanceOf(tokenId);
|
|
617
|
+
this.check("T3", "B.escrow", escrow === reward, `escrowBalanceOf(${tokenId}) = ${escrow} wei`);
|
|
618
|
+
const taskBacked = await k.ard.backed(taskRecordId);
|
|
619
|
+
this.check("T3", "B.ard.backed", taskBacked, `task record backed (funded, not cancelled, before settleBy) = ${taskBacked}`);
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
this.skipped("T3", "B.escrow", "replayed run: the tender was already settled");
|
|
623
|
+
this.skipped("T3", "B.ard.backed", "replayed run: the tender was already settled");
|
|
624
|
+
}
|
|
625
|
+
// --- A, as fulfiller: discover through ARD, pre-check, verify the package, run in isolation, submit, settle
|
|
626
|
+
const found = await k.ard.recordOf(taskRecordId);
|
|
627
|
+
const { parseDataUri } = await import("../core/canonical.js");
|
|
628
|
+
const discovered = parseDataUri(found.descriptorURI);
|
|
629
|
+
const specURI = discovered.data.specURI;
|
|
630
|
+
const spec = parseDataUri(specURI);
|
|
631
|
+
this.check("T3", "A.discovery", spec.skill === "foga-probe" && spec.input.subject.toLowerCase() === C.address.toLowerCase(), `ARD record ${taskRecordId.slice(0, 10)}… -> task ${tokenId}: skill ${spec.skill}, subject ${spec.input.subject}, block ${spec.input.block}`);
|
|
632
|
+
if (!submitted) {
|
|
633
|
+
const pre = await this.precheck(A, tokenId);
|
|
634
|
+
this.check("T3", "A.precheck", pre.ok, pre.detail);
|
|
635
|
+
if (!pre.ok)
|
|
636
|
+
throw new Error(`A refuses the task: ${pre.detail}`);
|
|
637
|
+
}
|
|
638
|
+
else
|
|
639
|
+
this.skipped("T3", "A.precheck", "replayed run: the submission was already made");
|
|
640
|
+
const pkg = await verifiedPackage(k, join(this.cfg.skillsDir, "foga-probe"), BigInt(probeSkill.tokenId));
|
|
641
|
+
this.check("T3", "A.skill.verified", pkg.packageHash === spec.packageHash, `local package ${pkg.packageHash.slice(0, 10)}… matches token ${probeSkill.tokenId} and the task's packageHash`);
|
|
642
|
+
const started = Date.now();
|
|
643
|
+
const run = await runProbeIsolated({ worldPath: this.cfg.worldPath, rpc: this.cfg.rpc, bundlePath: this.cfg.bundlePath, input: spec.input });
|
|
644
|
+
const reproduced = hashlock.answerHash(run.answer) === spec.acceptance.answerHash;
|
|
645
|
+
// The probe reads the chain at block N. A run replayed long after N against an endpoint that no longer serves state
|
|
646
|
+
// that old (a public cluster of non-archive nodes keeps about 128 blocks) cannot reproduce the answer the chain
|
|
647
|
+
// accepted; the settlement checked below is the proof of that answer, so the re-run is recorded as skipped, not failed.
|
|
648
|
+
const stale = settled && !reproduced;
|
|
649
|
+
if (!stale)
|
|
650
|
+
this.check("T3", "A.answer", reproduced, `isolated run (${Date.now() - started} ms): sha256(answer) matches the committed answerHash`);
|
|
651
|
+
else
|
|
652
|
+
this.skipped("T3", "A.answer", `replayed run: the probe re-run at block ${blockN} does not reproduce the committed answer any more (the endpoint no longer serves state at that block); the settlement on chain is the proof of the original answer`);
|
|
653
|
+
// the result hash and the run receipt are fixed by the first run: the submission was made from them (a stale
|
|
654
|
+
// replay takes the result hash from the submission the chain holds)
|
|
655
|
+
const receiptDoc = await k.memo("T3.A.runReceipt", () => ({ schema: "foga.run-receipt/1", skill: "foga-probe", packageHash: pkg.packageHash, tokenId: probeSkill.tokenId, input: spec.input, answerHash: hashlock.answerHash(run.answer), fulfiller: accountRef(chainId, A.address), worker: "isolated child process, view calls only" }));
|
|
656
|
+
let resultHash = k.fact("T3.resultHash") ?? (stale ? ZERO32 : hashlock.resultHash(run.answer, A.address));
|
|
657
|
+
const submit = await A.execute("T3.A.submit", k.tenders.address, k.tenders.encodeSubmit(tokenId, resultHash, dataUriJcs(receiptDoc)));
|
|
658
|
+
const submissionId = k.tenders.submissionIdFromReceipt(submit.receipt);
|
|
659
|
+
k.setFact("T3.submissionId", submissionId.toString());
|
|
660
|
+
if (resultHash === ZERO32)
|
|
661
|
+
resultHash = (await k.tenders.submissionOf(tokenId, submissionId)).resultHash;
|
|
662
|
+
k.setFact("T3.resultHash", resultHash);
|
|
663
|
+
// negatives, while the submission is still pending
|
|
664
|
+
if (!settled) {
|
|
665
|
+
const wrong = await this.wouldRevert(A, k.tenders.address, k.tenders.encodeSettle(tokenId, submissionId, Buffer.from(run.answer + " ", "utf8")));
|
|
666
|
+
this.check("T3", "wrong.answer.refused", wrong !== null && /verification failed/.test(wrong), `settling with a wrong proof reverts: ${wrong}`);
|
|
667
|
+
}
|
|
668
|
+
else
|
|
669
|
+
this.skipped("T3", "wrong.answer.refused", "replayed run: the tender was already settled");
|
|
670
|
+
if (!stale) {
|
|
671
|
+
const verifier = k.tenders.hashlockVerifier();
|
|
672
|
+
const asA = await verifier.verifyFulfillment(k.tenders.address, tokenId, submissionId, A.address, resultHash, Buffer.from(run.answer, "utf8"));
|
|
673
|
+
const asB = await verifier.verifyFulfillment(k.tenders.address, tokenId, submissionId, B.address, resultHash, Buffer.from(run.answer, "utf8"));
|
|
674
|
+
this.check("T3", "copy.refused", asA === true && asB === false, `verifyFulfillment with A's resultHash: as A ${asA}, as another fulfiller ${asB} (resultHash binds the account)`);
|
|
675
|
+
}
|
|
676
|
+
else
|
|
677
|
+
this.skipped("T3", "copy.refused", "replayed run: the original answer cannot be reproduced from this endpoint (see A.answer)");
|
|
678
|
+
const balanceBefore = await A.balance();
|
|
679
|
+
const settle = await A.execute("T3.A.settle", k.tenders.address, k.tenders.encodeSettle(tokenId, submissionId, Buffer.from(run.answer, "utf8")));
|
|
680
|
+
const balanceAfter = await A.balance();
|
|
681
|
+
const credit = await k.tenders.creditOf(tokenId, A.address);
|
|
682
|
+
let withdrawTx = null;
|
|
683
|
+
if (credit > 0n) {
|
|
684
|
+
const w = await A.execute("T3.A.withdraw", k.tenders.address, k.tenders.encodeWithdrawCredit(tokenId, A.address));
|
|
685
|
+
withdrawTx = w.txHash;
|
|
686
|
+
}
|
|
687
|
+
const paid = (await A.balance()) - balanceBefore;
|
|
688
|
+
k.evidence("T3", "tender", {
|
|
689
|
+
tokenId: tokenId.toString(),
|
|
690
|
+
demander: B.address,
|
|
691
|
+
fulfiller: A.address,
|
|
692
|
+
subject: C.address,
|
|
693
|
+
block: blockN,
|
|
694
|
+
answerHash,
|
|
695
|
+
resultHash,
|
|
696
|
+
submissionId: submissionId.toString(),
|
|
697
|
+
reward: reward.toString(),
|
|
698
|
+
paidToA: paid.toString(),
|
|
699
|
+
creditPath: credit > 0n ? "settlement credited; withdrawn" : "paid at settlement (the account can receive)",
|
|
700
|
+
runReceipt: receiptDoc,
|
|
701
|
+
txs: { mint: mint.txHash, fund: k.store.get("T3.B.fund")?.txHash, commit: k.store.get("T3.B.commit")?.txHash, ard: rec.txHash, submit: submit.txHash, settle: settle.txHash, withdraw: withdrawTx },
|
|
702
|
+
});
|
|
703
|
+
// checks (memo T3 "expected")
|
|
704
|
+
const sub = await k.tenders.submissionOf(tokenId, submissionId);
|
|
705
|
+
this.check("T3", "submission.accepted", sub.status === 1 && sub.machineSettled === true && getAddress(sub.fulfiller) === getAddress(A.address), `submissionOf(${tokenId}, ${submissionId}) = status ${sub.status} (1 = Accepted), machineSettled ${sub.machineSettled}, fulfiller ${sub.fulfiller}`);
|
|
706
|
+
if (!settled)
|
|
707
|
+
this.check("T3", "A.paid", paid === reward && balanceAfter >= balanceBefore, `A's account balance rose by ${paid} wei (reward ${reward})`);
|
|
708
|
+
else
|
|
709
|
+
this.skipped("T3", "A.paid", "replayed run: the tender was already settled");
|
|
710
|
+
const count = await k.tenders.submissionCount(tokenId);
|
|
711
|
+
this.check("T3", "submissions.one", count === 1n, `submissionCountOf(${tokenId}) = ${count}`);
|
|
712
|
+
const escrowAfter = await k.tenders.escrowBalanceOf(tokenId);
|
|
713
|
+
this.check("T3", "escrow.spent", escrowAfter === 0n, `escrowBalanceOf(${tokenId}) = ${escrowAfter} after settlement`);
|
|
714
|
+
// negative: a second settlement of the same submission must fail (maxCompletions reached, not pending)
|
|
715
|
+
const again = await this.wouldRevert(A, k.tenders.address, k.tenders.encodeSettle(tokenId, submissionId, Buffer.from(run.answer, "utf8")));
|
|
716
|
+
this.check("T3", "settle.twice.refused", again !== null, `second settleFulfillment reverts: ${again}`);
|
|
717
|
+
// negative: a second completion beyond maxCompletions is refused at submission
|
|
718
|
+
const second = await this.wouldRevert(B, k.tenders.address, k.tenders.encodeSubmit(tokenId, hashlock.resultHash(run.answer, B.address), "second"));
|
|
719
|
+
this.check("T3", "second.completion.refused", second !== null, `a further submission after the single completion reverts: ${second}`);
|
|
720
|
+
// the predicate-pattern proof over this completion (S_pred), see t3-zk.ts
|
|
721
|
+
const settledAt = await k.memo("T3.A.settledAt", async () => (await this.world.provider.getBlock(settle.receipt.blockNumber)).timestamp);
|
|
722
|
+
await (await import("./t3-zk.js")).predicateProof(this, A, { tokenId, submissionId, settledAt });
|
|
723
|
+
}
|
|
724
|
+
/** The kernel's pre-commitment check: liveness, safety verdict, budget, deadline. */
|
|
725
|
+
async precheck(cell, tokenId) {
|
|
726
|
+
const k = this.kernel;
|
|
727
|
+
const problems = [];
|
|
728
|
+
const state = await k.aid.stateName(cell.address);
|
|
729
|
+
if (state !== "ACTIVE")
|
|
730
|
+
problems.push(`anchor ${state}`);
|
|
731
|
+
const status = await cell.status();
|
|
732
|
+
if (status.verdict !== 1)
|
|
733
|
+
problems.push(`safety ${status.verdictName} [${status.reasonNames.join(",")}]`);
|
|
734
|
+
const gas = await k.balance(cell.operatorAddress);
|
|
735
|
+
const gasFloor = await operatorGasFloor(k, cell.roles.operator);
|
|
736
|
+
if (gas < gasFloor)
|
|
737
|
+
problems.push(`operator gas ${gas} wei below the budget floor ${gasFloor} wei`);
|
|
738
|
+
const terms = await k.tenders.terms(tokenId);
|
|
739
|
+
const now = await this.world.blockTimestamp();
|
|
740
|
+
if (terms.submitBy !== 0n && BigInt(now) > terms.submitBy)
|
|
741
|
+
problems.push("past submitBy");
|
|
742
|
+
if (terms.settleBy !== 0n && BigInt(now) > terms.settleBy)
|
|
743
|
+
problems.push("past settleBy");
|
|
744
|
+
if (await k.tenders.isCancelled(tokenId))
|
|
745
|
+
problems.push("tender cancelled");
|
|
746
|
+
const escrow = await k.tenders.escrowBalanceOf(tokenId);
|
|
747
|
+
if (escrow < terms.rewardPerCompletion)
|
|
748
|
+
problems.push(`escrow ${escrow} below reward ${terms.rewardPerCompletion}`);
|
|
749
|
+
return problems.length ? { ok: false, detail: problems.join("; ") } : { ok: true, detail: `anchor ACTIVE, safety Satisfied, operator gas ${gas} wei, escrow ${escrow} wei, deadlines open` };
|
|
750
|
+
}
|
|
751
|
+
/** eth_estimateGas as the cell's operator: the decoded revert reason, or null when the call would succeed. */
|
|
752
|
+
async wouldRevert(cell, target, data, by = "operator") {
|
|
753
|
+
return this.wouldRevertCall(by === "owner" ? cell.ownerAddress : cell.operatorAddress, cell.address, cell.iface.encodeFunctionData("execute", [target, 0n, data]));
|
|
754
|
+
}
|
|
755
|
+
/** eth_estimateGas of an arbitrary call: the decoded revert reason, or null when it would succeed. */
|
|
756
|
+
async wouldRevertCall(from, to, data, value = 0n) {
|
|
757
|
+
try {
|
|
758
|
+
await this.world.provider.estimateGas({ from, to, data, value });
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
catch (e) {
|
|
762
|
+
return this.kernel.tx.decoder.explain(e);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|