@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
package/docs/genesis.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# The genesis scenario (T0–T9)
|
|
2
|
+
|
|
3
|
+
The genesis is the run that proves the ten-ERC stack composes into a working agent: a world is set up, three cells are born, one of them acquires skills and earns a reward by doing verifiable work for another, the three organize into a swarm, the swarm earns a reward on a tender judged by a council, a service call between cells ends in a dispute whose ruling the escrow enacts, a guardian stops a cell at its execution gate, an independent measurer records what the cells realized, and an identity moves to a new body. Design memo section 4 defines ten phases, T0–T9; Phase 2a implemented T0–T5 (2a-1: T0–T3; 2a-2: T4–T5, the credential-pattern ZK path and the council), Phase 2b adds T6–T9, the predicate-pattern ZK path (`S_pred`), the measurement receipts and the doctor's fifth layer. Phase 2c ran the same scenario on a public chain (Sepolia, 2026-11-01): the section "Running on a public chain" below says what differs, which is less than it looks, and what the run showed.
|
|
4
|
+
|
|
5
|
+
Everything below runs on anvil with `foga demo --genesis T9` (fresh chain) or, against a deployed world, with `foga genesis run --world deployments/local.json --until T9`. The run is idempotent: a second invocation replays every recorded operation from the operation store and re-checks the end state without sending a transaction; an interrupted run is resumed by invoking it again (docs/kernel.md). Checks that cannot be remade on a replayed run (the state of a suspension that has since been lifted) are recorded as skipped with the reason, never as passed.
|
|
6
|
+
|
|
7
|
+
Two ZK paths need proving material. The credential path (`S_cred`) needs the upstream material (`foga vendor sync`) and the proving libraries (`npm run zk:install`); the predicate path (`S_pred`) needs FOGA's own circuit built with `npm run zk:predicate` (10–60 minutes once; docs/zk.md). `--zk auto` (the default) leaves out whichever path lacks its material, with a notice; `--zk require` insists on both (CI does); `--zk skip` leaves both out. The choices are recorded in the store on the first run and kept by later runs.
|
|
8
|
+
|
|
9
|
+
## Actors
|
|
10
|
+
|
|
11
|
+
| Signer | account | Role |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `W` | #0 | world operator: deploys the world, registers the KYA schemes and the ASF specification, issues the attested standing assertions; the treasury: funds the other roles to their floors and the cells with their capital |
|
|
14
|
+
| `G` | #1 | guardian: the intervener with suspend and resume rights in every cell's binding |
|
|
15
|
+
| `owner.A/B/C` | #2 #3 #4 | cold principals: deploy and administer the accounts, adopt the bindings |
|
|
16
|
+
| `operator.A/B/C` | #5 #6 #7 | hot signers: act through `execute` |
|
|
17
|
+
| `E` | #8 | the measurer of T8 (registers the method, asserts the receipts); in T4 and T5 also the stranger: an account with no identity and no standing |
|
|
18
|
+
| `J1`, `J2`, `J3` | #9, #10, #11 | the judges of the FOGA-Council |
|
|
19
|
+
|
|
20
|
+
The twelve keys are the first twelve accounts of one BIP-39 phrase (role i at `m/44'/60'/0'/0/i`). On anvil the phrase is anvil's well-known one, so a fresh local world always has the same addresses; on any other chain it is the phrase in `FOGA_MNEMONIC` or in the file `FOGA_MNEMONIC_FILE` names (`foga keys new` writes one, `foga keys show` lists the addresses), and `FOGA_KEY_<ROLE>` overrides one role (`FOGA_KEY_W` to make an account that already holds the chain's ether the treasury, for instance). anvil pre-funds its first ten accounts; J2 and J3 are beyond them, so W funds the judges in T0 there — the same funding step that, on a public chain, funds every role (below).
|
|
21
|
+
|
|
22
|
+
The cells are contract accounts (`FogaCellAccount`, docs/cell-account.md), not the signers: `A` is `0x663F…`, `B` is `0x057e…`, `C` is `0xbdEd…` on a fresh anvil world (deterministic: same keys, same nonces).
|
|
23
|
+
|
|
24
|
+
## T0 — world
|
|
25
|
+
|
|
26
|
+
Pre-condition: a deployed world (`foga world deploy`, 47 contracts, four ARD kinds). The genesis then registers, as `W`:
|
|
27
|
+
|
|
28
|
+
| Operation key | Call | Result fact |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `T0.kya.S_att.register` | `KYASchemeRegistry.registerScheme` — attested, IDENTITY binding, ordered levels 0–3 ("none, known, bound, audited"), descriptor as a `data:` URI committed by keccak | `T0.kya.S_att.schemeId` |
|
|
31
|
+
| `T0.kya.adapter.deploy` | deploy `Groth16KYAVerifierAdapter(Groth16Verifier of the world, FOGA's issuer-set root, 30-day epochs, grace 1)` — FOGA's own instance of the upstream credential adapter, pinned to an issuer set of one attestor key held by W (derived from W's key; `T0.kya.issuerSet` records the root and the public key) | `T0.kya.adapter` |
|
|
32
|
+
| `T0.kya.S_cred.register` | `registerScheme` — proved, verifier = FOGA's adapter, levels 0–4 | `T0.kya.S_cred.schemeId` |
|
|
33
|
+
| `T0.kya.predicate.verifier.deploy`, `T0.kya.predicate.adapter.deploy` | deploy `Groth16PredicateVerifier` (generated from FOGA's circuit `kya_predicate_count_v1`, docs/zk.md) and `Groth16PredicateKYAVerifierAdapter(verifier, source-set root, 30-day epochs, grace 1, validity 30 d)` — FOGA's own IKYAVerifier for the predicate pattern, pinned to a source set of one key held by W (the node that indexes task completions; `T0.kya.sourceSet` records the root and the public key). Both are deployed on every machine; the proving material is needed only to prove (T3) | `T0.kya.S_pred` |
|
|
34
|
+
| `T0.kya.S_pred.register` | `registerScheme` — proved, verifier = the predicate adapter, levels 0–1 ("not shown", "shown"): the rule "at least K accepted task completions about the subject inside [t0, t1], signed by a source of the set" | `T0.kya.S_pred.schemeId` |
|
|
35
|
+
| `T0.asf.spec.register` | `ASFSpecificationRegistry.registerSpecification(salt, keccak(JCS(spec)), data: URI, [])` — the FOGA-Cell-v1 specification: commitment-class actions only while the anchor is ACTIVE, interventions honored at the gate, de-risking always open, attribution only through the account | `T0.asf.spec` (specId, version 1, contentHash) |
|
|
36
|
+
| `T<n>.fund.<role>` | before every step, W tops up each role whose balance has fallen below half its floor (on anvil: 1 ETH for J1–J3 in T0; on a public chain: every role, see "Running on a public chain") | `T<n>.fund.price`, `T<n>.fund.<role>.amount` |
|
|
37
|
+
| `T0.acdf.policy.register` | `ACDFPolicyRegistry.registerPolicy` — FOGA-Council: one ROSTER_KOFN body of J1–J3, k = 2, on-chain tally, window; one appeal of a Decided result within the appeal window (an appeal round that ends without a decision preserves the first); a NoDecision is final at once; hard cap = 2 windows + appeal window; descriptorHash = keccak(JCS(descriptor)) | `T0.acdf.policy` |
|
|
38
|
+
| `T0.acdf.tenderAdapter.deploy` | deploy `ACDFTaskTenderAdapter(ACDFRegistry, TaskToken, policyId, margin)` — FOGA's instance of the upstream adapter: the judged acceptance authority of T4/T5 | `T0.acdf.tenderAdapter` |
|
|
39
|
+
| `T0.adrf.policy.register` | `ADRFPolicyRegistry.registerPolicy` — FOGA-Service-v1: (result, FORMAL) → the upstream `DeclaredVersionVerifier` over the world's `ADRFServiceEscrow`; (result, JUDGED) → the `ADRFACDFBridge` with FOGA-Council as parameters, burden NO_DECISION; one substantive effect, the escrow's refund; standing = the parties of a call; windows below (the decision window must hold the council's whole procedure, which the policy registry verifies) | `T0.adrf` |
|
|
40
|
+
| `T0.adrf.escrow.bind` | `ADRFServiceEscrow.bindTo(input)` by its admin W — all the escrow's calls, PRE_COMMIT, refund authorized, the escrow itself the executor; the binding document is a `data:` URI committed by hash | `T0.adrf.bindingId` |
|
|
41
|
+
|
|
42
|
+
Checks: the pinned version's content hash equals `keccak(JCS(spec))`; the three schemes exist; FOGA's credential adapter pins the issuer-set root and the predicate adapter the source-set root; the policy exists; the tender adapter is bound to it and does not declare `ITaskVerifier` (a judged authority, never machine settlement); the service policy exists and the escrow is bound to it. The genesis refuses to run when the world's escrow is not administered by W (the escrow must be deployed by W: on a public chain `foga world deploy` deploys it with W as deployer).
|
|
43
|
+
|
|
44
|
+
### Windows and margins
|
|
45
|
+
|
|
46
|
+
| | local chain (anvil) | other chains |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| council round window | 1 day | 180 s |
|
|
49
|
+
| appeal window | 1 hour | 60 s |
|
|
50
|
+
| workers' decision window | 1 day | 180 s |
|
|
51
|
+
| execution margin | 1 hour | 60 s |
|
|
52
|
+
| judgment window of the T4 tender | 7 days | 900 s |
|
|
53
|
+
| judgment window of the no-decision tender | 3 days | 600 s |
|
|
54
|
+
| service dispute: filing window | 7 days | 1800 s |
|
|
55
|
+
| service dispute: evidence window / decision window | 1 day / 3 days | 180 s / 600 s |
|
|
56
|
+
| service dispute: review window (one review) / enactment margin | 1 day / 1 day | 120 s / 120 s |
|
|
57
|
+
| service dispute: hard cap (decision + review + decision) | 7 days | 1320 s |
|
|
58
|
+
| escrow dispute period (how long a charge is held) | 10 days | 1800 s |
|
|
59
|
+
| AID liveness window (world parameter; T7 shortens A's to 120 s for the demonstration and restores it) | 90 days | 90 days (30 days in Sepolia's backing registry) |
|
|
60
|
+
|
|
61
|
+
The relations the registries enforce hold in both columns: the service policy's decision window holds the council's whole procedure (2 x window + appeal = 420 s), and a call's dispute period outlasts the service track's hard cap plus its enactment margin (1320 + 120 = 1440 s < 1800 s) so a ruling can still be enacted on the held charge. On anvil the kernel moves the clock past a window (`evm_increaseTime`; every skip is a journal entry and part of the evidence); elsewhere it waits (about fifty minutes in all over T5–T7).
|
|
62
|
+
|
|
63
|
+
## T1 — birth, for each of A, B, C
|
|
64
|
+
|
|
65
|
+
| # | Operation key | Who | Call |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
| 1 | `T1.X.deploy` | owner | deploy `FogaCellAccount(safety = ASFBindingRegistry, owner, operator, guardian = G, recovery = owner, requirement)`; the requirement is one clause — FOGA-Cell-v1 version 1, adopter = the account, subjectKey = keccak(`eip155:<chain>:<account>`), scopeKey = keccak(JCS({ref})), enforcement account+contract, intervener G with suspend+resume — built from the address predicted for the deployment nonce |
|
|
68
|
+
| 2 | `T1.X.fund` | W | working capital to the account: 0.05 ETH on anvil, 0.01 ETH on a public chain (what a cell funds and deposits over the run is about 0.005 ETH) |
|
|
69
|
+
| 3 | `T1.X.setOperator`, `T1.X.scope`, `T1.X.setAIDRegistry` | owner | operator scope: the key (no expiry, 0.01 ETH per day), then the allow-list in one `setOperatorAllowedBatch` — the (target, selector) pairs of the groups the cell plays (`src/genesis/operator-scope.ts`: A identity + standing + skills-provider + labour + organization-member + service-provider, B identity + demander + organization-coordinator + organization-member, C identity + organization-member + service-client; the backing AID registry mirrors the primary one's entries), recorded as the fact `T1.X.scope`; the AID registry for the commitment gate. The account is revision 2 (docs/cell-account.md): a call outside the list reverts with `OperatorNotAllowed`, an asset-moving selector with `OperatorAssetSelector`, so the scope is checked by the run itself |
|
|
70
|
+
| 4 | `T1.X.class.*` ×5, `T1.X.derisk.*` ×4 | owner | commitment-class selectors (8414 submitFulfillment, 8447 join / applyToJoin, 8454 assertStatement, 8436 castBallot) and de-risk selectors (8414 withdrawCredit / reclaimEscrow, 8447 leave, 8471 ADRFServiceEscrow.withdrawBalance) |
|
|
71
|
+
| 5 | `T1.X.adopt` | owner | `adoptBinding(input, 0)`: the account adopts FOGA-Cell-v1 v1 with G as intervener (rights suspend+resume), params and authority documents as `data:` URIs; the binding is presented for clause 0 |
|
|
72
|
+
| 6 | `T1.X.8004.register` | operator via `execute` | `IdentityRegistry.register(agentURI)` with an ERC-8004 registration file; the NFT belongs to the account |
|
|
73
|
+
| 7 | `T1.X.aid.bind`, `T1.X.aid.heartbeat` (and `T1.X.aid.backing.bind` where the world has a backing registry) | operator via `execute` | `AIDRegistry.bind(identityRegistry, agentId)` (the account owns the agent), `heartbeat()`; the same `bind` in the backing registry (below) |
|
|
74
|
+
| 8 | `T1.X.kya.attest` | W | `KYARegistry.attest({account, (chain, account)}, S_att, level 2, claimDigest, expiresAt = +365 d, evidence data: URI, claimDigest)` |
|
|
75
|
+
| 9 | `T1.X.aid.document` | operator via `execute` | `setDocumentURI(data: URI, keccak(JCS(document)))` — an `aid-document/v1` with a SELF identity facet (resolver erc8004-identity) and an ATTESTED standing facet (resolver erc8419-assertion citing the assertion) |
|
|
76
|
+
| 10 | `T1.X.ard.register` | operator via `execute` | `ARDOnchainRegistry.register(kind agent, target = account, id = 0, sha256(JCS(authored descriptor)), data: URI, urn)` — the AgentAdapter makes the anchor its own controller |
|
|
77
|
+
| 11 | `T1.X.auf.unit` | operator via `execute` | `AUFUnitRegistry.registerUnit(controller = account, keccak(JCS(definition)), data: URI, [])` — the cell's unit: agent element, treasury position, deliverable and funds ports |
|
|
78
|
+
|
|
79
|
+
| 12 (A only) | `T1.A.kya.attestWithProof` | operator via `execute` | the proved path (memo T1 ④): A derives a prover secret, hands its Poseidon commitment to W; W's attestor signs the scheme-bound credential (subject key, S_cred, level 4, claim digest, expiry, commitment) without seeing the secret; A proves membership of the credential's issuer in FOGA's set for this registry's `admissionDomain()` and the current epoch (Groth16, ~6 s), then `KYARegistry.attestWithProof(subject, S_cred, publicInputs, proof, evidence data: URI)`; the adapter is the issuer of record. The proving runs in `prover/run.cjs`, a bridge over the upstream prover (docs/kernel.md) |
|
|
80
|
+
|
|
81
|
+
Checks per cell (memo "expected"): `state(anchor) == ACTIVE`; `check({account, anchor}, S_att, 2, [W]) == true`; `status()` Satisfied; the ARD record `backed`; the unit's controller is the account; the ERC-8004 agent's owner is the account; `requirementId()` equals `keccak(abi.encode(requirement))` computed off-chain; `computeBindingId(account, input)` equals the adopted id. For A's proof: `check({account, A}, S_cred, 4, [adapter]) == true`; the nullifier is spent; the same proof again reverts (`KYA_NullifierUsed`); the proof presented for another subject reverts (`KYA_SubjectMismatch`); a proof for epoch `current - 2` reverts (`EpochOutOfWindow`). The proof and the credential are facts, so a resumed run reuses them rather than proving again.
|
|
82
|
+
|
|
83
|
+
One ordering differs from the memo's list: the binding is adopted before the ERC-8004 registration. The account executes nothing before a binding is presented (there is no privileged path), so adoption is the first act after deployment; the memo's order (register, bind, then adopt) assumed an ungated bootstrap that the account deliberately does not have.
|
|
84
|
+
|
|
85
|
+
## T2 — capability (A)
|
|
86
|
+
|
|
87
|
+
Two real ERC-8338 packages live in `scenarios/skills/`: `foga-probe` (the deterministic ten-ERC probe of one address at one block — the "known answer" source of T3) and `finch-extract-verify` (structured extraction plus consistency check over a pinned document — the first business task, judged in later phases). They are packed with the DAG-CBOR rules of the upstream `pack.py` (`src/core/pack/`, byte-exact against the upstream vectors).
|
|
88
|
+
|
|
89
|
+
| Operation key | Who | Call |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| `T2.A.skill.<name>.mint` | A via `execute` | `SkillToken.mintSkill(to = A, updateAuthority = A, mdHash, packageHash, packageURI)` |
|
|
92
|
+
| `T2.A.skill.<name>.ard` | A via `execute` | `register(kind skill, target = SkillToken, id = tokenId, …)` — the SkillAdapter makes the token owner the controller |
|
|
93
|
+
| `T2.A.skill.<name>.unit` | A via `execute` | the skill's unit (source element, calls-out and fees-in ports, controller A) |
|
|
94
|
+
| `T2.A.aid.document` | A via `execute` | the document gains an `aid:skills/erc8338/v1` facet listing the tokens |
|
|
95
|
+
|
|
96
|
+
Checks: `skillOf(tokenId)` equals the local package (mdHash, packageHash, version 1); the owner is A; the skill records are `backed` (the adapter reads the package hash); appending one byte to `SKILL.md` changes both hashes (the tampered-package case); the on-chain document digest equals the kernel's latest commitment before and after the update.
|
|
97
|
+
|
|
98
|
+
## T3 — labour (machine acceptance)
|
|
99
|
+
|
|
100
|
+
B, as demander, tenders "the `foga-probe` report of cell C at block N" and commits the answer's hash; A, as fulfiller, discovers the task through ARD, checks itself, runs the skill in isolation, submits, settles and is paid. Block N is fixed once (`T3.block`) and the answer is `JCS(report)`, where the report reads every registry at block N and includes the block hash.
|
|
101
|
+
|
|
102
|
+
| Operation key | Who | Call |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `T3.B.mint` | B via `execute` | `TaskToken.mintTask(to = B, updateAuthority = B, acceptanceAuthority = HashlockVerifier, tdHash, taskHash = sha256(task document), taskURI = data: URI, terms{ETH, 0.001 per completion, max 1, submitBy, settleBy, judgmentWindow 7 d})` |
|
|
105
|
+
| `T3.B.fund` | B via `execute` (value 0.001 ETH) | `fundTask(tokenId, 0.001 ETH)` — within the operator's daily cap |
|
|
106
|
+
| `T3.B.commit` | B via `execute` | `HashlockVerifier.commitAnswer(TaskToken, tokenId, sha256(answer))` — B is the update authority |
|
|
107
|
+
| `T3.B.ard` | B via `execute` | the task record (kind task; the TaskAdapter's controller is the token owner) |
|
|
108
|
+
| — | A (kernel) | discovery: read the record, decode the descriptor and the task document (skill, packageHash, subject, block, acceptance); pre-check: anchor ACTIVE, safety Satisfied, operator gas above the floor, escrow covers the reward, deadlines open, not cancelled; verify the local `foga-probe` directory against the token's commitment and the task's `packageHash`; run the probe in a child process with no keys; `sha256(answer)` must equal the committed hash |
|
|
109
|
+
| `T3.A.submit` | A via `execute` | `submitFulfillment(tokenId, resultHash = sha256(answer ‖ A), resultURI = run receipt as data: URI)` — commitment-class, so the AID gate applies |
|
|
110
|
+
| `T3.A.settle` | A via `execute` | `settleFulfillment(tokenId, submissionId, proof = answer)` — anyone may settle a verifier-judged tender; the vault pays A directly |
|
|
111
|
+
| `T3.A.withdraw` | A via `execute` | only if the settlement credited instead of paying (it does not when the account can receive, which it can) |
|
|
112
|
+
|
|
113
|
+
Checks: the submission is `Accepted` with `machineSettled` and fulfiller A; A's balance rose by exactly the reward; `submissionCountOf == 1`; the escrow is spent; `verifyFulfillment` with A's `resultHash` is true for A and false for any other fulfiller (the proof cannot be copied); settling with a wrong proof reverts `TaskToken: verification failed`; a second settlement and a further submission are refused (max completions reached).
|
|
114
|
+
|
|
115
|
+
The run receipt (`foga.run-receipt/1`: skill, package hash, token, input, answer hash, fulfiller, worker) is the `resultURI` and is kept as a fact.
|
|
116
|
+
|
|
117
|
+
### The predicate-pattern proof (`S_pred`, end of T3)
|
|
118
|
+
|
|
119
|
+
Right after its first accepted completion A proves a fact about its record under the predicate scheme (ERC-8419 section 5: the scheme is the decision rule, the prover evaluates it over data authenticated by a source). The memo places this proof at birth; it is made here because the records must exist first.
|
|
120
|
+
|
|
121
|
+
| Operation key | Who | Call |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| — (`T3.A.kya.S_pred.record`) | the source (W's node) | signs one task-completion record about A with its EdDSA key: `(subjectKey, taskRef = top 128 bits of keccak(chainId, TaskToken, tokenId, submissionId), status 1 = Accepted, timestamp = the settlement's block time)`. The record never goes on chain |
|
|
124
|
+
| — (`T3.A.kya.S_pred.rule`) | the relying party (the genesis, for the registry) | the rule's parameters: a challenge binding the request, the window `[now − 30 d, now]`, K = 1 |
|
|
125
|
+
| — (`T3.A.kya.S_pred.proof.k1`) | A (prover) | Groth16 proof over `kya_predicate_count_v1` (8 record slots, source-set depth 16): the source is in the committed set, each present record is signed by it and about A, records are distinct (strictly increasing `taskRef`), the count of Accepted records inside the window reaches K; outputs `result = 1` and the nullifier `Poseidon(secret, schemeId, admissionDomain, epoch, challenge)`. Verified locally against the committed verification key before it is sent |
|
|
126
|
+
| `T3.A.kya.attestWithProof` | A via `execute` | `KYARegistry.attestWithProof(subject A, S_pred, publicInputs, proof, evidence)`; the adapter maps the proof to level 1, `claimDigest = keccak(challenge, t0, t1, k, sourceSetRoot)`, `expiresAt = now + 30 d`, `anchor = sourceSetRoot` |
|
|
127
|
+
|
|
128
|
+
Checks: the prover counts 1 accepted completion and the proof verifies locally; `check({account, A}, S_pred, 1, [adapter]) == true`; the assertion's anchor is the source-set root and its claim digest commits to the rule; the nullifier is spent; the same proof again, the proof for another subject and the proof under `S_cred` revert; a proof for K = 2 (one completion) has `result = 0` and the adapter refuses it (`PredicateNotSatisfied`: a false predicate is rejected, not recorded); the same proof with the result byte set to 1 outside the circuit fails verification (`KYA_VerifierRejected`); a proof for epoch `current − 2` reverts (`EpochOutOfWindow`). Three proofs, each a few seconds to a minute depending on the machine; all are facts, so a resumed run reuses them.
|
|
129
|
+
|
|
130
|
+
When the predicate material is absent (`T0.zk.predicate = off`) these checks are recorded as skipped and the scheme stays registered with its adapter.
|
|
131
|
+
|
|
132
|
+
## T4 — organization
|
|
133
|
+
|
|
134
|
+
The swarm's objective binds to the tender it will fulfil, so the judged tender comes first (the memo lists its minting under T5).
|
|
135
|
+
|
|
136
|
+
| Operation key | Who | Call |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| `T4.D.mint`, `T4.D.fund`, `T4.D.ard` | W as demander | `mintTask(to = W, updateAuthority = W, acceptanceAuthority = ACDFTaskTenderAdapter, …, terms{0.002 ETH, max 1, settleBy +14 d, judgmentWindow})`, `fundTask`, the ARD task record (the TaskAdapter's controller is the token owner, W) |
|
|
139
|
+
| `T4.deploy.assertionLevel`, `T4.deploy.composite` | W | `AssertionLevelEligibility(KYARegistry, S_att, 2, [W])`; `CompositeEligibility([ERC8004Eligibility, IdentityStateEligibility, AssertionLevelEligibility], requireAll)` — the first two were deployed with the world, the third needs a scheme id |
|
|
140
|
+
| `T4.X.declareAgent` ×3 | each cell via `execute` | `ERC8004Eligibility.declareAgent(agentId)` — a seat is bound to an identity the account controls |
|
|
141
|
+
| `T4.deploy.decisionAdapter`, `T4.B.scope.decisionAdapter.open` | W; owner of B | `SwarmDecisionAdapter(SwarmRegistry, ACDFRegistry, decidingRole = worker, 2 of 2, window, margin)`; B's owner opens `SwarmDecisionAdapter.open` to B's operator (the adapter exists only now, so it is not in T1's list) |
|
|
142
|
+
| `T4.B.form` | B via `execute` | `form(spec, charterHash, commitmentHash, charterURI, 0, origin{task, keccak(chain, TaskToken, tokenId)}, objective{TERMINAL, acceptanceAuthority = SwarmTenderAdapter, obligation = (chain, TaskToken, tokenId)})`. Roles: `sai.role.coordinator` (1 seat) and `sai.role.worker` (2 required, 3 seats), both self-join under the composite eligibility. Authorities: AMEND = the decision adapter; REPRESENT = the coordinator role; ACTIVATE by roster; ADVANCE and SUCCEED none; everything else the initiator (B). One OPEN RECRUIT phase (shared payoff, partitioned topology, collective coordination); recruitment window 7 d, max duration 30 d, ineligible grace 1 d, settlement window 7 d, amendment exit window 1 d. The Swarm Pack (schema `swarm-pack.schema.json`) is the charter: `charterHash = SHA-256(JCS(pack))`, `objectiveHash`, `commitmentHash` over its members (byte-exact against the upstream vectors, `test/swarm.test.ts`); `specHash = keccak256(abi.encode(spec))` |
|
|
143
|
+
| `T4.B.join`, `T4.A.join`, `T4.C.join` | via `execute` (commitment-class: the AID gate applies) | `join(swarmId, role, charterHash)` — admission completes only if the composite module says so |
|
|
144
|
+
| `T4.activate` | anyone (W) | `activate` once the roster is met |
|
|
145
|
+
| `T4.deploy.swarmAccount`, `T4.B.setAccount`, `T4.B.scope.swarmAccount.execute`, `T4.B.class.swarmAccount.execute` | W; B via `execute`; owner of B | `SwarmAccount(SwarmRegistry, swarmId, recovery = B)`; `setAccount`; B's owner opens `SwarmAccount.execute` to B's operator and marks it commitment-class (representing the swarm while not ACTIVE is refused at B's gate) |
|
|
146
|
+
| `T4.B.unit` | B via `execute` | the swarm's AUF unit: controller B, elements the swarm account, `contains = [A@v1, B@v1, C@v1]` |
|
|
147
|
+
| `T4.W.revoke.C`, `T4.challenge.C.suspend`, `T4.W.reattest.C`, `T4.challenge.C.reinstate` | W; anyone (W) | the memo's failure case: W revokes C's S_att assertion; `challenge(swarm, C)` suspends C (INELIGIBLE) and the roster is no longer met; a fresh assertion and a second `challenge` reinstate C. The challenge calls carry an explicit gas limit: the registry treats a module's `false` with too little gas left as unevaluable (its anti-starvation rule), and an estimate stops at that first non-reverting path |
|
|
148
|
+
|
|
149
|
+
Checks: the tender's acceptance authority is the adapter and its escrow the reward; the composite module says yes for A, B, C and no for a stranger (E); the on-chain `specHash` equals the one computed off-chain; `rosterMet` after three joins; A, B, C ACTIVE; the swarm ACTIVE with the account bound and B as initiator; `isAuthorized(REPRESENT)` true for B and false for A; the unit contains the three member units at version 1; E's `join` as worker reverts `SAI_Ineligible`; the suspension and the reinstatement.
|
|
150
|
+
|
|
151
|
+
Deviation from the memo: the memo's "2-of-3 members" governance vote is realized as 2 of 2 workers, because the upstream decision adapter takes its roster from one deciding role; the coordinator proposes and may not vote.
|
|
152
|
+
|
|
153
|
+
## T5 — governance
|
|
154
|
+
|
|
155
|
+
| Operation key | Who | Call |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| `T5.swarm.submit` | B via `execute` → `SwarmAccount.execute` → `TaskToken.submitFulfillment(tokenId, resultHash, resultURI)` | the deliverable is the `finch-extract-verify` report over `scenarios/fixtures/sample-filing.txt` produced by the skill's deterministic path (`src/genesis/extract.ts`: line-label extraction, the field list's consistency rules, evidence quotes); `resultHash = sha256(JCS(report))`; the fulfiller of record is the swarm account, not a member |
|
|
158
|
+
| `T5.open` | anyone (W) | `ACDFTaskTenderAdapter.open(tokenId, submissionId)` files the council's case (CONSUMER_FILED, consumer = the adapter, deadline inside the judgment window minus the margin) |
|
|
159
|
+
| `T5.J1.ballot`, `T5.J2.ballot` | J1, J2 | `castBallot(issueId, round, body 0, approve)` — two approvals decide |
|
|
160
|
+
| `T5.settleRound`, `T5.finalize` | anyone (W) | `settleRound` → Provisional (a Decided result may be appealed); after the appeal window → `finalize` → Final Decided Yes |
|
|
161
|
+
| `T5.execute` | anyone (W) | `ACDFTaskTenderAdapter.execute(issueId)` → `acceptFulfillment` → the vault pays the swarm account 0.002 ETH; the enactment is recorded |
|
|
162
|
+
| `T5.gov.open` | B via `execute` | `SwarmDecisionAdapter.open(swarmId, AMEND, amend(swarmId, charterHash v2, URI v2, 0, 0, false), roster = [A, C])` — the adapter registers a policy version with the two workers as roster and files the proposal |
|
|
163
|
+
| `T5.gov.A.ballot`, `T5.gov.C.ballot`, `T5.gov.settle`, `T5.gov.execute` | A and C via `execute` (commitment-class); anyone | the workers approve; the round settles Final at once (no appeal in the adapter's policy); `execute` performs `amend` as the AMEND authority: charter version 2, commitment version unchanged (not material) |
|
|
164
|
+
| `T5.nd.mint`, `T5.nd.fund`, `T5.nd.submit`, `T5.nd.open`, `T5.nd.J1.ballot`, `T5.nd.settleRound` | W; B via `execute`; J1 | the no-decision path: a second judged tender (0.001 ETH, judgment window 3 d), the swarm account submits, the case opens, one judge approves, the window passes → `settleRound` → Final NoDecision (QUORUM_NOT_MET); the adapter enacts nothing |
|
|
165
|
+
| `T5.nd.claimUnjudged` | anyone (W) | after the judgment window, `TaskToken.claimUnjudged(tokenId, submissionId)` pays the fulfiller: silence is the kernel's own default |
|
|
166
|
+
| `T5.swarmTender.settle` | anyone (W) | `SwarmTenderAdapter.settle(swarmId, submissionId)` → the swarm concludes ACHIEVED with the report's hash |
|
|
167
|
+
|
|
168
|
+
Checks: the judges are nobody's party (not the demander, a member, the swarm account or an operator); the submission's fulfiller is the swarm account on the judged path; a worker driving the swarm account reverts (`SwarmAccount_NotRepresentative`); a ballot for the wrong round, a ballot by a non-judge and `execute` before Final revert; the result is Final Decided Yes from round 1; the submission is Accepted and the enactment Enacted; the swarm account received exactly 0.002 ETH and the members nothing; `execute` again reverts; the coordinator cannot vote on the workers' decision; the charter is version 2 with commitment version 1; the proposal is enacted; the second case ends Final NoDecision (QUORUM_NOT_MET), `execute` is inert, `claimUnjudged` pays after the window; the swarm is CONCLUDING / ACHIEVED with the report's hash.
|
|
169
|
+
|
|
170
|
+
Deviation from the memo: the settlement pays the swarm account directly (the account can receive), so no `withdrawCredit` follows.
|
|
171
|
+
|
|
172
|
+
## T6 — dispute (ERC-8471 through the world's service escrow)
|
|
173
|
+
|
|
174
|
+
A service call between cells goes through the world's `ADRFServiceEscrow` (the upstream worked example, bound in T0 to FOGA-Service-v1). The call's terms commit the provider to a skill version; the provider delivers another one; the client disputes; FOGA-Council judges through the bridge; the escrow enacts the refund within its own authority.
|
|
175
|
+
|
|
176
|
+
| Operation key | Who | Call |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `T6.C.escrow.deposit` | C via `execute` (value 0.002 ETH) | `deposit()` |
|
|
179
|
+
| `T6.C.call1.open` | C via `execute` | `openCall(provider = A, 0.0005 ETH, termsHash = keccak(JCS(terms)), committedVersion = packageHash of finch-extract-verify v1, disputePeriod)` |
|
|
180
|
+
| `T6.A.skill.v2.update` | A via `execute` | `SkillToken.updateSkill(tokenId, mdHash v2, packageHash v2)` — version 2 of the package (manifest 1.1.0 and a changelog note), `prev` = v1 |
|
|
181
|
+
| `T6.A.call1.deliver`, `T6.A.call1.settle` | A via `execute` | `deliver(callId, declaredVersion = packageHash v2)`, `settle(callId)` — the charge is held for the dispute period |
|
|
182
|
+
| `T6.C.call1.file` | C via `execute` | `ADRFDisputeRegistry.file(bindingId, claim)`: object = `objectOf(callId)`, event = the delivery, norm = the terms, clause "the service runs the committed version", respondent A, effect on sustained = refund, fallback = status quo, document hash of the eight-element case document; sub-question 0 FORMAL "the declared version differs from the committed one" (the verifier's canonical proposition, inputs `abi.encode(callId)`), sub-question 1 JUDGED "the version actually used matches the committed version" (decisive; the claim is sustained when FALSE). PRE_COMMIT: admitted at once |
|
|
183
|
+
| `T6.call1.resolveFormal` | anyone (W) | `resolveFormal(caseId, 0, abi.encode(callId))` → TRUE, COMPUTED by the `DeclaredVersionVerifier` over the escrow's `versionsOf` |
|
|
184
|
+
| `T6.call1.openJudgment` | anyone (W) | `openJudgment(caseId, 1)` → the bridge files the council's issue (CONSUMER_FILED, consumer = the bridge, deadline = the case's decision deadline) |
|
|
185
|
+
| `T6.call1.J1.ballot`, `T6.call1.J2.ballot`, `T6.call1.acdf.settleRound`, `T6.call1.acdf.finalize` | J1, J2; anyone | two judges hold the proposition FALSE (the run receipt and the token's commitments show v2 ran, the terms said v1); the round settles, the appeal window passes, the council's result is Final Decided No |
|
|
186
|
+
| `T6.call1.import` | anyone (W) | `ADRFACDFBridge.importResult(caseId, 1)` → the finding FALSE, JUDGED, with the council's provenance (registry, issue, policy, round) |
|
|
187
|
+
| `T6.call1.decide`, `T6.call1.finalize` | anyone (W) | `decide` → Provisional Sustained; the review window passes; `finalize` → Final, allowed effect = refund, closure REVIEW_LAPSED |
|
|
188
|
+
| `T6.C.call1.enact` | C via `execute` | `ADRFServiceEscrow.enact(caseId, refund)` — the escrow checks finality, effect, object and binding, refunds the held charge to the client and reports Enacted to the registry |
|
|
189
|
+
| `T6.C.call2.open`, `T6.A.call2.deliver`, `T6.A.call2.settle`, `T6.call2.release` | C, A via `execute`; anyone | the control case: a call committed to v2, delivered as v2; the verifier's answer is FALSE (no departure); after the dispute period `release` moves the charge to the provider's escrow balance |
|
|
190
|
+
| `T6.C.escrow.withdraw` | C via `execute` | C withdraws what is left (its deposit minus the one conforming call); A's earnings stay in the escrow until T7 |
|
|
191
|
+
|
|
192
|
+
Checks: the judges are no party to the call; the call holds the charge with committed v1 and declared v2; a stranger (B) has no standing to file and the respondent cannot file against itself; the case is Admitted, Binding, source = the escrow, claimant C, respondent A; the same contested point cannot be filed twice (`ADRF_Duplicate`); the formal finding is TRUE/COMPUTED; the council's issue is under FOGA-Council with the bridge as consumer; `decide` before the judged finding exists reverts; the judged finding is FALSE with the issue as provenance; `finalize` inside the review window reverts; the ruling is Final Sustained with the refund allowed; the client's escrow balance rose by the charge, the settlement is Refunded, the registry records Enacted by the escrow; a second enactment reverts (`Escrow_NotEnactable`); the provider's escrow balance is zero for the disputed call and equals the price for the conforming one; the verifier finds no departure on call 2.
|
|
193
|
+
|
|
194
|
+
## T7 — safety (the stop is enforced where the cell acts)
|
|
195
|
+
|
|
196
|
+
| Operation key | Who | Call |
|
|
197
|
+
|---|---|---|
|
|
198
|
+
| `T7.B.mint`, `T7.B.fund`, `T7.B.commit` | B via `execute` | a hashlock tender over the deterministic finch-extract-verify report, two completions of 0.001 ETH: a pending opportunity for A |
|
|
199
|
+
| `T7.G.suspend` | G | `ASFBindingRegistry.suspend(bindingId of A, NO_NEW_RISK ∣ DERISK_ONLY, responseRef, reasonRef)` — the intervener's suspension is also recorded as its standing stop against A for the subject and the specification |
|
|
200
|
+
| — | kernel | reads `status()` (NotSatisfied, Suspended) and records its decision: no new commitment-class calls, de-risk actions continue, wait for resume (`T7.kernel.decision`) |
|
|
201
|
+
| — (`T7.bypass`) | operator.A, around the kernel | a raw transaction straight to `A.execute(TaskToken, submitFulfillment(...))` with an explicit gas limit: mined with status 0 (the gate's `BindingNotSatisfied`); the hash, gas and decoded reason are evidence |
|
|
202
|
+
| `T7.A.derisk.withdraw` | A via `execute` | `ADRFServiceEscrow.withdrawBalance(earned)` — a de-risk action on the allow-list passes the same gate under the suspension: A brings its T6 earnings home |
|
|
203
|
+
| `T7.operator.submit`, `T7.operator.settle` | operator.A directly (not through the account) | `submitFulfillment(tokenId, sha256(answer ‖ operator), …)` and its settlement: accepted by the chain, but the fulfiller is the operator's own key and the payout went there; the kernel records the submission as not attributable to A (`T7.unattributed`), and T8 does not count it |
|
|
204
|
+
| `T7.A.adopt2` | owner | `adoptBinding(input₂, 0)`: a fresh binding of the same specification presented while the first is suspended |
|
|
205
|
+
| `T7.G.resume` | G | `resume(bindingId, reasonRef)` — lifts the standing stop with it |
|
|
206
|
+
| `T7.A.submit`, `T7.A.settle` | A via `execute` | A takes the second completion through its account and is paid |
|
|
207
|
+
| `T7.A.aid.window` | A via `execute` | `AIDRegistry.setLivenessWindow(120)` — the anchor shortens its own window (its parameter, bounded by the registry's maximum) |
|
|
208
|
+
| — | kernel | lets the window pass without a heartbeat (moved on anvil, waited for elsewhere): A is STALE |
|
|
209
|
+
| `T7.A.aid.heartbeat`, `T7.B.aid.heartbeat`, `T7.C.aid.heartbeat` (and `T7.X.aid.backing.heartbeat` in a backing registry) | each cell via `execute` | A back to ACTIVE; B and C renew |
|
|
210
|
+
| `T7.A.aid.window.reset` | A via `execute` | `setLivenessWindow(0)`: the registry's default again (90 days) |
|
|
211
|
+
|
|
212
|
+
Checks: a non-guardian cannot suspend (`NotAuthorized`); `status()` is NotSatisfied [Suspended] during the stop; the operator's bypass transaction failed on chain with the gate's reason; a call that is neither de-risk nor allow-listed (an escrow deposit) reverts under the suspension; the de-risk withdrawal succeeded and A's account rose by the earnings; the operator's own submission is not attributable (fulfiller ≠ the account) and its payout left the cell's boundary; with the fresh binding presented `status()` is NotSatisfied [StoppedByIntervener] and the commitment-class call still reverts (the stop follows the adopter and the subject, not the binding); the adopter cannot lift the intervener's suspension; after `resume` the status is Satisfied, A's submission is Accepted with the account as fulfiller and A is paid; with its window shortened to 120 s and the window passed, `state(A) = STALE` and the commitment-class call reverts at the gate's AID clause (`AnchorNotActive`); after the heartbeats the three anchors are ACTIVE (in the backing registry too, where there is one) and the same call passes the gate; A's window is the registry's default again; after the heartbeats all three are ACTIVE and the same call passes the gate (it then fails only inside the tender, both completions being taken). On a replayed run the checks that depend on the suspended or stale state are recorded as skipped.
|
|
213
|
+
|
|
214
|
+
## T8 — value, as measured (ERC-8454 observation receipts)
|
|
215
|
+
|
|
216
|
+
| Operation key | Who | Call |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| `T8.E.method.register` | E | `AUFAssertionRegistry.registerDescriptor(keccak(JCS(method)), kind "auf:method-descriptor", data: URI)` — `foga.realized-net-cash-flow/1`: inflows that settled on chain to accounts inside the unit's boundary, minus the gas the unit's own operations paid; unobservable items are listed as missing, never written as zero; no field states a value |
|
|
219
|
+
| — (`T8.receipt.A`) | the measurer (kernel) | the receipt of cell A's unit over T3–T7: counted — the T3 tender reward, the T6 conforming call (released and withdrawn), the T7 tender reward; shown and excluded — the refunded call (nothing reached the provider) and the operator key's own payout (not attributable); costs — the gas A's keys paid for the account's confirmed operations in the window, from the chain's receipts; missing — model fees, the operator's gas for the refused bypass |
|
|
220
|
+
| — (`T8.receipt.swarm`) | the measurer | the receipt of the swarm unit over T4–T5: the two tender payouts to the swarm account; members' accounts and the gas their keys paid are outside the boundary (missing) |
|
|
221
|
+
| `T8.E.assert.A`, `T8.E.assert.swarm` | E | `assertStatement({kind Observation, unitId, version, profileId = method hash, contentHash = keccak(JCS(receipt)), contentURI = data: URI, payload = ObservationCore(snapshotHash, windowStart, windowEnd)})` — E is not the units' controller (`byController = false`) |
|
|
222
|
+
| — | the resolver (`src/core/measure.ts`) | recomputes each receipt from the chain (settlement state and recipient of every inflow reference, the registered method, the unit version, the assertion's fields) and checks the boundary, the totals, the absence of value fields and double counting across the set |
|
|
223
|
+
|
|
224
|
+
Checks: the method is registered by E; the assertions are Observations by E with the receipts' hashes; both receipts pass the resolver; a receipt that counts the swarm's tender for A's unit fails the boundary rule and the once rule; a receipt citing an unregistered method is refused; neither receipt carries a value field.
|
|
225
|
+
|
|
226
|
+
Deviation from the memo: the measured unit for A is the cell's unit (every inflow reaches the account), not a skill unit; the chain pays accounts, not skills, so per-skill revenue would need an allocation rule the receipt does not invent.
|
|
227
|
+
|
|
228
|
+
## T9 — succession
|
|
229
|
+
|
|
230
|
+
| Operation key | Who | Call |
|
|
231
|
+
|---|---|---|
|
|
232
|
+
| `T9.Aprime.deploy`, `T9.Aprime.fund`, `T9.Aprime.setOperator`, `T9.Aprime.scope`, `T9.Aprime.setAIDRegistry`, `T9.Aprime.class.8414.submitFulfillment` | owner.A (the same owner and operator keys as A); W funds on a local chain | the new body, configured as a cell; its allow-list is the successor's (identity, skills-provider, labour, service-provider) |
|
|
233
|
+
| `T9.Aprime.adopt` | owner | `adoptBinding` of A-prime's own binding (the old one is A's); nothing executes before it |
|
|
234
|
+
| `T9.A.8004.transfer` | A via `execute`, signed by A's owner | `IdentityRegistry.transferFrom(A, A-prime, agentId)` — an asset leaves the cell, which revision 2 of the account refuses to the operator; A's anchor turns STALE: the binding predicate no longer holds |
|
|
235
|
+
| `T9.Aprime.aid.bind` (and `T9.Aprime.aid.backing.bind`) | A-prime via `execute` | `AIDRegistry.bind(registry, agentId)` — the takeover: the stale binding does not hold the agent hostage; A's binding record is cleared; A-prime is ACTIVE (the same in the backing registry) |
|
|
236
|
+
| `T9.A.aid.retire` (and `T9.A.aid.backing.retire`) | A via `execute` | `retire(successor = A-prime)` — only the anchor itself; `retire` has no delegated form |
|
|
237
|
+
| `T9.Aprime.ard.register`, `T9.Aprime.aid.document` | A-prime via `execute` | a new ARD agent record anchored at (chain, A-prime, 0) and A-prime's own AID document (identity facet only: A's standing facet is not carried over) |
|
|
238
|
+
| `T9.A.skill.transfer` | A via `execute`, signed by A's owner | `SkillToken.safeTransferFrom(A, A-prime, finch-extract-verify)` — the owner's act, like the identity; the ARD skill record's controller follows the token; the record is no longer adopted (committed by a former controller) |
|
|
239
|
+
| `T9.Aprime.skill.adopt` | A-prime via `execute` | `ARDOnchainRegistry.updateDescriptor(recordId, same hash, same URI, same urn)` — recommitting the same descriptor is the adoption |
|
|
240
|
+
| `T9.Aprime.auf.unit` | A-prime via `execute` | A-prime's own unit (A's stays A's) |
|
|
241
|
+
|
|
242
|
+
Checks: A-prime cannot execute before presenting a binding (`BindingsNotPresented`) and cannot bind an agent it does not hold (`NotAgentOfAnchor`); A is ACTIVE before and STALE after the transfer; after the takeover the agent's anchor is A-prime (ACTIVE) and A's binding record is cleared; `retire(successor)` acts on `msg.sender` only; A is RETIRED with `successorOf(A) = A-prime` and cannot heartbeat again; A-prime's record is a new, backed record; its document is its own; the skill token and the record's controller moved, the record was unadopted and is adopted again by recommitment; A-prime has no `S_att` standing while A's assertion still resolves for A; the swarm seat stays with A; A's unit stays A's; a commitment-class call by the retired A reverts at the gate (`AnchorNotActive`); the two histories share one pointer and nothing else.
|
|
243
|
+
|
|
244
|
+
9d, key loss, is documented rather than demonstrated: under the guarded profile (owner, operator, guardian, recovery) the owner rotates the operator and the anchor stays; a lost anchor key under a lite profile (the anchor is a plain EOA) cannot `retire`, so the old address stays STALE forever and the new identity starts from nothing — no RETIRED state can be promised there.
|
|
245
|
+
|
|
246
|
+
## Loop C — crashes and resumption
|
|
247
|
+
|
|
248
|
+
| Case | How to reproduce | Expected, and what CI asserts |
|
|
249
|
+
|---|---|---|
|
|
250
|
+
| killed after submit, before settle | `foga genesis run … --until T9 --crash-after T3.A.submit` (exit 75), then `foga genesis run … --until T9` | the second run recovers the receipt of `T3.A.submit` by hash and does not re-send; it settles and carries on to T9; `submissionCountOf == 1`; evidence `recovered: 1`. The same holds at any key, for instance `--crash-after T5.swarm.submit` or `--crash-after T6.C.call1.file` |
|
|
251
|
+
| intent recorded, never broadcast | `--crash-before T3.A.settle`, then run again | the hash is unknown to the node and the nonce is free: re-sent with the same nonce (`resent: 1`) |
|
|
252
|
+
| the same request twice | run a completed scenario again | every operation replayed from the store, nothing sent (`sent: 0`), all end-state checks pass |
|
|
253
|
+
| the chain was reset under the store | restart anvil, redeploy, run with the old store | refused: block 0 differs from the one the store recorded |
|
|
254
|
+
| RPC timeout after broadcast | (the recovery path above covers it: hash and nonce are consulted before any retry) | — |
|
|
255
|
+
|
|
256
|
+
`foga genesis status --world <world>` lists the store's rows with status, signer, nonce and hash.
|
|
257
|
+
|
|
258
|
+
## Operation count and evidence
|
|
259
|
+
|
|
260
|
+
A complete T0–T9 run on anvil is 193 operations (13 in T0, 67 in T1, 7 in T2, 7 in T3, 24 in T4, 20 in T5, 21 in T6, 16 in T7, 3 in T8, 15 in T9; scenario version 5 — the six operator-scope operations were added in 0.6.0, a 0.5.0 run had 187) and 166 checks, about 100 s including five Groth16 proofs (two credential, three predicate; the predicate proofs take 10-15 s each on a small machine). On a world with a backing registry the run adds the bind, heartbeat, takeover and retirement there (201 operations, 171 checks in the anvil rehearsal of that path); the public-chain funding adds one transfer per role topped up (203 operations on Sepolia with 0.5.0, where nine roles were funded in T0 and the predicate path was off). A store made by an earlier scenario version is not resumed into the new one: the version is recorded in the store, and a mismatch is refused with both versions named. The evidence file `deployments/<world>.genesis.json` holds the checks, the facts, per-step evidence (addresses, ids, hashes, transaction hashes, run receipts, case documents, measurement receipts and their resolver reports, results of the deliberations), the operation summary of the store and the run's journal (time skips and waits included); `deployments/local.genesis.example.json` is a committed local run, `deployments/local-attach.genesis.example.json` the rehearsal, `deployments/sepolia.genesis.example.json` the Sepolia run (below). `foga doctor --genesis <evidence.json>` turns the checks into the doctor's fifth layer, per standard (docs/doctor.md).
|
|
261
|
+
|
|
262
|
+
## Running on a public chain
|
|
263
|
+
|
|
264
|
+
The scenario is the same; the chain is not. Four things differ, and each is a branch the anvil run also takes, so the public-chain path is rehearsed in CI on anvil (an attach-mode world built by `scripts/attach-rehearsal.mjs`) before it is run for real.
|
|
265
|
+
|
|
266
|
+
**The world is attached and completed.** `worlds/sepolia.json` is an attach-mode config: every address comes from the upstream deployment records (docs/version-lock.md). Plan steps the config does not list are what the upstream records do not provide at the locked revision — on Sepolia the whole ERC-8471 stack (deployed upstream at revision 3, which cannot be bound to a revision-4 escrow: the binding module admits only sources declaring its own `IADRFObligationSource` id) and the two ERC-8447 eligibility modules never deployed upstream. `foga world deploy --world worlds/sepolia.json` attaches what is listed and deploys the rest from the bundle, with W as deployer (111 M gas for the 15 contracts on Sepolia; before the first one is sent the deployer's balance is checked against a budget of 11 M gas per contract at the chain's quote, `DEPLOY_GAS_PER_STEP` in `src/world/deploy.ts`), so the escrow the genesis binds in T0 is administered by W. The resulting `deployments/sepolia.json` is an ordinary world file (`mode: attached`; the steps say which contracts were deployed here), and the doctor runs on it as on any other.
|
|
267
|
+
|
|
268
|
+
**Twelve keys, one treasury.** `foga keys new` writes a BIP-39 phrase to a file outside the repository (`~/.foga/genesis.mnemonic`, mode 0600) and prints the twelve role addresses; `FOGA_MNEMONIC_FILE` (or `FOGA_MNEMONIC`) hands it to every command, and `FOGA_KEY_<ROLE>` overrides a role. Only W needs to be funded by hand: before every step the kernel tops up each role whose balance has fallen below half its floor, and gives each cell its capital at birth (`T<n>.fund.<role>`, `T1.X.fund`, `T9.Aprime.fund`; 0.005 ETH per cell and 0.001 for the successor on a public chain — B funds 0.003 ETH of rewards, C deposits 0.002). A floor is a gas budget — what the role spent in the Sepolia run, with a margin of about one half (`src/genesis/funding.ts`, `ROLE_GAS_BUDGET`; the table below) — priced at the fee the chain quotes when the step starts, never below a floor of its own (`FOGA_FEE_FLOOR_GWEI`, 0.1 gwei by default: a quiet testnet quotes fractions of a wei), so a rising base fee raises the top-up rather than starving a role, and the amount of every transfer is fixed in the store the first time so a resumed run rebuilds it. The cells' pre-commitment check compares the operator's gas with a quarter of the same budget. `foga genesis preflight --world deployments/sepolia.json` prints the plan without sending anything: every role's balance against its floor, what W must hold (the top-ups, the cells' capital and W's own budget of 200 M gas for the genesis), the escrow's admin, the backing registry, the proving material and the waits.
|
|
269
|
+
|
|
270
|
+
What the run costs is a matter of the chain's gas schedule, not of the scenario: Sepolia runs the repriced schedule of its Glamsterdam fork (block-level access lists in its headers; a plain transfer costs 204,600 gas there, code deposit about 1,500 gas per byte), the schedule the public chains are moving to, and the 203 operations of the genesis, which take 108 M gas on anvil, took 475 M gas on Sepolia — deployments 6.5–7 times anvil's figure, calls 2–5 times, 4.4 times in all — plus 111 M gas for the 15 contracts of the world deployment: 586 M gas for the whole thing. The budgets are those figures with their margin, 721 M gas in all (W 200 M for the genesis, 521 M for the other eleven roles), so the preflight asks W for 0.72 ETH per gwei of the quoted fee plus 0.019 ETH of capital and tender rewards: 0.09 ETH at the 0.1 gwei floor, 0.74 ETH at 1 gwei, 2.2 ETH at 3 gwei, 7.2 ETH at 10 gwei; the world deployment asks for a further 0.165 ETH per gwei (0.0165 at the floor). What the roles do not spend stays in their accounts (the phrase holds every key): the Sepolia run itself was priced by the chain at 22 wei per gas (base fee 15, priority 7), so its 586 M gas cost 0.000013 ETH, and what left W (0.031 ETH of a faucet's 0.05) was the roles' top-ups and the cells' capital, which the phrase still holds. When the chain is that quiet, `FOGA_FEE_FLOOR_GWEI=0.05` halves what the preflight asks for.
|
|
271
|
+
|
|
272
|
+
| Role | Gas spent on Sepolia (M) | Largest step | Budget (M gas) |
|
|
273
|
+
|---|---|---|---|
|
|
274
|
+
| W | 131.3 in the genesis (+ 111.2 in the world deployment) | T0 54.7 (T4 50.3) | 200 (+ 11 per contract deployed) |
|
|
275
|
+
| G | 0.8 | T7 | 1.25 |
|
|
276
|
+
| owner.A | 44.7 | T1 21.8 (T9 20.1) | 67 |
|
|
277
|
+
| owner.B, owner.C | 21.9, 21.8 | T1 21.8 | 33 each |
|
|
278
|
+
| operator.A | 85.7 | T2 33.9 (T1 25.4, T9 19.4) | 130 |
|
|
279
|
+
| operator.B | 103.5 | T4 29.1 (T1 23.2, T3 22.6, T5 21.2) | 156 |
|
|
280
|
+
| operator.C | 32.2 | T1 23.2 | 49 |
|
|
281
|
+
| E | 31.8 | T8 31.8 | 48 |
|
|
282
|
+
| J1, J2, J3 | 0.8, 0.4, 0 | T5 | 1.25 each |
|
|
283
|
+
|
|
284
|
+
**Two liveness registries.** On Sepolia the ERC-8444 agent kind is anchored to ARD's own fixture `AIDRegistry` (`8434/AIDRegistry(ard-fixture)`, 30-day default window), not to aid-standard's reference instance the cells' accounts gate on (seam 1 of docs/version-lock.md). A world that names a backing registry under that id makes the cells keep their liveness in both: `bind` at birth (T1), heartbeats in T7, the takeover and the retirement in T9, each a `backing.*` operation mirroring the primary one; the ARD `backed` checks read the backing registry, and the evidence records both states. Documents and facets stay in the reference registry; the backing registry carries liveness only.
|
|
285
|
+
|
|
286
|
+
**Real time.** The public timing profile (table above) is waited for: about fifty minutes over T5–T7, plus one block per operation. T7 makes A STALE by shortening A's own liveness window to 120 s rather than by moving the clock 90 days, which is also what the anvil run does now. Receipts are awaited for 120 s per operation; a transaction still pending after that is replaced under the same nonce with fees raised by a third (up to three times), and a resumed run recognizes whichever of an operation's transactions was mined (docs/kernel.md). The probe of T3 reads state at a fixed block; a run resumed or replayed later than the endpoint's state window (about 128 blocks on a public cluster of non-archive nodes) cannot reproduce the answer and records `A.answer` and `copy.refused` as skipped — the settlement on chain is the proof of the original answer.
|
|
287
|
+
|
|
288
|
+
The sequence on a machine that reaches a Sepolia RPC (the repository's EC2, for instance):
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
export SEPOLIA_RPC_URL=https://... # the config's rpc is ${SEPOLIA_RPC_URL}
|
|
292
|
+
npm run foga -- keys new # once; prints W's address: fund it (0.11 ETH at the 0.1 gwei floor for the deployment and the genesis; the two commands below say what they need)
|
|
293
|
+
export FOGA_MNEMONIC_FILE=~/.foga/genesis.mnemonic
|
|
294
|
+
npm run foga -- world deploy --world worlds/sepolia.json --rpc $SEPOLIA_RPC_URL # attaches 35, deploys 15 (W pays; refused before the first one if W holds less than the budget); add --resume to continue an interrupted deployment
|
|
295
|
+
npm run foga -- genesis preflight --world deployments/sepolia.json --rpc $SEPOLIA_RPC_URL
|
|
296
|
+
npm run foga -- genesis run --world deployments/sepolia.json --rpc $SEPOLIA_RPC_URL --until T9 # 1.5-2.5 h; re-run to resume
|
|
297
|
+
npm run foga -- doctor --world deployments/sepolia.json --rpc $SEPOLIA_RPC_URL --genesis deployments/sepolia.genesis.json
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`--zk auto` records which proving paths the machine has; `--zk require` insists on both (the predicate material is built with `npm run zk:predicate`, docs/zk.md). The store `deployments/sepolia.genesis.sqlite` is the run's memory: an interrupted run is resumed by the same command; nothing is sent twice.
|
|
301
|
+
|
|
302
|
+
### The Sepolia run (2026-11-01)
|
|
303
|
+
|
|
304
|
+
World `sepolia-11155111-20261101T043153Z` (`deployments/sepolia.example.json`): 35 upstream contracts attached, 15 deployed by W (`0x3D177ecaa56402335fC7C6449d35bC1Fa68Bc1b8`) in blocks 12038625–12038640, the ERC-8004 registry at its official address. The genesis ran from the project's EC2 against a public RPC (publicnode), T0 at 04:35 UTC, the 203rd operation confirmed in block 12040082; the operations span blocks 12038641–12040082. Cells: A `0xA0d1B8e9370EA798f517F8DD8562ca6e77c0BcCc`, B `0x9303a594f6224320016a30f9eB01F79b7426d40F`, C `0x235EDC4D29146686B2CB3e750c5dB5224720D76e`, A-prime `0x09F35299C34694929f0f35DDD7AE894E8F122D75`; ERC-8004 agents 11255–11257; the swarm's account `0xe86650Fca5C117f7f1a8361618Db7f48BB23EA70`; the service escrow `0xCDc9Eb5Cb1D6b195Dbf29353f368C52e7ba76cf0`. Every address, id, hash and transaction is in the evidence file, every contract's verdict in the doctor report (`deployments/sepolia.doctor.example.json`: Pass 234, Fail 1 — the ARD fixture registry's known older source —, Unknown 2 — the two interface ids upstream publishes none for —, all eleven standards Pass on layer 5; docs/version-lock.md).
|
|
305
|
+
|
|
306
|
+
The run was not uneventful, and the evidence shows it: its journal (1,532 entries) is the store's whole history, resumes included. The timing profile cost about fifty minutes of waiting as planned (the dispute period alone 1,801 s). Twice a read landed on a node of the RPC cluster that had not yet imported the block of the previous receipt — the swarm's roster read as not met one second after the third join confirmed (`T4.activate`, `SAI_RosterNotMet`), and the delivery of a call opened one block earlier was refused by the estimate (`T6.A.call2.deliver`, `Escrow_UnknownCall`); both are `estimate-failed` entries, the run was resumed, and the kernel's reads have been pinned to the blocks it has written since (docs/kernel.md; the 43 `pinned-read` entries record the nodes' refusals in their own words). Once the process hung after a transaction was mined (`T9.Aprime.aid.backing.bind`, block 12039588); the resumed run recovered the receipt by hash (`recovered-receipt`) and finished. The predicate-pattern proof was off (`T0.zk.predicate = off`: the proving material is not built on that machine), so the eleven `S_pred` checks of T3 are one skipped check there.
|
|
307
|
+
|
|
308
|
+
The committed evidence (`deployments/sepolia.genesis.example.json`) is the final replay of the completed run, made with 0.5.0-alpha.9 at block 12040269: 203 operations replayed from the store, nothing sent, 161 checks of which 114 were remade against the chain as it stood and passed, and 47 recorded as skipped because the state they are about had been changed later by the run itself (a suspension since lifted, a tender since settled, an identity since transferred — the reason is in each check). The same checks had passed live when their steps ran — or, for the two that a lagging node failed (`T4 swarm.rosterMet`, `T6 refund.twice.refused`), on the resume that followed; what the replay adds is that the end state still holds, which the doctor's layer 5 then attributes per standard (every effective check Pass). The world was read against an independent node while the replays were being debugged: the chain was right, the first replays were reading it at the wrong block (0.5.0-alpha.9).
|
|
309
|
+
|
|
310
|
+
## Not in this phase
|
|
311
|
+
|
|
312
|
+
- A production trusted setup for the predicate circuit: the committed keys come from a development ceremony with a public beacon (docs/zk.md).
|
package/docs/kernel.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# The cell kernel (foga-kernel)
|
|
2
|
+
|
|
3
|
+
`src/kernel/` is the deterministic part of a node: it holds the keys, decides what to send, sends each thing once, and keeps the record a reviewer or a restarted process needs. There is no model inside it. `src/core/` (foga-core) underneath is one module per ERC — views, calldata builders, event decoders, canonicalization and the DAG-CBOR skill packager — with no transaction management of its own.
|
|
4
|
+
|
|
5
|
+
## Pieces
|
|
6
|
+
|
|
7
|
+
| File | What it is |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `kernel/store.ts` | `OperationStore`: the operation table (each row bound to the request that claimed it), the job table, the decision records of the node (docs/node.md), scenario facts and an append-only journal in one SQLite file (`node:sqlite`, WAL, `synchronous=FULL`). `:memory:` for tests. |
|
|
10
|
+
| `kernel/tx.ts` | `TxManager`: exactly-once sending by idempotency key, one operation at a time per signer, receipt recovery, re-send and fee-replacement rules with a persisted limit and a fee cap, fault injection. |
|
|
11
|
+
| `kernel/cell.ts` | `Cell`: one FogaCellAccount driven through its owner and operator keys (`deploy`, `execute`, `admin`, `adoptBinding`, `status`). |
|
|
12
|
+
| `kernel/kernel.ts` | `Kernel`: a world, a store, the signers, the ten modules opened on that world, `call` / `deploy` / `transfer`, facts, `memo`, evidence. |
|
|
13
|
+
| `kernel/errors.ts` | `RevertDecoder`: custom errors across every world ABI, `Error(string)`, `Panic`, and `CallFailed` unwrapping. |
|
|
14
|
+
|
|
15
|
+
## Exactly once
|
|
16
|
+
|
|
17
|
+
Every chain write has a key chosen by the caller (`T3.A.submit`). `TxManager.send` runs this sequence for a new key:
|
|
18
|
+
|
|
19
|
+
0. take the signer's turn — a signer's operations run one at a time, in the order they were asked for, whoever asks concurrently (a resumed genesis step and a public caller, two public callers): the nonce is taken, the transaction sent and its receipt awaited before the next operation of that signer begins. Different signers run side by side. A failure releases the turn; a pending transaction holds it, which is what the chain would do with the nonce anyway;
|
|
20
|
+
1. fix the nonce (`getTransactionCount(signer, "pending")`);
|
|
21
|
+
2. build the request — the builder receives the nonce, so a deployment can predict its own address;
|
|
22
|
+
3. estimate gas on the pending state and add a margin (25 % + 20 000): the inclusion state differs from the estimate's at least by the block timestamp, and an `SSTORE` that was a no-op during the estimate (a heartbeat written in the same second) costs 2 900 more when the second ticks;
|
|
23
|
+
4. sign, compute the hash, **write the row as `sent` with hash, nonce and the value the transaction carries**;
|
|
24
|
+
5. broadcast;
|
|
25
|
+
6. wait for the receipt (120 s by default); write the row as `confirmed` with block, gas, log count and the fee paid, or `failed` with the reason (and the fee paid, when the transaction was mined and reverted).
|
|
26
|
+
|
|
27
|
+
Step 4 before step 5 is what makes a crash recoverable: whatever happens afterwards, the store knows which transaction may be out there.
|
|
28
|
+
|
|
29
|
+
When the wait runs out, the chain is asked what became of the transaction. Still pending (a public chain whose base fee rose after the quote): it is **replaced** — the same payload under the same nonce, signed again with fees a third above the pending ones, recorded as the operation's new hash (`replaced`, naming the hash it replaces) and waited for in turn. The number of replacements is counted on the row, so the limit (`maxReplacements`, three by default) is for the operation's life: a run that stops after the third and a run that resumes an hour later do not start the count again — the resumed run finds the transaction pending, records `replace-limit` and waits for it as it is. A fee cap (`maxFeePerGasCap`, per manager or per request) bounds both the first send — a quote above the cap is clamped to it (`fee-capped`) — and every replacement: one that would pass the cap is not made (`replace-capped`), the pending transaction is waited for, and the row stays `sent` for the next run. Gone from the pool with the nonce still free: broadcast again as it was (`resend`). Gone with the nonce consumed by something else: `OperationAmbiguous`. Every hash an operation's current attempt ever had is in the journal, and the receipt of any of them completes it — the nonce makes them mutually exclusive, so exactly one can be mined. On a local chain a timeout almost always means the node is starved by another process (a proving-key build), not fees; the replacement is harmless there and the message says what to look for.
|
|
30
|
+
|
|
31
|
+
A key is **bound to its request**. `prepare(key, kind, signer, binding)` claims the key together with the canonical hash of the request that produced it, the capability, the caller and the cell, and the value and fee it reserves; the claim and its reservation are one SQLite transaction. The same key with the same request hash is the same operation and is answered from the store; the same key with another request hash is refused (`OperationConflict`, naming both hashes) before anything touches the chain — a key is never a way to get someone else's result, nor to send a second transaction under a first one's name. A claim without a hash (the genesis driver) binds nothing and takes the first binding it is later asked with. Keys name the business fact they settle, not a permanent slot: `tender.submit:<tokenId>` is the submission of that tender, and a second submission of the same tender is a conflict by design.
|
|
32
|
+
|
|
33
|
+
A **failed** operation is the operation's answer. A key whose estimate reverted, or whose transaction was mined and reverted, answers with that failure again on every later `send` (`replayed-failure`): nothing is sent anew because a key was asked for twice. Only an explicit retry (`retryFailed` on the request or on the manager) starts a new attempt — the row goes back to `prepared` (`retry`, keeping the failure in the journal and the fee it cost on the row), and the new attempt takes a new nonce; the hashes of the failed attempt are not waited for again. The genesis driver retries, because its failures are fixed between runs; a public caller does not, because its failure is a verdict it must read.
|
|
34
|
+
|
|
35
|
+
`budgetUse(signer, since)` sums what a signer's settled operations spent since a time — the value moved by confirmed ones and the fees paid by confirmed and reverted ones alike — and what its open operations (`prepared`, `sent`) still reserve (value plus fee cap). That pair is what a cell's budget is checked against before a new claim (Phase 3). Fees are cumulative over an operation's attempts.
|
|
36
|
+
|
|
37
|
+
### What a resumed run does with a row
|
|
38
|
+
|
|
39
|
+
| Row status | The chain says | Decision |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `confirmed` | receipt for the hash exists | answered from the store; the receipt is re-read for its logs (`replayed`) |
|
|
42
|
+
| `confirmed` | no receipt for the hash | the store and the chain have diverged: refuse (`OperationAmbiguous`) |
|
|
43
|
+
| `sent` | a receipt exists for any hash the operation had | finish from the receipt, nothing re-sent (`recovered-receipt`) |
|
|
44
|
+
| `sent` | the transaction is known and pending | replace it with higher fees under the same nonce and wait for whichever is mined (`recovered-pending`, `replaced`); at the replacement limit or the fee cap, wait for it as it is (`replace-limit`, `replace-capped`) |
|
|
45
|
+
| `sent` | unknown hash, the signer's nonce still equals the row's | the transaction never reached the chain: re-send with the same nonce (`resend`) |
|
|
46
|
+
| `sent` | unknown hash, the signer's nonce has moved past the row's | something else consumed the nonce; the outcome cannot be decided by code (`OperationAmbiguous`): never retried automatically |
|
|
47
|
+
| `failed` | — | answered with the recorded failure (`replayed-failure`); a new attempt only with `retryFailed` (`retry`) |
|
|
48
|
+
| `prepared` | — | claimed but never signed (a crash between the claim and the signature): sent as a new operation under its binding |
|
|
49
|
+
| none | — | a new send |
|
|
50
|
+
|
|
51
|
+
An RPC failure while waiting (a node that cannot answer a receipt lookup) is not the operation's failure: the error propagates, the row stays `sent`, and the next run resolves it by the table above.
|
|
52
|
+
|
|
53
|
+
A store belongs to one chain instance and one world. It records the hash of block 0, the chain id and the world id the first time it opens (an older store that recorded only the block-0 hash gains the other two on its next open); a later open against a chain with a different block 0 (a restarted anvil), another chain id or another world file is refused with both identities named rather than replaying a history into a world where it never happened.
|
|
54
|
+
|
|
55
|
+
### Jobs
|
|
56
|
+
|
|
57
|
+
Operations are chain writes. A **job** (`jobs` table: `createJob`, `updateJob`, `getJob`, `jobs(caller)`) is the unit of business work a caller asks for — a tender taken, a skill run, a document published — with its input and the hash of it, its state (`accepted`, `running`, `done`, `failed`, `rejected`, `cancelled`), its result or error and the keys of the operations it made. The same job id with the same input is the same job; with another input it is a conflict. A caller sees its own jobs; the operator sees every job. The genesis has no jobs: it is one driver running one scenario. The node answers a request with a job id, the job holds the operation ids, and a reviewer reads the chain from the operations; how the node derives job ids, when it creates a job and what it records about every attempt is in docs/node.md.
|
|
58
|
+
|
|
59
|
+
### Fault injection
|
|
60
|
+
|
|
61
|
+
`--crash-before <key>` exits the process (code 75, `EX_TEMPFAIL`) right after the row is written as `sent`, before the broadcast; `--crash-after <key>` exits right after the broadcast, before the receipt is read. Both leave the store in the state a real crash would, and the next run must resolve it by the table above. CI crashes the genesis after the broadcast of `T3.A.submit`, resumes it to T9 (exactly one `recovered` operation, the tender with exactly one submission) and then replays the whole store (nothing sent). The rest of the table is covered offline: `test/tx-semantics.test.ts` drives the manager against an in-process JSON-RPC chain (`test/mock-chain.ts`) through the replacement limit across a restart, the same key under two requests, the reverted operation and its explicit retry, the estimate that reverts, one signer's queue and another signer beside it, the fee cap at the first send and at a replacement, the failing receipt lookup, the transaction a pool forgot and the nonce something else consumed; `test/tx-replace.test.ts` makes one replacement against a real anvil when one is installed.
|
|
62
|
+
|
|
63
|
+
## Facts and `memo`
|
|
64
|
+
|
|
65
|
+
Values a later step needs (ids, addresses, hashes) are facts in the store, written when known. Values that enter a transaction and depend on the clock or on chain state at run time — deadlines, the probe block, a Groth16 proof bound to an epoch — go through `kernel.memo(key, compute)`: computed once, read back on every later run, so a resumed run rebuilds exactly the inputs the recorded transactions were made from. Rebuilding them from the clock again would make the store's account of a replayed operation disagree with the chain.
|
|
66
|
+
|
|
67
|
+
## Chain time
|
|
68
|
+
|
|
69
|
+
`kernel.advanceTime(seconds, why)` lets a window pass. On anvil it moves the clock (`evm_increaseTime`, then a block) and writes a `time-skipped` journal entry with the interval and the reason, so a skipped window is visible in the evidence; on any other chain it waits for real time (`time-waited`, polling the block timestamp every 5 s). The genesis chooses realistic windows on a local chain and short ones elsewhere (docs/genesis.md).
|
|
70
|
+
|
|
71
|
+
The provider polls a local node every 250 ms and a public endpoint every 4 s: a shared, rate-limited endpoint that makes a block every 12 s is asked a few times per block, not several times per second.
|
|
72
|
+
|
|
73
|
+
A public RPC URL usually fronts a cluster whose nodes import a block at slightly different times, so a read made right after a receipt can land on a node that has not seen the block yet (on Sepolia the roster of a swarm read as not met one second after its third `join` confirmed, and a call opened in one block was unknown to the gas estimate of its delivery in the next). The kernel therefore reads what it has written: its provider (`kernel/provider.ts`, `PinnedProvider`) keeps the highest block it has seen in a receipt, a block header or a block number, and sends every state read made at "latest" — `eth_call`, `eth_estimateGas`, balances, code, storage — at that block instead. A node behind the pin answers with an error rather than with stale state, and the request is retried (up to 60 s) until a node that has the block answers; a contract's revert is an answer and is never retried. When the retries run out, the endpoint's head is sampled again: a pin that came from a node ahead of the rest is lowered to the head the endpoint reports and the read made there, once; otherwise the error is raised with the node's own words and the run stops where it is, to be resumed from its store. A read at "latest" is never substituted — the node that refuses an explicit block may be the one that is hours behind, and a stale answer fails checks that hold on chain (seen on Sepolia: a swarm read as ACTIVE after its conclusion, a document digest read as the version before its update). The kernel journals these events (`pinned-read`). Explicit block tags (the probe's fixed block), "pending" (the nonce) and everything that is not a state read pass through unchanged, and the pin moves forward with every receipt and with the block headers read while waiting for a window, so time-dependent views are read at the current block. The pin starts at the chain's head when the kernel opens, so a replayed run — which reads back old receipts and sends nothing — reads the chain as it is now, not as it was at each step. On a single local node the pin is simply the node's tip. The transaction manager additionally waits, after a receipt on a public chain, until the endpoint reports that block twice in a row (`visibility-waited` when it had to). The journal of the Sepolia run (`deployments/sepolia.genesis.example.json`) has the whole story in order: the two `estimate-failed` entries of the lag, the `pinned-read` entries with the nodes' refusals in their own words, one `recovered-receipt` for a transaction mined while the process hung, and the `time-waited` entries of the windows.
|
|
74
|
+
|
|
75
|
+
## Gas
|
|
76
|
+
|
|
77
|
+
Every estimate gets a margin (see above). A call whose outcome depends on the gas it is given — the SwarmRegistry's `challenge`, which treats an eligibility module's `false` returned with little gas left as unevaluable — takes an explicit `gasLimit` through `kernel.call`, because an estimate stops at the first non-reverting path, and "unevaluable, nothing changed" is one.
|
|
78
|
+
|
|
79
|
+
## Evidence
|
|
80
|
+
|
|
81
|
+
`kernel.evidence(step, label, values)` records what a reviewer needs per step (addresses, ids, hashes, transaction hashes, run receipts). The genesis writes it, together with the checks, facts, operation summary and the journal of the run, to `deployments/<world>.genesis.json` (`foga.genesis-evidence/1`). `deployments/local.genesis.example.json` is a committed run. The doctor consumes that file for its fifth layer (docs/doctor.md).
|
|
82
|
+
|
|
83
|
+
## Keys and funds
|
|
84
|
+
|
|
85
|
+
The kernel's signers are named by role (`W`, `G`, `owner.A`, `operator.A`, …). The genesis plays every part, so it loads all twelve: the first twelve accounts of one BIP-39 phrase (`src/genesis/roles.ts`), anvil's well-known phrase on anvil, so every local run uses the same addresses; on any other chain the phrase in `FOGA_MNEMONIC` or in the file named by `FOGA_MNEMONIC_FILE` (`foga keys new` writes one, `foga keys show` lists the addresses), with `FOGA_KEY_<ROLE>` (`FOGA_KEY_W`, `FOGA_KEY_OWNER_A`, `FOGA_KEY_OPERATOR_A`, …) overriding one role. Keys never leave the kernel: the skill worker (below) is started with an environment reduced to `PATH`.
|
|
86
|
+
|
|
87
|
+
A **node** plays one part and holds one key, its cell's operator (`src/node/keys.ts`, `loadOperatorKey`): `FOGA_OPERATOR_KEY_FILE` names a file holding the key — a regular file owned by the node's user and readable by nobody else (mode 0600 or tighter) — or `FOGA_OPERATOR_KEY` holds the key itself; exactly one of the two. There is no phrase fallback: a node whose environment also carries `FOGA_MNEMONIC`, `FOGA_MNEMONIC_FILE`, `FOGA_DEPLOYER_PK`, `FOGA_OWNER_KEY[_FILE]`, `FOGA_GUARDIAN_KEY[_FILE]` or any `FOGA_KEY_<ROLE>` refuses to start and names the variables (never their values), because a process that can derive or sign for the owner, the guardian or the treasury is not a node holding its operator key alone. The owner's and the guardian's keys are read the same way (`FOGA_OWNER_KEY[_FILE]`, `FOGA_GUARDIAN_KEY[_FILE]`, `loadRoleKey`) by the CLI's one-shot commands in the operator's own shell — a birth, a migration, a rotation, a suspension — never by a running node. `foga keys export --role operator.A --out <file>` writes that file (0600) from the operator's phrase, in the operator's shell, for the node's separate OS user to own; `foga keys check` reads the current environment as a node would. The account the key operates is what bounds it: revision 2 of FogaCellAccount allow-lists what the operator may call and refuses it every asset-moving selector (docs/cell-account.md), so a stolen operator key can do what the cell does, in the cell's scope, under its daily cap — and nothing else.
|
|
88
|
+
|
|
89
|
+
W is the treasury. Before every step the genesis tops up each role whose balance has fallen below half its floor (`src/genesis/funding.ts`: a gas budget per role measured on the Sepolia run of 2026-11-01 with a margin of one half — 721 M gas for the twelve roles, 4.4 times what anvil's older gas schedule charges for the same operations — priced at the fee the chain quotes when the step starts; fixed floors on anvil, where only the judges beyond the pre-funded accounts need anything) and gives each cell its working capital at birth. The amounts are memoized facts, so a resumed run rebuilds the same transfers. `foga genesis preflight` prints the plan and what W must hold without sending anything; `foga world deploy` checks the deployer's balance against a budget of 11 M gas per contract before the first deployment (docs/genesis.md, "Running on a public chain").
|
|
90
|
+
|
|
91
|
+
## Running a skill
|
|
92
|
+
|
|
93
|
+
Before a cell executes a skill it holds, the kernel rebuilds the package from the local directory and compares `mdHash` and `packageHash` with the token's on-chain commitment (`skillOf`); a tampered file changes both and the run is refused. The two built-in executors then run in the skill worker, a separate process (`src/genesis/skill-worker.ts`, started by `src/genesis/skill-runner.ts`): `foga-probe` makes view calls against the world at a pinned block, `finch-extract-verify` extracts and checks a document deterministically — the extraction runs there too since 0.6.0, not in the kernel's process. The worker holds no key; its environment is reduced to `PATH` (it inherits no `NODE_OPTIONS`, so nothing can be preloaded into it), its heap is capped (512 MB), its run has a time limit and its output a size limit (1 MiB; over either it is killed), and outside the development loader it runs under Node's permission model with read access to the directories the job names and nothing else. A document path is accepted only inside the input directory the parent names, a fields file only inside the skills directory (real paths, so a link cannot point outside). The parent receives one JSON answer on stdout and decides what to commit to. That is what the boundary is: a process with the least the job needs, not a sandbox for arbitrary code — the node's executors are these two, and docs/design-memo.md says so where it describes what a skill may run.
|
|
94
|
+
|
|
95
|
+
## The prover
|
|
96
|
+
|
|
97
|
+
Both ZK paths of ERC-8419 run through `prover/run.cjs`: a stdin/stdout bridge over the upstream prover (`vendor/kya-standard/companions/zk-kya-groth16/prover.js`, with the credential circuit's wasm and proving key next to it) and over FOGA's own predicate circuit (`zk/build`, docs/zk.md). `prover/` is a sub-package with its own dependencies (snarkjs, circomlibjs; `npm run zk:install`), so the kernel's own dependency tree stays at ethers. The bridge keeps the roles apart: `commit` (the prover's Poseidon commitment to its secret), `attest` (the attestor signs the credential over the commitment, never seeing the secret), `root` (an issuer or source set from keys), `prove` (the credential proof: the prover's secret plus public material only), `record.sign` (a source signs one task-completion record) and `predicate.prove` (the predicate proof: the records with their signatures, the prover's secret, public material). FOGA's attestor and source keys and the cells' prover secrets are derived from the signers' keys, so a run is reproducible and nothing extra is stored; a resumed run reuses the recorded proofs.
|
|
98
|
+
|
|
99
|
+
## `node:sqlite`
|
|
100
|
+
|
|
101
|
+
Node 22 ships `node:sqlite` unflagged but still prints an experimental notice on first load. The store silences that one notice during its own load and nothing else; Node 24 prints none.
|