capacity-attest 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -15
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +3 -3
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +537 -25
- package/dist/ledger.js.map +1 -1
- package/dist/schema.d.ts +4 -9
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +86 -11
- package/dist/schema.js.map +1 -1
- package/dist/tools.d.ts +10 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +23 -5
- package/dist/tools.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -2,26 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
MCP-server voor leverings-attestaties bij x402-capaciteitshandel tussen AI-agents.
|
|
4
4
|
|
|
5
|
-
> Status: MVP,
|
|
5
|
+
> Status: MVP, gepubliceerd op npm (`npm install capacity-attest`) en in het officiële MCP-register (`io.github.holistis/capacity-attest`).
|
|
6
6
|
|
|
7
7
|
## Waarom dit bestaat
|
|
8
8
|
|
|
9
|
-
Wanneer een AI-agent via het [x402-protocol](https://www.x402.org/) betaalt voor capaciteit
|
|
9
|
+
Wanneer een AI-agent via het [x402-protocol](https://www.x402.org/) betaalt voor capaciteit (GPU-uren, opslag, API/inference-credits, bandbreedte) bij een andere agent of dienst, is er na de betaling geen bewijs dat het beloofde ook echt geleverd is. De kopende agent weet het zelf (hij zag de output, of zag hem niet), maar die kennis gaat verloren zodra de sessie eindigt. De volgende agent die met dezelfde verkoper zaken wil doen, begint weer blind.
|
|
10
10
|
|
|
11
11
|
`capacity-attest` lost dat specifieke gat op: na afwikkeling laat de **betalende** agent een cryptografisch ondertekende, feitelijke claim achter (`delivered: yes/no/partial` + een hash van het bewijsmateriaal). Andere agents kunnen die geschiedenis opvragen **voordat** ze zelf met die verkoper in zee gaan.
|
|
12
12
|
|
|
13
|
-
Geen oordeel. Geen reputatiescore. Geen "vonnis"
|
|
13
|
+
Geen oordeel. Geen reputatiescore. Geen "vonnis", puur een ondertekende bon-plus-claim, net zoals een afleverbon bij een fysieke levering.
|
|
14
14
|
|
|
15
15
|
## Wat dit NIET is
|
|
16
16
|
|
|
17
17
|
Dit is bewust en hardcoded **niet**:
|
|
18
18
|
|
|
19
|
-
- **Geen reputatiescore of rating.** `get_delivery_history` retourneert de ruwe, chronologische lijst van claims
|
|
20
|
-
- **Geen financieel product.** Geen rente, geen tijd-disconto op betalingen, geen yield op het ledger-saldo (er ís geen saldo
|
|
19
|
+
- **Geen reputatiescore of rating.** `get_delivery_history` retourneert de ruwe, chronologische lijst van claims, geen gemiddelde, geen percentage, geen "trust score". Het samenvatten tot één getal is impliciet een oordeel, en dat is expliciet afgewezen tijdens de besluitvorming voor dit project.
|
|
20
|
+
- **Geen financieel product.** Geen rente, geen tijd-disconto op betalingen, geen yield op het ledger-saldo (er ís geen saldo, dit is geen escrow), geen lening, geen onderpand, geen invoice-financing/factoring. `assetType` is een gesloten enum van capaciteitssoorten (`gpu-hours`, `storage`, `api-credits`, `bandwidth`) en bevat bewust niets dat op een financieel instrument lijkt.
|
|
21
21
|
- **Geen eigen token of munt.** Betalingen lopen via x402/USDC zoals gebruikelijk; dit project registreert alleen de *bon* van een afwikkeling die al ergens anders heeft plaatsgevonden.
|
|
22
|
-
- **Geen krediet-verlening.** Een claim wordt pas gemaakt **na** een voltooide betaling
|
|
22
|
+
- **Geen krediet-verlening.** Een claim wordt pas gemaakt **na** een voltooide betaling. Dit project financiert niets, het documenteert een reeds afgeronde ijara (verhuur/dienst)-transactie.
|
|
23
23
|
|
|
24
|
-
Dit is een
|
|
24
|
+
Dit is een bewuste, formeel getoetste ontwerpkeuze, niet een toevallige scope-beperking. Zie de guardrails-sectie in het project-brief als je overweegt hier iets aan toe te voegen: bij twijfel of een veld/functie hiertegenaan schuurt, laat het weg.
|
|
25
25
|
|
|
26
26
|
## Hoe het werkt
|
|
27
27
|
|
|
@@ -32,25 +32,25 @@ De betalende agent (de koper) roept dit aan **na** een x402-afwikkeling, zodra b
|
|
|
32
32
|
| Veld | Betekenis |
|
|
33
33
|
| --- | --- |
|
|
34
34
|
| `sellerAddress` | 0x-adres van de partij die betaald werd |
|
|
35
|
-
| `buyerAddress` | 0x-adres van de betalende agent
|
|
35
|
+
| `buyerAddress` | 0x-adres van de betalende agent, moet overeenkomen met het adres dat uit `signature` wordt teruggerekend |
|
|
36
36
|
| `assetType` | `gpu-hours` \| `storage` \| `api-credits` \| `bandwidth` |
|
|
37
|
-
| `promisedSpec` | Wat er beloofd was
|
|
37
|
+
| `promisedSpec` | Wat er beloofd was: vrije tekst of een gestructureerd object |
|
|
38
38
|
| `delivered` | `yes` \| `no` \| `partial` |
|
|
39
|
-
| `evidenceHash` | sha256-hex van bewijsmateriaal (logs, response-payload, ...)
|
|
39
|
+
| `evidenceHash` | sha256-hex van bewijsmateriaal (logs, response-payload, ...), het bewijs zelf wordt niet opgeslagen |
|
|
40
40
|
| `settlementRef` | x402-payment-ref of on-chain tx-hash van de onderliggende betaling |
|
|
41
41
|
| `timestamp` | ISO-8601 tijdstip |
|
|
42
|
-
| `claimId` | content-addressed sha256-hash van alle velden hierboven
|
|
42
|
+
| `claimId` | content-addressed sha256-hash van alle velden hierboven, zie `computeClaimId()` in `src/schema.ts` |
|
|
43
43
|
| `signature` | EIP-191 personal-sign handtekening van de koper over `claimId` |
|
|
44
44
|
|
|
45
45
|
De server valideert eerst het schema, dan of `claimId` echt de hash van de inhoud is, en dan of `signature` echt terugrekent naar `buyerAddress`. Alleen dan wordt de claim toegevoegd aan de append-only ledger (`data/claims.jsonl`). Een ongeldige handtekening of een claim die al eerder is opgeslagen (zelfde `claimId`) wordt geweigerd.
|
|
46
46
|
|
|
47
47
|
### 2. `get_delivery_history`
|
|
48
48
|
|
|
49
|
-
Gegeven een `sellerAddress`, retourneert dit alle bekende claims tegen die verkoper, chronologisch (oudst eerst). Puur feitelijk
|
|
49
|
+
Gegeven een `sellerAddress`, retourneert dit alle bekende claims tegen die verkoper, chronologisch (oudst eerst). Puur feitelijk, geen samengevat getal. Een kopende agent roept dit aan **vóórdat** hij betaalt, om de ruwe leveringsgeschiedenis van een potentiële verkoper te zien en zelf te beoordelen.
|
|
50
50
|
|
|
51
51
|
## Ondertekening
|
|
52
52
|
|
|
53
|
-
De claim wordt ondertekend door de **koper** (de partij die betaalde en dus weet wat er wel/niet aankwam), niet door de verkoper. Dit is bewust eenvoudige EIP-191 `personal_sign` over `claimId` (via `ethers.Signer#signMessage`), geen EIP-712 typed data
|
|
53
|
+
De claim wordt ondertekend door de **koper** (de partij die betaalde en dus weet wat er wel/niet aankwam), niet door de verkoper. Dit is bewust eenvoudige EIP-191 `personal_sign` over `claimId` (via `ethers.Signer#signMessage`), geen EIP-712 typed data. Dat houdt het crypto-oppervlak van deze MVP klein en makkelijk te controleren. Een latere upgrade naar EIP-712 (zoals in `mcp-paywall/src/x402.mjs`) is additief mogelijk zonder bestaande claims ongeldig te maken.
|
|
54
54
|
|
|
55
55
|
## Lokaal draaien
|
|
56
56
|
|
|
@@ -63,7 +63,7 @@ npm run demo # end-to-end lokale demo met TEST-sleutels, geen live infra
|
|
|
63
63
|
npm start # start de MCP-server over stdio (bijv. voor Claude Desktop/Code als lokale MCP-server)
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
De ledger-locatie is instelbaar via `CAPACITY_ATTEST_DATA_DIR` (default: `./data` in dit package). Tests en de demo gebruiken altijd een eigen, wegwerpbare tijdelijke map
|
|
66
|
+
De ledger-locatie is instelbaar via `CAPACITY_ATTEST_DATA_DIR` (default: `./data` in dit package). Tests en de demo gebruiken altijd een eigen, wegwerpbare tijdelijke map, nooit de echte `data/` map.
|
|
67
67
|
|
|
68
68
|
## Architectuur
|
|
69
69
|
|
|
@@ -82,4 +82,4 @@ examples/demo.ts end-to-end lokaal voorbeeld met TEST-sleutels
|
|
|
82
82
|
|
|
83
83
|
## Relatie tot x402
|
|
84
84
|
|
|
85
|
-
Dit project verifieert of settelt zelf géén x402-betalingen
|
|
85
|
+
Dit project verifieert of settelt zelf géén x402-betalingen, dat gebeurt al bij de betaalstap zelf (zie bijvoorbeeld `mcp-paywall/src/x402.mjs` in dit ecosysteem voor een volledige EIP-3009-verify/settle-implementatie). `settlementRef` verwijst simpelweg naar die reeds-voltooide afwikkeling. Dat betekent ook dat de MVP-koppeling met een echte x402-facilitator eenvoudig kan blijven: `settlementRef` is vrije tekst, met als aanname dat de koper 'm eerlijk invult. Een latere versie kan dat veld optioneel verifiëren tegen een echte facilitator (TODO, niet in deze MVP).
|
package/dist/index.js
CHANGED
|
@@ -27,7 +27,7 @@ server.registerTool("record_delivery", {
|
|
|
27
27
|
"other agents read it back.",
|
|
28
28
|
inputSchema: DeliveryClaimSchema.shape,
|
|
29
29
|
}, async (args) => {
|
|
30
|
-
const result = recordDelivery(args);
|
|
30
|
+
const result = await recordDelivery(args);
|
|
31
31
|
if (!result.ok)
|
|
32
32
|
return errorResult(result.reason);
|
|
33
33
|
return textResult({ ok: true, claimId: result.claimId });
|
|
@@ -42,7 +42,7 @@ server.registerTool("get_delivery_history", {
|
|
|
42
42
|
sellerAddress: ClaimContentSchema.shape.sellerAddress,
|
|
43
43
|
},
|
|
44
44
|
}, async ({ sellerAddress }) => {
|
|
45
|
-
return textResult(getDeliveryHistory(sellerAddress));
|
|
45
|
+
return textResult(await getDeliveryHistory(sellerAddress));
|
|
46
46
|
});
|
|
47
47
|
// Re-exported so callers embedding this package can reference the same enums
|
|
48
48
|
// / schema the tools validate against without duplicating them.
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,yEAAyE;AACzE,2EAA2E;AAC3E,oDAAoD;AACpD,uDAAuD;AAEvD,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACrG,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhE,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,iBAAiB;IACvB,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,SAAS,UAAU,CAAC,KAAc;IAChC,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAChF,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED,SAAS,WAAW,CAAC,OAAe;IAClC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,OAAO,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACnF,CAAC;AAED,MAAM,CAAC,YAAY,CACjB,iBAAiB,EACjB;IACE,KAAK,EAAE,yBAAyB;IAChC,WAAW,EACT,oHAAoH;QACpH,qGAAqG;QACrG,4GAA4G;QAC5G,2GAA2G;QAC3G,4BAA4B;IAC9B,WAAW,EAAE,mBAAmB,CAAC,KAAK;CACvC,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,yEAAyE;AACzE,2EAA2E;AAC3E,oDAAoD;AACpD,uDAAuD;AAEvD,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACrG,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhE,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,iBAAiB;IACvB,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,SAAS,UAAU,CAAC,KAAc;IAChC,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAChF,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED,SAAS,WAAW,CAAC,OAAe;IAClC,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,OAAO,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACnF,CAAC;AAED,MAAM,CAAC,YAAY,CACjB,iBAAiB,EACjB;IACE,KAAK,EAAE,yBAAyB;IAChC,WAAW,EACT,oHAAoH;QACpH,qGAAqG;QACrG,4GAA4G;QAC5G,2GAA2G;QAC3G,4BAA4B;IAC9B,WAAW,EAAE,mBAAmB,CAAC,KAAK;CACvC,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAClD,OAAO,UAAU,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;AAC3D,CAAC,CACF,CAAC;AAEF,MAAM,CAAC,YAAY,CACjB,sBAAsB,EACtB;IACE,KAAK,EAAE,iCAAiC;IACxC,WAAW,EACT,8GAA8G;QAC9G,gHAAgH;QAChH,4GAA4G;QAC5G,mBAAmB;IACrB,WAAW,EAAE;QACX,aAAa,EAAE,kBAAkB,CAAC,KAAK,CAAC,aAAa;KACtD;CACF,EACD,KAAK,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE;IAC1B,OAAO,UAAU,CAAC,MAAM,kBAAkB,CAAC,aAAa,CAAC,CAAC,CAAC;AAC7D,CAAC,CACF,CAAC;AAEF,6EAA6E;AAC7E,gEAAgE;AAChE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,CAAC;AAEzC,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;IACjB,OAAO,CAAC,KAAK,CAAC,6CAA6C,EAAE,CAAC,CAAC,CAAC;IAChE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/ledger.d.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { type DeliveryClaim } from "./schema.js";
|
|
|
3
3
|
* Append one already-verified claim to the ledger. Throws if a claim with
|
|
4
4
|
* the same claimId was already recorded.
|
|
5
5
|
*/
|
|
6
|
-
export declare function appendClaim(claim: DeliveryClaim): DeliveryClaim
|
|
6
|
+
export declare function appendClaim(claim: DeliveryClaim): Promise<DeliveryClaim>;
|
|
7
7
|
/** All claims recorded against one seller, oldest first. */
|
|
8
|
-
export declare function claimsForSeller(sellerAddress: string): DeliveryClaim[]
|
|
8
|
+
export declare function claimsForSeller(sellerAddress: string): Promise<DeliveryClaim[]>;
|
|
9
9
|
/** Every claim in the ledger, oldest first. Mainly useful for tests/inspection. */
|
|
10
|
-
export declare function allClaims(): DeliveryClaim[]
|
|
10
|
+
export declare function allClaims(): Promise<DeliveryClaim[]>;
|
|
11
11
|
//# sourceMappingURL=ledger.d.ts.map
|
package/dist/ledger.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"AAYA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"AAYA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAoftE;;;GAGG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CA0D9E;AAED,4DAA4D;AAC5D,wBAAsB,eAAe,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAQrF;AAED,mFAAmF;AACnF,wBAAsB,SAAS,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAG1D"}
|
package/dist/ledger.js
CHANGED
|
@@ -6,52 +6,564 @@
|
|
|
6
6
|
// history. A second claim with the same content-addressed claimId (i.e. an
|
|
7
7
|
// attempt to re-record the exact same claim) is rejected rather than
|
|
8
8
|
// silently duplicated.
|
|
9
|
-
import {
|
|
9
|
+
import { readFileSync, appendFileSync, openSync, closeSync, unlinkSync, statSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { dataDir, ensureDataDir } from "./config.js";
|
|
12
12
|
import { DeliveryClaimSchema } from "./schema.js";
|
|
13
13
|
function claimsFile() {
|
|
14
14
|
return join(dataDir(), "claims.jsonl");
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
// A concurrency benchmark (bench/ledger-concurrency.mjs) found that
|
|
17
|
+
// appendClaim()'s original read-then-check-then-write sequence was a real,
|
|
18
|
+
// reproducible TOCTOU race: with separate OS processes sharing one
|
|
19
|
+
// CAPACITY_ATTEST_DATA_DIR (exactly the multi-agent deployment this ledger
|
|
20
|
+
// exists for), 3 of 5 runs at N=8 concurrent submissions of the SAME claim
|
|
21
|
+
// produced actual duplicate lines in claims.jsonl — the ledger's one
|
|
22
|
+
// documented guarantee, broken 60% of the time under realistic load.
|
|
23
|
+
//
|
|
24
|
+
// Fix: a lockfile-based mutex around the whole read+check+append sequence,
|
|
25
|
+
// using O_EXCL ("wx") as the atomic primitive — only one process can
|
|
26
|
+
// successfully create the lock file at a time, so the race window closes.
|
|
27
|
+
//
|
|
28
|
+
// SECOND FIX (2026-08-31, same day): the mutex's first version retried a
|
|
29
|
+
// contended lock with a synchronous `while (Date.now() < until) {}` spin
|
|
30
|
+
// loop. That busy-wait blocks the entire Node.js event loop for however
|
|
31
|
+
// long it spins (up to LOCK_TIMEOUT_MS = 5000ms) — in a live server this
|
|
32
|
+
// package is designed to run inside (a buyer agent recording a delivery
|
|
33
|
+
// claim right after a paid x402 call), that stalls every other in-flight
|
|
34
|
+
// request on the process, not just the caller. The retry now awaits a real
|
|
35
|
+
// `setTimeout`-based delay instead, which yields control back to the event
|
|
36
|
+
// loop between attempts. This makes acquireLock() / appendClaim() (and,
|
|
37
|
+
// for a consistent async surface across the module, readClaims() /
|
|
38
|
+
// claimsForSeller() / allClaims()) Promise-returning. Every call site
|
|
39
|
+
// (tools.ts, index.ts's tool handlers, examples/demo.ts, the test suite,
|
|
40
|
+
// the bench/ scripts) was updated to await them — a half-converted fix
|
|
41
|
+
// (async internals with a caller that forgot to await) would silently
|
|
42
|
+
// reorder the read-check-write sequence and could reintroduce the exact
|
|
43
|
+
// duplicate-claim race the lock exists to prevent. Verified after the fix:
|
|
44
|
+
// bench/ledger-concurrency.mjs still shows 0% duplicates under real
|
|
45
|
+
// multi-process contention (same as right after the first fix), and a new
|
|
46
|
+
// bench/event-loop-non-blocking.mjs proves a concurrent setInterval no
|
|
47
|
+
// longer stalls while the lock is contended.
|
|
48
|
+
const LOCK_RETRY_DELAY_MS = 5;
|
|
49
|
+
const LOCK_TIMEOUT_MS = 5_000;
|
|
50
|
+
// If a process crashes while holding the lock, the lock file would
|
|
51
|
+
// otherwise block every future write forever. Treat a lock older than this
|
|
52
|
+
// as abandoned and reclaim it.
|
|
53
|
+
const STALE_LOCK_MS = 30_000;
|
|
54
|
+
function lockFile() {
|
|
55
|
+
return claimsFile() + ".lock";
|
|
56
|
+
}
|
|
57
|
+
/** Resolves after `ms` real milliseconds — yields control back to the event loop, unlike a spin loop. */
|
|
58
|
+
function delay(ms) {
|
|
59
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
60
|
+
}
|
|
61
|
+
// Errno codes that mean "could not create the lock file right now because
|
|
62
|
+
// someone else is touching it" and are therefore worth retrying, rather
|
|
63
|
+
// than "something is actually wrong" and worth throwing immediately.
|
|
64
|
+
// EEXIST is the expected, documented case (another process holds the
|
|
65
|
+
// lock). EPERM is included too: on Windows/NTFS, deleting a file (as
|
|
66
|
+
// releaseLock() does) can leave it briefly in a "pending delete" state, and
|
|
67
|
+
// a `wx`-mode open racing against that transition can surface as EPERM
|
|
68
|
+
// rather than either a clean success or a clean EEXIST. Found empirically
|
|
69
|
+
// (2026-08-31) while writing bench/event-loop-non-blocking.mjs: 25
|
|
70
|
+
// concurrent in-process callers all racing to re-acquire a just-released
|
|
71
|
+
// lock intermittently hit this, and — before this was added — it threw
|
|
72
|
+
// past the retry loop entirely, turning ordinary contention into a hard
|
|
73
|
+
// failure instead of a bounded wait. Reproduces identically on the
|
|
74
|
+
// pre-async-fix synchronous code too, so this is not something the
|
|
75
|
+
// busy-wait -> async change introduced; it is a pre-existing Windows-only
|
|
76
|
+
// gap in what counted as "retryable" that this same fix pass closed.
|
|
77
|
+
function isRetryableLockError(e) {
|
|
78
|
+
const code = e.code;
|
|
79
|
+
return code === "EEXIST" || code === "EPERM";
|
|
80
|
+
}
|
|
81
|
+
async function acquireLock(path) {
|
|
82
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
83
|
+
for (;;) {
|
|
84
|
+
try {
|
|
85
|
+
closeSync(openSync(path, "wx"));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
if (!isRetryableLockError(e))
|
|
90
|
+
throw e;
|
|
91
|
+
try {
|
|
92
|
+
if (Date.now() - statSync(path).mtimeMs > STALE_LOCK_MS) {
|
|
93
|
+
unlinkSync(path);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Lock file vanished between our failed open() and this stat/unlink
|
|
99
|
+
// — another process released it. Just retry the open immediately.
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (Date.now() > deadline) {
|
|
103
|
+
throw new Error(`ledger_lock_timeout: could not acquire lock at ${path}`);
|
|
104
|
+
}
|
|
105
|
+
// Non-blocking backoff: await a real timer instead of spinning, so the
|
|
106
|
+
// event loop stays free to process other work (timers, other
|
|
107
|
+
// requests) while this call is waiting for the lock.
|
|
108
|
+
await delay(LOCK_RETRY_DELAY_MS);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function releaseLock(path) {
|
|
113
|
+
try {
|
|
114
|
+
unlinkSync(path);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Already gone (e.g. reclaimed as stale by another process) — fine.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// THIRD FIX (2026-08-31, same day, found by an adversarial verifier): the
|
|
121
|
+
// function this comment used to describe (readClaims()) re-read and
|
|
122
|
+
// re-validated the ENTIRE ledger file, through zod, on every single call —
|
|
123
|
+
// appendClaim() (for its duplicate-claimId check) AND claimsForSeller()/
|
|
124
|
+
// allClaims(). Cost is linear in TOTAL ledger size, and since this ledger is
|
|
125
|
+
// explicitly append-only, permanent history (nothing is ever deleted — see
|
|
126
|
+
// this file's header comment), that cost only grows over the service's
|
|
127
|
+
// lifetime, never shrinks. Measured empirically before this fix: 903ms of
|
|
128
|
+
// unbroken event-loop freeze at 40,000 lines for a single call with zero
|
|
129
|
+
// contention; under real lock contention against a 5,000-line ledger with 10
|
|
130
|
+
// concurrent callers, a single continuous 526ms stretch where the event loop
|
|
131
|
+
// processed nothing at all. In this package's real deployment shape (a
|
|
132
|
+
// single long-lived Node process, no clustering — see x402-server.mjs), that
|
|
133
|
+
// is every other in-flight request on the process freezing for over half a
|
|
134
|
+
// second, every time anyone records a delivery, and it gets worse forever.
|
|
135
|
+
//
|
|
136
|
+
// Fix: an in-memory cache, per resolved ledger file path (NOT a single
|
|
137
|
+
// global — CAPACITY_ATTEST_DATA_DIR differs between tests/instances, so
|
|
138
|
+
// keying by path is what makes this correct for more than one ledger file
|
|
139
|
+
// per process), holding the already-parsed-and-validated claims plus two
|
|
140
|
+
// indexes built for the two things this module actually needs to answer
|
|
141
|
+
// fast: a claimId Set (appendClaim()'s duplicate check) and a
|
|
142
|
+
// sellerAddress -> claims[] Map (claimsForSeller()). Each cache entry also
|
|
143
|
+
// carries the ledger file's byte size AT THE MOMENT it was built.
|
|
144
|
+
//
|
|
145
|
+
// On every call, a cheap `statSync` (no parsing) compares the file's CURRENT
|
|
146
|
+
// byte size against the cached size. Byte size, not mtime: filesystem mtime
|
|
147
|
+
// resolution can be coarse enough on some platforms that two real writes
|
|
148
|
+
// within the same tick look unchanged to an mtime check — that would be a
|
|
149
|
+
// silent-stale-read correctness bug, not just a missed optimization.
|
|
150
|
+
// Unchanged size -> the cache is used as-is, no disk read, no re-parse, no
|
|
151
|
+
// re-validation: this is the fast path this fix exists to add. Changed size
|
|
152
|
+
// -> the file was written since our cache was built (by another process, or
|
|
153
|
+
// by our own appendClaim() before it started updating the cache in-place —
|
|
154
|
+
// see below), so the cache is stale and gets rebuilt from disk.
|
|
155
|
+
//
|
|
156
|
+
// LOCK-VS-STALENESS-CHECK ORDERING (the part that is easy to get backwards
|
|
157
|
+
// and silently reintroduce the original duplicate-claim race): appendClaim()
|
|
158
|
+
// does its staleness check/resync AFTER acquireLock(), never before. If it
|
|
159
|
+
// checked staleness first and only acquired the lock afterward, there would
|
|
160
|
+
// be a TOCTOU gap between "we decided the cache is fresh" and "we hold the
|
|
161
|
+
// lock" during which another process could append — our now-actually-stale
|
|
162
|
+
// cache would then pass the duplicate check against a claimId set that is
|
|
163
|
+
// missing that concurrent write, and two processes could both believe they
|
|
164
|
+
// won the race, exactly the bug the lock was built to close in the first
|
|
165
|
+
// fix. Doing the freshness check (and, if needed, the resync) INSIDE the
|
|
166
|
+
// locked section instead means: by the time appendClaim() looks at the
|
|
167
|
+
// cache, no other process can be mid-write against this file (the lock
|
|
168
|
+
// serializes that), so the cache, once confirmed fresh (or just rebuilt),
|
|
169
|
+
// stays a true reflection of on-disk state for the rest of the critical
|
|
170
|
+
// section — there is no gap left for a concurrent writer to hide in. The
|
|
171
|
+
// cache is therefore a performance layer INSIDE the lock's critical section,
|
|
172
|
+
// never a substitute for it; duplicate-claimId safety still comes 100% from
|
|
173
|
+
// the lock, exactly as before this fix.
|
|
174
|
+
//
|
|
175
|
+
// claimsForSeller()/allClaims() do NOT take the lock (same as before this
|
|
176
|
+
// fix — see the original comment this one replaces), because their
|
|
177
|
+
// correctness need is different: they don't need atomicity with a
|
|
178
|
+
// concurrent write, they just need to reflect a recent, self-consistent
|
|
179
|
+
// on-disk state at the time they're called. The same statSync-based
|
|
180
|
+
// staleness check gives them that: right after another process (or our own
|
|
181
|
+
// appendClaim()) writes, the file's size changes, so the very next
|
|
182
|
+
// claimsForSeller()/allClaims() call resyncs instead of trusting a now-stale
|
|
183
|
+
// cache. This is exactly the "process A warm cache, process B writes
|
|
184
|
+
// directly, process A must see it" scenario the new
|
|
185
|
+
// cross-process-cache-invalidation test in ledger.test.ts exercises.
|
|
186
|
+
//
|
|
187
|
+
// The resync-from-disk path is still fundamentally O(n) — unavoidable if the
|
|
188
|
+
// true on-disk state must be validated — but two things change: (1) it is
|
|
189
|
+
// now the EXCEPTION (only when the file actually changed underneath us),
|
|
190
|
+
// not the norm (every call, regardless); (2) it no longer holds the event
|
|
191
|
+
// loop hostage for its full duration in one stretch — resyncFromDisk()
|
|
192
|
+
// processes lines in RESYNC_BATCH_SIZE-sized batches and `await`s a real
|
|
193
|
+
// setImmediate between batches, bounding any single blocking stretch to
|
|
194
|
+
// roughly one batch's cost rather than the whole file's, the same
|
|
195
|
+
// non-blocking-yield pattern the previous fix in this file applied to lock
|
|
196
|
+
// contention.
|
|
197
|
+
//
|
|
198
|
+
// Concurrent-resync safety within one process: resyncFromDisk() builds its
|
|
199
|
+
// claims array / claimId Set / sellerAddress Map into a brand-new object and
|
|
200
|
+
// only publishes it (a single synchronous Map.set on cacheByPath) once
|
|
201
|
+
// fully built — so a reader that hits the fast path mid-resync sees either
|
|
202
|
+
// the old (stale but internally consistent) cache or the new one, never a
|
|
203
|
+
// half-populated one. If two callers in the same process both observe
|
|
204
|
+
// staleness at once (e.g. a concurrent appendClaim() and claimsForSeller()),
|
|
205
|
+
// they share ONE in-flight resync via pendingResyncs rather than each
|
|
206
|
+
// starting their own redundant O(n) pass.
|
|
207
|
+
//
|
|
208
|
+
// Verified after this fix: bench/ledger-scale.mjs (rewritten for this fix)
|
|
209
|
+
// shows append/history calls after the first one at a given N running in
|
|
210
|
+
// well under a millisecond instead of growing linearly with ledger size;
|
|
211
|
+
// bench/ledger-concurrency.mjs still shows 0% duplicates and 0 lost writes
|
|
212
|
+
// under real multi-process contention (unchanged from the previous fix);
|
|
213
|
+
// bench/event-loop-non-blocking.mjs (the previous fix's proof) still passes
|
|
214
|
+
// unmodified, confirming this fix did not reintroduce event-loop blocking on
|
|
215
|
+
// the lock-wait path it already fixed.
|
|
216
|
+
const RESYNC_BATCH_SIZE = 500;
|
|
217
|
+
function emptyCache() {
|
|
218
|
+
return { size: 0, claims: [], claimIdSet: new Set(), bySeller: new Map() };
|
|
219
|
+
}
|
|
220
|
+
// Keyed by resolved ledger file path, not a single global — tests (and any
|
|
221
|
+
// future multi-tenant use) point CAPACITY_ATTEST_DATA_DIR at different
|
|
222
|
+
// directories within the same process, and a single shared cache variable
|
|
223
|
+
// would silently serve one ledger's data for another's calls.
|
|
224
|
+
const cacheByPath = new Map();
|
|
225
|
+
// De-duplicates concurrent resyncs of the SAME path within one process: if
|
|
226
|
+
// two callers both observe staleness before either resync finishes, the
|
|
227
|
+
// second joins the first's in-flight Promise instead of redoing the O(n)
|
|
228
|
+
// work itself.
|
|
229
|
+
const pendingResyncs = new Map();
|
|
230
|
+
/** Yields control back to the event loop — used between resync batches. */
|
|
231
|
+
function yieldToEventLoop() {
|
|
232
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
233
|
+
}
|
|
234
|
+
function timestampMs(claim) {
|
|
235
|
+
return Date.parse(claim.timestamp);
|
|
236
|
+
}
|
|
237
|
+
// WHY THE CACHE KEEPS claims/bySeller PRE-SORTED, INSTEAD OF SORTING AT READ
|
|
238
|
+
// TIME (which is what the pre-cache readClaims()-based code did, and what an
|
|
239
|
+
// earlier draft of THIS fix also did): claimsForSeller()/allClaims() are
|
|
240
|
+
// exactly the calls this whole fix is about making cheap on the warm path.
|
|
241
|
+
// Re-running `.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp))`
|
|
242
|
+
// on every single warm call is itself O(n log n) with an expensive
|
|
243
|
+
// comparator (Date.parse() is a real date-string parse, not a cheap number
|
|
244
|
+
// compare) — at N=40,000 that measured at ~13-14ms PER WARM CALL in this
|
|
245
|
+
// fix's own benchmark (bench/ledger-cache-scale.mjs), 450x the N=0 floor,
|
|
246
|
+
// which is not "amortized O(1) or close to it" by any reasonable reading,
|
|
247
|
+
// even though it correctly skipped the disk read. So: claims/bySeller are
|
|
248
|
+
// sorted ONCE per resync (still inside the O(n) resync's cost, effectively
|
|
249
|
+
// free to add), and appendClaim() keeps that invariant on the warm
|
|
250
|
+
// incremental-update path too, via a binary-search insert (sortedInsertIndex
|
|
251
|
+
// below) instead of a plain push — O(log n) to find the position, O(n) for
|
|
252
|
+
// the underlying array shift (a plain memmove of object references, not
|
|
253
|
+
// re-parsing/re-validating anything), which is what turns the warm-call
|
|
254
|
+
// benchmark's claimsForSeller()/allClaims() numbers into a plain array copy:
|
|
255
|
+
// no comparator, no Date.parse, cost proportional only to the RESULT size,
|
|
256
|
+
// not the ledger's total size.
|
|
257
|
+
function sortedInsertIndex(sorted, ts) {
|
|
258
|
+
let lo = 0;
|
|
259
|
+
let hi = sorted.length;
|
|
260
|
+
while (lo < hi) {
|
|
261
|
+
const mid = (lo + hi) >>> 1;
|
|
262
|
+
// <= keeps insertion AFTER any existing entries with an equal
|
|
263
|
+
// timestamp, so a newly appended claim with a tied timestamp lands
|
|
264
|
+
// after previously-recorded ones — the same tie-break a stable
|
|
265
|
+
// file-order sort would have produced, since this is an append-only
|
|
266
|
+
// log and the new claim is, by definition, the most recently arrived.
|
|
267
|
+
if (timestampMs(sorted[mid]) <= ts)
|
|
268
|
+
lo = mid + 1;
|
|
269
|
+
else
|
|
270
|
+
hi = mid;
|
|
271
|
+
}
|
|
272
|
+
return lo;
|
|
273
|
+
}
|
|
274
|
+
// SIXTH FIX (2026-08-31, same day, found by adversarial verification): the
|
|
275
|
+
// parse loop above correctly yields every RESYNC_BATCH_SIZE lines, but the
|
|
276
|
+
// code that used to follow it — a single `decorated.sort(...)` over the
|
|
277
|
+
// WHOLE array, then a single unyielded `for` loop over the WHOLE sorted
|
|
278
|
+
// array to build `bySeller` — both ran as one uninterrupted synchronous
|
|
279
|
+
// stretch after the batched parse loop finished. Measured: at N=50,000 that
|
|
280
|
+
// unyielded tail alone was ~75ms (26ms sort + 47ms bySeller-build); at a
|
|
281
|
+
// plausible future N=200,000 it was ~438ms — approaching the magnitude of
|
|
282
|
+
// the original pre-cache bug (903ms at N=40,000) this entire fix exists to
|
|
283
|
+
// eliminate. A plain per-batch merge (merge each newly-parsed batch into the
|
|
284
|
+
// accumulated sorted result, yield between batches) would NOT actually fix
|
|
285
|
+
// this: the merge step itself is O(accumulated size), so the LAST merge in a
|
|
286
|
+
// large ledger would still be one unyielded O(n) stretch — the exact same
|
|
287
|
+
// shape of bug, just moved earlier. What actually bounds every unyielded
|
|
288
|
+
// stretch to ~one batch's cost, all the way through an O(n log n) sort, is
|
|
289
|
+
// counting WORK DONE (elements written to the merge output), not batch
|
|
290
|
+
// boundaries, and yielding whenever that counter crosses RESYNC_BATCH_SIZE —
|
|
291
|
+
// this is what yieldingMergeSort() below does: a standard iterative
|
|
292
|
+
// bottom-up merge sort (stable, like the native Array.prototype.sort() it
|
|
293
|
+
// replaces — see its own comment for why <= on ties matters), except every
|
|
294
|
+
// single element written into the merge output increments a shared counter
|
|
295
|
+
// that triggers a real yield once it reaches RESYNC_BATCH_SIZE, regardless
|
|
296
|
+
// of which pass or which pair-merge that element happened to fall in. Total
|
|
297
|
+
// work is still the same O(n log n) a native sort would do; what changes is
|
|
298
|
+
// that it is never done in one unbroken stretch bigger than one batch.
|
|
299
|
+
async function yieldingMergeSort(items) {
|
|
300
|
+
const n = items.length;
|
|
301
|
+
if (n < 2)
|
|
302
|
+
return;
|
|
303
|
+
let src = items;
|
|
304
|
+
let dst = new Array(n);
|
|
305
|
+
let writtenSinceYield = 0;
|
|
306
|
+
for (let width = 1; width < n; width *= 2) {
|
|
307
|
+
for (let lo = 0; lo < n; lo += width * 2) {
|
|
308
|
+
const mid = Math.min(lo + width, n);
|
|
309
|
+
const hi = Math.min(lo + width * 2, n);
|
|
310
|
+
let i = lo;
|
|
311
|
+
let j = mid;
|
|
312
|
+
let k = lo;
|
|
313
|
+
while (i < mid || j < hi) {
|
|
314
|
+
// <= (not <) is what makes this stable: on a tie, the left run
|
|
315
|
+
// (which, at every level of a bottom-up merge sort, always holds
|
|
316
|
+
// elements that were earlier in the pre-sort/file order than the
|
|
317
|
+
// right run) is drained first — identical tie-break to the native
|
|
318
|
+
// stable `.sort()` this replaces.
|
|
319
|
+
if (j >= hi || (i < mid && src[i].ts <= src[j].ts)) {
|
|
320
|
+
dst[k++] = src[i++];
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
dst[k++] = src[j++];
|
|
324
|
+
}
|
|
325
|
+
if (++writtenSinceYield >= RESYNC_BATCH_SIZE) {
|
|
326
|
+
writtenSinceYield = 0;
|
|
327
|
+
await yieldToEventLoop();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
[src, dst] = [dst, src];
|
|
332
|
+
}
|
|
333
|
+
if (src !== items) {
|
|
334
|
+
// The last swap left the fully-sorted array in `src`, which is the
|
|
335
|
+
// scratch buffer, not the caller's `items` array — copy it back in
|
|
336
|
+
// place, itself batched/yielded for the same reason as everything else
|
|
337
|
+
// in this function: a plain unyielded `for` here over all n elements
|
|
338
|
+
// would just reintroduce the exact bug this function exists to close.
|
|
339
|
+
for (let i = 0; i < n; i++) {
|
|
340
|
+
items[i] = src[i];
|
|
341
|
+
if ((i + 1) % RESYNC_BATCH_SIZE === 0 && i + 1 < n) {
|
|
342
|
+
await yieldToEventLoop();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Re-reads and re-validates the entire ledger file from disk, in batches
|
|
349
|
+
* with a real yield between them so this never holds the event loop for the
|
|
350
|
+
* whole file's cost in one uninterrupted stretch. Builds a brand-new cache
|
|
351
|
+
* object and only publishes it once complete (see the header comment above
|
|
352
|
+
* for why that matters for in-process concurrent readers).
|
|
353
|
+
*/
|
|
354
|
+
async function resyncFromDisk(file) {
|
|
355
|
+
let content;
|
|
356
|
+
try {
|
|
357
|
+
content = readFileSync(file, "utf8");
|
|
358
|
+
}
|
|
359
|
+
catch (e) {
|
|
360
|
+
// FIFTH FIX (2026-08-31, same day, found by adversarial verification):
|
|
361
|
+
// this bare `catch` used to treat EVERY readFileSync error identically
|
|
362
|
+
// to "file doesn't exist yet" — including a transient EACCES, EBUSY,
|
|
363
|
+
// EMFILE, or a Windows sharing-violation, none of which mean the ledger
|
|
364
|
+
// is empty. Silently downgrading a real error to "empty cache" can wipe
|
|
365
|
+
// a warm, correct in-memory cache down to zero known claimIds, after
|
|
366
|
+
// which a genuine duplicate claimId would be wrongly accepted — the
|
|
367
|
+
// exact guarantee this whole module exists to protect. Only ENOENT
|
|
368
|
+
// (mirrors isRetryableLockError()'s explicit-code-check style above)
|
|
369
|
+
// means "does not exist yet, empty is the correct read"; every other
|
|
370
|
+
// error now propagates as a real thrown error instead of being eaten.
|
|
371
|
+
if (e.code !== "ENOENT")
|
|
372
|
+
throw e;
|
|
373
|
+
// Doesn't exist (or vanished) — a fresh, empty, but still validly-sized
|
|
374
|
+
// (0 bytes) cache. Matches the pre-fix behavior of readClaims()
|
|
375
|
+
// returning [] when the file doesn't exist yet.
|
|
376
|
+
const empty = emptyCache();
|
|
377
|
+
cacheByPath.set(file, empty);
|
|
378
|
+
return empty;
|
|
379
|
+
}
|
|
380
|
+
const lines = content.split("\n").filter(Boolean);
|
|
381
|
+
// Decorated with each claim's parsed timestamp so the sort below compares
|
|
382
|
+
// plain numbers instead of re-parsing every timestamp string on every
|
|
383
|
+
// comparison (an O(n log n)-times cost vs. this O(n)-times one).
|
|
384
|
+
const decorated = [];
|
|
385
|
+
const claimIdSet = new Set();
|
|
386
|
+
for (let i = 0; i < lines.length; i++) {
|
|
387
|
+
const line = lines[i];
|
|
24
388
|
try {
|
|
25
|
-
|
|
389
|
+
const claim = DeliveryClaimSchema.parse(JSON.parse(line));
|
|
390
|
+
decorated.push({ claim, ts: timestampMs(claim) });
|
|
391
|
+
claimIdSet.add(claim.claimId.toLowerCase());
|
|
26
392
|
}
|
|
27
393
|
catch {
|
|
28
394
|
// A corrupt/partial line (e.g. a truncated write) must not take down
|
|
29
|
-
// reads of the rest of the ledger.
|
|
30
|
-
|
|
395
|
+
// reads of the rest of the ledger — same tolerance as before this fix.
|
|
396
|
+
}
|
|
397
|
+
if ((i + 1) % RESYNC_BATCH_SIZE === 0 && i + 1 < lines.length) {
|
|
398
|
+
await yieldToEventLoop();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Stable sort, same tie-break behavior (ties keep original file/append
|
|
402
|
+
// order) as the pre-fix code's native `.sort()` — but via
|
|
403
|
+
// yieldingMergeSort() instead, which bounds every unyielded stretch to
|
|
404
|
+
// ~RESYNC_BATCH_SIZE elements instead of running as one uninterrupted
|
|
405
|
+
// O(n log n) block. See yieldingMergeSort()'s own comment (SIXTH FIX) for
|
|
406
|
+
// why a naive per-batch merge would not actually have achieved this.
|
|
407
|
+
await yieldingMergeSort(decorated);
|
|
408
|
+
const claims = decorated.map((d) => d.claim);
|
|
409
|
+
// sellerAddress is already lower-cased by ClaimContentSchema's own
|
|
410
|
+
// .transform() (see schema.ts), so no extra normalization needed here.
|
|
411
|
+
// Iterating the now-sorted `claims` means every bucket comes out sorted
|
|
412
|
+
// too, with no separate per-seller sort needed. Batched with a yield every
|
|
413
|
+
// RESYNC_BATCH_SIZE claims (same SIXTH FIX as the sort above) instead of
|
|
414
|
+
// one unyielded pass over the whole array — building this map is itself
|
|
415
|
+
// O(n) and was part of the same measured 47ms-at-N=50,000 unyielded tail.
|
|
416
|
+
const bySeller = new Map();
|
|
417
|
+
for (let i = 0; i < claims.length; i++) {
|
|
418
|
+
const claim = claims[i];
|
|
419
|
+
const bucket = bySeller.get(claim.sellerAddress);
|
|
420
|
+
if (bucket)
|
|
421
|
+
bucket.push(claim);
|
|
422
|
+
else
|
|
423
|
+
bySeller.set(claim.sellerAddress, [claim]);
|
|
424
|
+
if ((i + 1) % RESYNC_BATCH_SIZE === 0 && i + 1 < claims.length) {
|
|
425
|
+
await yieldToEventLoop();
|
|
31
426
|
}
|
|
427
|
+
}
|
|
428
|
+
// size is derived from the exact content string we just validated
|
|
429
|
+
// (Buffer.byteLength), not a separate statSync call — that would open a
|
|
430
|
+
// gap where a write between the read and the stat could make the marker
|
|
431
|
+
// disagree with what we actually parsed. This way the two can never
|
|
432
|
+
// disagree by construction.
|
|
433
|
+
const entry = { size: Buffer.byteLength(content, "utf8"), claims, claimIdSet, bySeller };
|
|
434
|
+
cacheByPath.set(file, entry);
|
|
435
|
+
return entry;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Returns a cache guaranteed fresh as of the moment this is called: if the
|
|
439
|
+
* on-disk file's byte size still matches what the cache was built from,
|
|
440
|
+
* returns it as-is (no disk read at all beyond the one cheap statSync).
|
|
441
|
+
* Otherwise resyncs from disk first (see resyncFromDisk()).
|
|
442
|
+
*/
|
|
443
|
+
async function getFreshCache(file) {
|
|
444
|
+
let diskSize;
|
|
445
|
+
try {
|
|
446
|
+
diskSize = statSync(file).size;
|
|
447
|
+
}
|
|
448
|
+
catch (e) {
|
|
449
|
+
// Same explicit-ENOENT-only reasoning as resyncFromDisk()'s catch above
|
|
450
|
+
// (FIFTH FIX) — a non-ENOENT statSync error is a real problem, not an
|
|
451
|
+
// empty ledger, and must not be swallowed into diskSize=0.
|
|
452
|
+
if (e.code !== "ENOENT")
|
|
453
|
+
throw e;
|
|
454
|
+
diskSize = 0; // file does not exist yet
|
|
455
|
+
}
|
|
456
|
+
const cached = cacheByPath.get(file);
|
|
457
|
+
if (cached && cached.size === diskSize) {
|
|
458
|
+
return cached;
|
|
459
|
+
}
|
|
460
|
+
// KNOWN LIMITATION (documented 2026-08-31, not fixed): this join does not
|
|
461
|
+
// re-check the in-flight resync's eventual `.size` against the `diskSize`
|
|
462
|
+
// measured above. If that resync was already snapshotting disk (via
|
|
463
|
+
// resyncFromDisk()'s own readFileSync) before an external, lock-bypassing
|
|
464
|
+
// writer appended a claim, and this call's own statSync happened to run
|
|
465
|
+
// after that external write, the joined resync can resolve to a cache
|
|
466
|
+
// that is already stale relative to what this caller just observed on
|
|
467
|
+
// disk. In appendClaim()'s case that means the duplicate-claimId check
|
|
468
|
+
// could pass against a claimId the external writer just added, producing
|
|
469
|
+
// a real duplicate line in the ledger. Reproduced 3/3 in adversarial
|
|
470
|
+
// testing. Low real-world severity today: this package's actual
|
|
471
|
+
// deployment (a single long-lived process holding the lockfile for every
|
|
472
|
+
// write) never has a second, lock-bypassing writer racing a resync in
|
|
473
|
+
// this exact window; the gap only opens for a future deployment shape
|
|
474
|
+
// (e.g. multiple processes/hosts sharing one CAPACITY_ATTEST_DATA_DIR,
|
|
475
|
+
// with something other than this package's own appendClaim() writing to
|
|
476
|
+
// the file directly). Fix direction if this is ever revisited: re-check
|
|
477
|
+
// `inFlight`'s resolved `.size` against `diskSize` after joining, and
|
|
478
|
+
// loop to a fresh resyncFromDisk() call if they still disagree.
|
|
479
|
+
const inFlight = pendingResyncs.get(file);
|
|
480
|
+
if (inFlight)
|
|
481
|
+
return inFlight;
|
|
482
|
+
const resync = resyncFromDisk(file).finally(() => {
|
|
483
|
+
pendingResyncs.delete(file);
|
|
32
484
|
});
|
|
485
|
+
pendingResyncs.set(file, resync);
|
|
486
|
+
return resync;
|
|
33
487
|
}
|
|
34
488
|
/**
|
|
35
489
|
* Append one already-verified claim to the ledger. Throws if a claim with
|
|
36
490
|
* the same claimId was already recorded.
|
|
37
491
|
*/
|
|
38
|
-
export function appendClaim(claim) {
|
|
39
|
-
const existing = readClaims();
|
|
40
|
-
if (existing.some((c) => c.claimId.toLowerCase() === claim.claimId.toLowerCase())) {
|
|
41
|
-
throw new Error(`claim_already_recorded: ${claim.claimId}`);
|
|
42
|
-
}
|
|
492
|
+
export async function appendClaim(claim) {
|
|
43
493
|
ensureDataDir();
|
|
44
|
-
|
|
45
|
-
|
|
494
|
+
const file = claimsFile();
|
|
495
|
+
const lock = lockFile();
|
|
496
|
+
await acquireLock(lock);
|
|
497
|
+
try {
|
|
498
|
+
// Freshness check happens AFTER acquiring the lock — see this file's
|
|
499
|
+
// header comment for why that ordering, not the reverse, is what keeps
|
|
500
|
+
// the duplicate-claimId guarantee intact across processes.
|
|
501
|
+
const cache = await getFreshCache(file);
|
|
502
|
+
const claimIdLower = claim.claimId.toLowerCase();
|
|
503
|
+
if (cache.claimIdSet.has(claimIdLower)) {
|
|
504
|
+
throw new Error(`claim_already_recorded: ${claim.claimId}`);
|
|
505
|
+
}
|
|
506
|
+
// FOURTH FIX (2026-08-31, same day, found by adversarial verification of
|
|
507
|
+
// the THIRD FIX above): this used to re-`statSync(file).size` right
|
|
508
|
+
// after the write below, on the theory that "exact by construction" was
|
|
509
|
+
// safer than hand-computing a delta. That reasoning was backwards: a
|
|
510
|
+
// statSync reads the file's CURRENT combined size, so if any OTHER
|
|
511
|
+
// writer — even one that doesn't respect this module's own lockfile,
|
|
512
|
+
// e.g. a second independent process doing a raw fs.appendFileSync, which
|
|
513
|
+
// this whole lock exists to defend against for the on-disk data but was
|
|
514
|
+
// never guarding OUR in-memory size marker — wrote to the same file in
|
|
515
|
+
// the narrow window between our appendFileSync and that statSync, the
|
|
516
|
+
// cache's `size` would end up matching the file's new COMBINED size
|
|
517
|
+
// while cache.claims/claimIdSet only reflect OUR write. The next
|
|
518
|
+
// getFreshCache() would then see size-matches-disk and trust a cache
|
|
519
|
+
// that is silently missing the other writer's claim — permanently,
|
|
520
|
+
// until some unrelated write happens to invalidate it again. Reproduced
|
|
521
|
+
// 4/4 runs: a duplicate claimId got accepted (should have been
|
|
522
|
+
// rejected), and claimsForSeller()/allClaims() silently omitted the
|
|
523
|
+
// externally-written claim. Fixed by computing the new size as a DELTA —
|
|
524
|
+
// old cached size + exactly the bytes of the line we ourselves just
|
|
525
|
+
// wrote — which by construction cannot be perturbed by anyone else's
|
|
526
|
+
// write landing in that window, matching resyncFromDisk()'s own
|
|
527
|
+
// Buffer.byteLength(content, "utf8") convention for what "size" means.
|
|
528
|
+
const serializedLine = JSON.stringify(claim) + "\n";
|
|
529
|
+
appendFileSync(file, serializedLine);
|
|
530
|
+
// Update the cache in place to reflect our own write, synchronously and
|
|
531
|
+
// with no `await` anywhere in this block — that's what guarantees no
|
|
532
|
+
// other same-process caller can observe a half-updated cache (JS never
|
|
533
|
+
// preempts a synchronous stretch of code). This keeps OUR process's
|
|
534
|
+
// cache warm for the next call without forcing an immediate resync, and
|
|
535
|
+
// is why repeated appendClaim()/claimsForSeller() calls from the same
|
|
536
|
+
// long-lived process stay cheap even as the ledger grows. Insertion
|
|
537
|
+
// (not push) keeps claims/bySeller sorted by timestamp at all times —
|
|
538
|
+
// see sortedInsertIndex()'s comment for why that matters.
|
|
539
|
+
const ts = timestampMs(claim);
|
|
540
|
+
cache.claims.splice(sortedInsertIndex(cache.claims, ts), 0, claim);
|
|
541
|
+
cache.claimIdSet.add(claimIdLower);
|
|
542
|
+
const bucket = cache.bySeller.get(claim.sellerAddress);
|
|
543
|
+
if (bucket)
|
|
544
|
+
bucket.splice(sortedInsertIndex(bucket, ts), 0, claim);
|
|
545
|
+
else
|
|
546
|
+
cache.bySeller.set(claim.sellerAddress, [claim]);
|
|
547
|
+
cache.size = cache.size + Buffer.byteLength(serializedLine, "utf8");
|
|
548
|
+
return claim;
|
|
549
|
+
}
|
|
550
|
+
finally {
|
|
551
|
+
releaseLock(lock);
|
|
552
|
+
}
|
|
46
553
|
}
|
|
47
554
|
/** All claims recorded against one seller, oldest first. */
|
|
48
|
-
export function claimsForSeller(sellerAddress) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
555
|
+
export async function claimsForSeller(sellerAddress) {
|
|
556
|
+
const cache = await getFreshCache(claimsFile());
|
|
557
|
+
const bucket = cache.bySeller.get(sellerAddress.toLowerCase()) ?? [];
|
|
558
|
+
// bucket is already sorted (see LedgerCache's invariant) — just copy it,
|
|
559
|
+
// so a caller mutating the returned array can't corrupt the shared cache.
|
|
560
|
+
// No sort, no Date.parse, at call time: this is the whole point of the
|
|
561
|
+
// fix — cost proportional to the RESULT size, not the ledger's total size.
|
|
562
|
+
return [...bucket];
|
|
52
563
|
}
|
|
53
564
|
/** Every claim in the ledger, oldest first. Mainly useful for tests/inspection. */
|
|
54
|
-
export function allClaims() {
|
|
55
|
-
|
|
565
|
+
export async function allClaims() {
|
|
566
|
+
const cache = await getFreshCache(claimsFile());
|
|
567
|
+
return [...cache.claims];
|
|
56
568
|
}
|
|
57
569
|
//# sourceMappingURL=ledger.js.map
|
package/dist/ledger.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ledger.js","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,EAAE;AACF,4EAA4E;AAC5E,qEAAqE;AACrE,yEAAyE;AACzE,2EAA2E;AAC3E,qEAAqE;AACrE,uBAAuB;AAEvB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACnE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,mBAAmB,EAAsB,MAAM,aAAa,CAAC;AAEtE,SAAS,UAAU;IACjB,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,cAAc,CAAC,CAAC;AACzC,CAAC;AAED,SAAS,UAAU;IACjB,MAAM,IAAI,GAAG,UAAU,EAAE,CAAC;IAC1B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC;SAC9B,KAAK,CAAC,IAAI,CAAC;SACX,MAAM,CAAC,OAAO,CAAC;SACf,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QAChB,IAAI,CAAC;YACH,OAAO,CAAC,mBAAmB,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC;YACP,qEAAqE;YACrE,mCAAmC;YACnC,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,KAAoB;IAC9C,MAAM,QAAQ,GAAG,UAAU,EAAE,CAAC;IAC9B,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;QAClF,MAAM,IAAI,KAAK,CAAC,2BAA2B,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IAC9D,CAAC;IACD,aAAa,EAAE,CAAC;IAChB,cAAc,CAAC,UAAU,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC;IAC3D,OAAO,KAAK,CAAC;AACf,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,eAAe,CAAC,aAAqB;IACnD,OAAO,UAAU,EAAE;SAChB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,WAAW,EAAE,KAAK,aAAa,CAAC,WAAW,EAAE,CAAC;SAC5E,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;AACvE,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,SAAS;IACvB,OAAO,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;AACxF,CAAC"}
|
|
1
|
+
{"version":3,"file":"ledger.js","sourceRoot":"","sources":["../src/ledger.ts"],"names":[],"mappings":"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"}
|
package/dist/schema.d.ts
CHANGED
|
@@ -3,14 +3,9 @@ export declare const ASSET_TYPES: readonly ["gpu-hours", "storage", "api-credits
|
|
|
3
3
|
export type AssetType = (typeof ASSET_TYPES)[number];
|
|
4
4
|
export declare const DELIVERED_VALUES: readonly ["yes", "no", "partial"];
|
|
5
5
|
export type Delivered = (typeof DELIVERED_VALUES)[number];
|
|
6
|
-
/**
|
|
7
|
-
* The part of a claim that is hashed to produce claimId and that gets
|
|
8
|
-
* signed. Everything in here is chosen by the buyer at claim-creation time;
|
|
9
|
-
* claimId and signature (in DeliveryClaimSchema below) are derived from it.
|
|
10
|
-
*/
|
|
11
6
|
export declare const ClaimContentSchema: z.ZodObject<{
|
|
12
|
-
sellerAddress: z.ZodString
|
|
13
|
-
buyerAddress: z.ZodString
|
|
7
|
+
sellerAddress: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
8
|
+
buyerAddress: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
14
9
|
assetType: z.ZodEnum<{
|
|
15
10
|
"gpu-hours": "gpu-hours";
|
|
16
11
|
storage: "storage";
|
|
@@ -30,8 +25,8 @@ export declare const ClaimContentSchema: z.ZodObject<{
|
|
|
30
25
|
export type ClaimContent = z.infer<typeof ClaimContentSchema>;
|
|
31
26
|
/** The full, stored, signed claim — ClaimContent plus its content-address and signature. */
|
|
32
27
|
export declare const DeliveryClaimSchema: z.ZodObject<{
|
|
33
|
-
sellerAddress: z.ZodString
|
|
34
|
-
buyerAddress: z.ZodString
|
|
28
|
+
sellerAddress: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
29
|
+
buyerAddress: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
35
30
|
assetType: z.ZodEnum<{
|
|
36
31
|
"gpu-hours": "gpu-hours";
|
|
37
32
|
storage: "storage";
|
package/dist/schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAG5B,eAAO,MAAM,WAAW,+DAAgE,CAAC;AACzF,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,eAAO,MAAM,gBAAgB,mCAAoC,CAAC;AAClE,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAG5B,eAAO,MAAM,WAAW,+DAAgE,CAAC;AACzF,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,eAAO,MAAM,gBAAgB,mCAAoC,CAAC;AAClE,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAoD1D,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;iBA6C7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,4FAA4F;AAC5F,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;iBAS9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEnD;AA+BD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAI5D"}
|
package/dist/schema.js
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
// actually arrived." It is deliberately NOT a score, rating, or judgment —
|
|
6
6
|
// see README.md "Wat dit NIET is".
|
|
7
7
|
//
|
|
8
|
-
// GUARDRAIL (
|
|
9
|
-
// kinds. Do not add anything that looks like a financial instrument
|
|
10
|
-
// loan, yield, interest-bearing balance, invoice-financing) to this
|
|
11
|
-
// see the "
|
|
12
|
-
// value even smells like that, leave it out.
|
|
8
|
+
// GUARDRAIL (design boundary): assetType is a closed enum of physical/compute
|
|
9
|
+
// capacity kinds. Do not add anything that looks like a financial instrument
|
|
10
|
+
// (credit, loan, yield, interest-bearing balance, invoice-financing) to this
|
|
11
|
+
// file — see the "DESIGN GUARDRAILS" section of the project brief. If a field
|
|
12
|
+
// or enum value even smells like that, leave it out.
|
|
13
13
|
import * as z from "zod/v4";
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
15
15
|
export const ASSET_TYPES = ["gpu-hours", "storage", "api-credits", "bandwidth"];
|
|
@@ -25,18 +25,76 @@ const SIGNATURE_RE = /^0x[0-9a-fA-F]{130}$/;
|
|
|
25
25
|
* signed. Everything in here is chosen by the buyer at claim-creation time;
|
|
26
26
|
* claimId and signature (in DeliveryClaimSchema below) are derived from it.
|
|
27
27
|
*/
|
|
28
|
+
// Guardrails against two verified bugs (2026-08-31 adversarial audit):
|
|
29
|
+
// - MAX_PROMISED_SPEC_STRING_LENGTH / MAX_SETTLEMENT_REF_LENGTH: without a
|
|
30
|
+
// bound, a multi-megabyte string was accepted in ~6ms, letting any caller
|
|
31
|
+
// grow the append-only ledger and every future read's memory footprint
|
|
32
|
+
// without limit.
|
|
33
|
+
// - MAX_PROMISED_SPEC_JSON_BYTES: the object branch of promisedSpec has no
|
|
34
|
+
// own length check, so it needs an explicit serialized-size cap instead.
|
|
35
|
+
const MAX_PROMISED_SPEC_STRING_LENGTH = 4_000;
|
|
36
|
+
const MAX_SETTLEMENT_REF_LENGTH = 512;
|
|
37
|
+
const MAX_PROMISED_SPEC_JSON_BYTES = 8_000;
|
|
38
|
+
// Shared with sortKeysDeep() below: how deep promisedSpec may nest.
|
|
39
|
+
const MAX_DEPTH = 32;
|
|
40
|
+
// Iterative (non-recursive) depth check for the object branch of
|
|
41
|
+
// promisedSpec, used at schema-parse time. This deliberately does NOT use
|
|
42
|
+
// recursion, and deliberately runs BEFORE any JSON.stringify of the raw
|
|
43
|
+
// input: an earlier version of this guard called JSON.stringify(v) directly
|
|
44
|
+
// inside the refine below to enforce MAX_PROMISED_SPEC_JSON_BYTES, and that
|
|
45
|
+
// call is itself a recursive walk that blew the call stack on the exact
|
|
46
|
+
// pathologically-deep input it was meant to reject (caught by this
|
|
47
|
+
// package's own fuzz tests). Checking depth first, with an explicit stack
|
|
48
|
+
// instead of language-level recursion, closes that gap for good.
|
|
49
|
+
function exceedsMaxDepth(value, maxDepth) {
|
|
50
|
+
const stack = [{ v: value, d: 0 }];
|
|
51
|
+
while (stack.length > 0) {
|
|
52
|
+
const { v, d } = stack.pop();
|
|
53
|
+
if (d > maxDepth)
|
|
54
|
+
return true;
|
|
55
|
+
if (Array.isArray(v)) {
|
|
56
|
+
for (const item of v)
|
|
57
|
+
stack.push({ v: item, d: d + 1 });
|
|
58
|
+
}
|
|
59
|
+
else if (v !== null && typeof v === "object") {
|
|
60
|
+
for (const key of Object.keys(v)) {
|
|
61
|
+
stack.push({ v: v[key], d: d + 1 });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
28
67
|
export const ClaimContentSchema = z.object({
|
|
68
|
+
// .transform(toLowerCase): a fuzz-and-benchmark audit found that
|
|
69
|
+
// computeClaimId() hashed addresses exactly as submitted while every other
|
|
70
|
+
// address comparison in this codebase (signature recovery, seller
|
|
71
|
+
// filtering) was already case-insensitive. That let the same real claim be
|
|
72
|
+
// resubmitted under a different claimId by only re-casing a hex letter,
|
|
73
|
+
// defeating appendClaim()'s claimId-based duplicate rejection. Normalizing
|
|
74
|
+
// here, once, before anything is hashed or compared, closes that gap for
|
|
75
|
+
// every consumer of this schema.
|
|
29
76
|
sellerAddress: z
|
|
30
77
|
.string()
|
|
31
78
|
.regex(ETH_ADDRESS_RE, "sellerAddress must be a 0x-prefixed 20-byte address")
|
|
79
|
+
.transform((v) => v.toLowerCase())
|
|
32
80
|
.describe("0x address of the agent/service that was paid and was supposed to deliver"),
|
|
33
81
|
buyerAddress: z
|
|
34
82
|
.string()
|
|
35
83
|
.regex(ETH_ADDRESS_RE, "buyerAddress must be a 0x-prefixed 20-byte address")
|
|
84
|
+
.transform((v) => v.toLowerCase())
|
|
36
85
|
.describe("0x address of the paying agent — must match the address recovered from `signature`"),
|
|
37
86
|
assetType: z.enum(ASSET_TYPES).describe("What kind of capacity this claim is about"),
|
|
38
87
|
promisedSpec: z
|
|
39
|
-
.union([
|
|
88
|
+
.union([
|
|
89
|
+
z.string().min(1).max(MAX_PROMISED_SPEC_STRING_LENGTH),
|
|
90
|
+
z.record(z.string(), z.unknown()).refine((v) => {
|
|
91
|
+
if (exceedsMaxDepth(v, MAX_DEPTH))
|
|
92
|
+
return false;
|
|
93
|
+
// Only safe to JSON.stringify for the size check once depth is
|
|
94
|
+
// bounded — see exceedsMaxDepth's comment above.
|
|
95
|
+
return Buffer.byteLength(JSON.stringify(v), "utf8") <= MAX_PROMISED_SPEC_JSON_BYTES;
|
|
96
|
+
}, `promisedSpec object exceeds the allowed nesting depth (${MAX_DEPTH}) or size (${MAX_PROMISED_SPEC_JSON_BYTES} bytes)`),
|
|
97
|
+
])
|
|
40
98
|
.describe("What the seller promised to deliver — free text or a structured object"),
|
|
41
99
|
delivered: z.enum(DELIVERED_VALUES).describe("Whether what was promised actually arrived"),
|
|
42
100
|
evidenceHash: z
|
|
@@ -46,6 +104,7 @@ export const ClaimContentSchema = z.object({
|
|
|
46
104
|
settlementRef: z
|
|
47
105
|
.string()
|
|
48
106
|
.min(1, "settlementRef is required (x402 payment reference or on-chain tx hash)")
|
|
107
|
+
.max(MAX_SETTLEMENT_REF_LENGTH)
|
|
49
108
|
.describe("x402 payment reference or on-chain tx hash for the settlement this claim is about"),
|
|
50
109
|
timestamp: z
|
|
51
110
|
.string()
|
|
@@ -70,16 +129,32 @@ export const DeliveryClaimSchema = ClaimContentSchema.extend({
|
|
|
70
129
|
* sorted.
|
|
71
130
|
*/
|
|
72
131
|
export function canonicalize(value) {
|
|
73
|
-
return JSON.stringify(sortKeysDeep(value));
|
|
132
|
+
return JSON.stringify(sortKeysDeep(value, 0));
|
|
74
133
|
}
|
|
75
|
-
|
|
134
|
+
// A fuzz audit found that a promisedSpec nested a few thousand levels deep
|
|
135
|
+
// (no valid signature required — this runs before signature verification)
|
|
136
|
+
// crashes the process with an uncaught RangeError instead of the normal
|
|
137
|
+
// {ok:false, reason} response every other invalid claim gets. MAX_DEPTH
|
|
138
|
+
// (declared above, shared with the schema-level guard on promisedSpec)
|
|
139
|
+
// turns that crash into a clean, catchable Error well before the engine's
|
|
140
|
+
// own call-stack limit, however it's shaped (objects or arrays) — kept here
|
|
141
|
+
// too as defense-in-depth in case computeClaimId is ever called on content
|
|
142
|
+
// that bypassed schema validation.
|
|
143
|
+
function sortKeysDeep(value, depth) {
|
|
144
|
+
if (depth > MAX_DEPTH) {
|
|
145
|
+
throw new Error(`promisedSpec nesting exceeds max depth of ${MAX_DEPTH}`);
|
|
146
|
+
}
|
|
76
147
|
if (Array.isArray(value))
|
|
77
|
-
return value.map(sortKeysDeep);
|
|
148
|
+
return value.map((v) => sortKeysDeep(v, depth + 1));
|
|
78
149
|
if (value !== null && typeof value === "object") {
|
|
79
150
|
const input = value;
|
|
80
|
-
|
|
151
|
+
// Object.create(null) has no inherited `__proto__` accessor, so a key
|
|
152
|
+
// literally named "__proto__" becomes a normal own property instead of
|
|
153
|
+
// silently reassigning out's prototype (and vanishing from the hashed
|
|
154
|
+
// output) the way `out["__proto__"] = ...` would on a plain {} object.
|
|
155
|
+
const out = Object.create(null);
|
|
81
156
|
for (const key of Object.keys(input).sort()) {
|
|
82
|
-
out[key] = sortKeysDeep(input[key]);
|
|
157
|
+
out[key] = sortKeysDeep(input[key], depth + 1);
|
|
83
158
|
}
|
|
84
159
|
return out;
|
|
85
160
|
}
|
package/dist/schema.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,EAAE;AACF,2EAA2E;AAC3E,yEAAyE;AACzE,2EAA2E;AAC3E,mCAAmC;AACnC,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/tools.d.ts
CHANGED
|
@@ -10,8 +10,14 @@ export type RecordDeliveryResult = {
|
|
|
10
10
|
* Validate the claim's schema, verify its signature (must recover to
|
|
11
11
|
* buyerAddress), and persist it to the append-only ledger. This is the
|
|
12
12
|
* logic behind the `record_delivery` MCP tool.
|
|
13
|
+
*
|
|
14
|
+
* Async because appendClaim() is: its lock-contention retry awaits a real
|
|
15
|
+
* timer instead of busy-waiting, so this function must be awaited by every
|
|
16
|
+
* caller — an un-awaited call here would let the caller's next statement
|
|
17
|
+
* run before the claim is actually locked/checked/written, reordering the
|
|
18
|
+
* exact read-check-write sequence the lock exists to make atomic.
|
|
13
19
|
*/
|
|
14
|
-
export declare function recordDelivery(input: unknown): RecordDeliveryResult
|
|
20
|
+
export declare function recordDelivery(input: unknown): Promise<RecordDeliveryResult>;
|
|
15
21
|
export interface DeliveryHistoryResult {
|
|
16
22
|
sellerAddress: string;
|
|
17
23
|
count: number;
|
|
@@ -22,6 +28,8 @@ export interface DeliveryHistoryResult {
|
|
|
22
28
|
* oldest first. Purely factual — no aggregate score, rating, or reputation
|
|
23
29
|
* judgment is computed here; see README.md "Wat dit NIET is". This is the
|
|
24
30
|
* logic behind the `get_delivery_history` MCP tool.
|
|
31
|
+
*
|
|
32
|
+
* Async to match claimsForSeller()'s now-async signature (see ledger.ts).
|
|
25
33
|
*/
|
|
26
|
-
export declare function getDeliveryHistory(sellerAddress: string): DeliveryHistoryResult
|
|
34
|
+
export declare function getDeliveryHistory(sellerAddress: string): Promise<DeliveryHistoryResult>;
|
|
27
35
|
//# sourceMappingURL=tools.d.ts.map
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAMA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAItE,MAAM,MAAM,oBAAoB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjG
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAMA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAItE,MAAM,MAAM,oBAAoB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjG;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA4BlF;AAED,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAG9F"}
|
package/dist/tools.js
CHANGED
|
@@ -10,19 +10,35 @@ import { appendClaim, claimsForSeller } from "./ledger.js";
|
|
|
10
10
|
* Validate the claim's schema, verify its signature (must recover to
|
|
11
11
|
* buyerAddress), and persist it to the append-only ledger. This is the
|
|
12
12
|
* logic behind the `record_delivery` MCP tool.
|
|
13
|
+
*
|
|
14
|
+
* Async because appendClaim() is: its lock-contention retry awaits a real
|
|
15
|
+
* timer instead of busy-waiting, so this function must be awaited by every
|
|
16
|
+
* caller — an un-awaited call here would let the caller's next statement
|
|
17
|
+
* run before the claim is actually locked/checked/written, reordering the
|
|
18
|
+
* exact read-check-write sequence the lock exists to make atomic.
|
|
13
19
|
*/
|
|
14
|
-
export function recordDelivery(input) {
|
|
20
|
+
export async function recordDelivery(input) {
|
|
15
21
|
const parsed = DeliveryClaimSchema.safeParse(input);
|
|
16
22
|
if (!parsed.success) {
|
|
17
23
|
return { ok: false, reason: `invalid_claim: ${parsed.error.issues.map((i) => i.message).join("; ")}` };
|
|
18
24
|
}
|
|
19
25
|
const claim = parsed.data;
|
|
20
|
-
|
|
26
|
+
let verdict;
|
|
27
|
+
try {
|
|
28
|
+
verdict = verifyClaim(claim);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
// computeClaimId() (called inside verifyClaim, before any signature is
|
|
32
|
+
// even checked) can throw on pathological input, e.g. promisedSpec
|
|
33
|
+
// nested past schema.ts's MAX_DEPTH. That must degrade to the tool's
|
|
34
|
+
// normal error contract, not an uncaught exception.
|
|
35
|
+
return { ok: false, reason: `invalid_claim: ${e.message}` };
|
|
36
|
+
}
|
|
21
37
|
if (!verdict.ok) {
|
|
22
38
|
return { ok: false, reason: `signature_invalid: ${verdict.reason}` };
|
|
23
39
|
}
|
|
24
40
|
try {
|
|
25
|
-
appendClaim(claim);
|
|
41
|
+
await appendClaim(claim);
|
|
26
42
|
}
|
|
27
43
|
catch (e) {
|
|
28
44
|
return { ok: false, reason: e.message };
|
|
@@ -34,9 +50,11 @@ export function recordDelivery(input) {
|
|
|
34
50
|
* oldest first. Purely factual — no aggregate score, rating, or reputation
|
|
35
51
|
* judgment is computed here; see README.md "Wat dit NIET is". This is the
|
|
36
52
|
* logic behind the `get_delivery_history` MCP tool.
|
|
53
|
+
*
|
|
54
|
+
* Async to match claimsForSeller()'s now-async signature (see ledger.ts).
|
|
37
55
|
*/
|
|
38
|
-
export function getDeliveryHistory(sellerAddress) {
|
|
39
|
-
const claims = claimsForSeller(sellerAddress);
|
|
56
|
+
export async function getDeliveryHistory(sellerAddress) {
|
|
57
|
+
const claims = await claimsForSeller(sellerAddress);
|
|
40
58
|
return { sellerAddress, count: claims.length, claims };
|
|
41
59
|
}
|
|
42
60
|
//# sourceMappingURL=tools.js.map
|
package/dist/tools.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,uEAAuE;AACvE,yEAAyE;AACzE,0EAA0E;AAC1E,4BAA4B;AAE5B,OAAO,EAAE,mBAAmB,EAAsB,MAAM,aAAa,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAI3D
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,uEAAuE;AACvE,yEAAyE;AACzE,0EAA0E;AAC1E,4BAA4B;AAE5B,OAAO,EAAE,mBAAmB,EAAsB,MAAM,aAAa,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAI3D;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,KAAc;IACjD,MAAM,MAAM,GAAG,mBAAmB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACpD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;IACzG,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC;IAC1B,IAAI,OAAuC,CAAC;IAC5C,IAAI,CAAC;QACH,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,uEAAuE;QACvE,mEAAmE;QACnE,qEAAqE;QACrE,oDAAoD;QACpD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAmB,CAAW,CAAC,OAAO,EAAE,EAAE,CAAC;IACzE,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;QAChB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,sBAAsB,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;IACvE,CAAC;IAED,IAAI,CAAC;QACH,MAAM,WAAW,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAG,CAAW,CAAC,OAAO,EAAE,CAAC;IACrD,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;AAC9C,CAAC;AAQD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,aAAqB;IAC5D,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,aAAa,CAAC,CAAC;IACpD,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;AACzD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "capacity-attest",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "MCP server for capacity-delivery attestations: after an x402 payment for compute capacity (GPU-hours, storage, API credits, bandwidth), the paying agent leaves a signed, factual claim of what was actually delivered
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "MCP server for capacity-delivery attestations: after an x402 payment for compute capacity (GPU-hours, storage, API credits, bandwidth), the paying agent leaves a signed, factual claim of what was actually delivered, so other agents can check a seller's delivery history before paying. No reputation score, no judgment, no financial instrument.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
7
7
|
"model-context-protocol",
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
"homepage": "https://tokenizen.nl",
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "https://github.com/holistis/tokenizen.git",
|
|
21
|
+
"url": "git+https://github.com/holistis/tokenizen.git",
|
|
22
22
|
"directory": "packages/capacity-attest"
|
|
23
23
|
},
|
|
24
24
|
"mcpName": "io.github.holistis/capacity-attest",
|
|
25
25
|
"type": "module",
|
|
26
26
|
"main": "./dist/index.js",
|
|
27
27
|
"bin": {
|
|
28
|
-
"capacity-attest": "
|
|
28
|
+
"capacity-attest": "dist/index.js"
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"dist",
|
|
@@ -50,6 +50,6 @@
|
|
|
50
50
|
"@types/node": "^22.10.5",
|
|
51
51
|
"tsx": "^4.19.2",
|
|
52
52
|
"typescript": "^5.7.3",
|
|
53
|
-
"vitest": "^
|
|
53
|
+
"vitest": "^4.1.11"
|
|
54
54
|
}
|
|
55
55
|
}
|