fix(ocr): review pass — min-ada floor regression test, auth-before-parse, failure-path reconciliation, disclosure label coverage

Fixes 2 critical, 2 important, and 5 minor findings from review of 069edcb:

- C1: add a happy-path test that reaches postOcrEscrow.ts's escrowLockFloor
  call and asserts the locked amount; verified it fails against a bare
  economicTotal and passes against the real code.
- C2: gate /v1/ocr/extract (auth + rate limits) before the 16mb body parser
  so an unauthenticated caller can no longer force a full buffer/JSON.parse.
- I2: hoist result/used/escrowRef in the OCR one-shot handler so the failure
  path records escrow_ref/supplier/cost when an escrow was actually posted,
  and emit an ocr_failed structured log line mirroring ocr_settled.
- I3: export venueAttribution and cover its sanitization directly; add a
  stubbed happy-path route test asserting the first-party-brokered
  disclosure label reaches the response.
- Minor: drop unused hfWeightsSource end to end, rename a misleading test,
  make the OCR capability template's pinning language conditional on the
  self-hosted upstream, tighten the /v1/ocr path-prefix guard, and make
  preflight's required_lovelace use the same escrowLockFloor the builder
  locks (exported from the shared tx barrel for gateway use).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
FilipBl4gojevic
2026-08-10 17:12:12 +02:00
parent 069edcb076
commit 2b2295b7ed
13 changed files with 326 additions and 51 deletions

View File

@@ -138,7 +138,7 @@ describe("POST /v1/ocr/extract — header + body validation", () => {
});
describe("POST /v1/ocr/extract — revision-probe gate", () => {
it("503 revision_probe_failed when the probe cannot reach the runtime", async () => {
it("503 advert_unavailable — chain validation runs before the revision probe fires", async () => {
// Advert/escrow would resolve AFTER the probe? No — probe runs at step
// 7.5, after chain checks. Seeding a full escrow in the mock provider is
// the chat-route suite's job; here we assert the config gate wiring by

View File

@@ -24,6 +24,13 @@ import {
} from "../../packages/shared/src/tx/index.js";
import { canonicalize } from "../../packages/shared/src/cbor/canonical.js";
import { MockChainProvider } from "../../packages/shared/src/chain/MockChainProvider.js";
import { encodeAdvertDatum } from "../../packages/shared/src/cbor/AdvertDatum.js";
import { decodeEscrowDatum } from "../../packages/shared/src/cbor/EscrowDatum.js";
import { escrowLockFloor } from "../../packages/shared/src/tx/internal/minAdaFloor.js";
import type { AdvertDatum } from "../../packages/shared/src/cbor/types.js";
import type { Utxo, OutputReference } from "../../packages/shared/src/chain/ChainProvider.js";
import { buildBuyerWalletKey } from "../fixtures/buyer-side/wallet-keys.js";
import { buildSupplierWalletKey } from "../fixtures/supplier-side/wallet-keys.js";
const IMG = Buffer.from("not really a png but bytes are bytes").toString("base64");
@@ -120,3 +127,74 @@ describe("buildPostOcrEscrowTx — validation precedes chain access", () => {
})).rejects.toMatchObject({ reason: "advert ref not on chain" });
});
});
describe("buildPostOcrEscrowTx — happy path locks the min-ada floor (C1 regression)", () => {
// Guards packages/shared/src/tx/escrow/postOcrEscrow.ts:205
// (`escrowLockFloor(escrowDatum, economicTotal)`). Without that call the
// escrow locks only the raw economic total, the Submit continuing output
// gets bumped to min-ada, value_equal fails, and the validator errors with
// an empty trace — the 2026-08-07 mainnet incident. Neither existing
// `buildPostOcrEscrowTx` test above reaches line 205 (both throw first),
// so this is the only test that would catch a regression back to a bare
// `economicTotal` assignment.
const ADVERT_SCRIPT_ADDRESS = "addr_test1wrqq9qqjzf3uh4w9hm0kqzrpvt60r4ryjp5rjf5epd3nptq7yscm6";
const ADVERT_REF: OutputReference = { txHash: "c".repeat(64), index: 0 };
const PRICE = 200_000n;
const BUYER_BOND = 1_000_000n;
const SUPPLIER_BOND = 1_000_000n;
const ECONOMIC_TOTAL = PRICE + BUYER_BOND + SUPPLIER_BOND; // 2_200_000n
function makeOcrAdvert(): AdvertDatum {
const supplier = buildSupplierWalletKey();
return {
supplier_pkh: supplier.pubKeyHash,
capability_id: "ocr.page.extract.chandra-ocr-2.v1",
model: "chandra-ocr-2",
max_output_tokens: 4096,
max_processing_ms: 60_000,
price_lovelace: PRICE,
supplier_bond_lovelace: SUPPLIER_BOND,
buyer_bond_lovelace: BUYER_BOND,
endpoint_url: "https://supplier.example.com/v1",
detail_uri: "ipfs://Qm000",
detail_hash: "a".repeat(64),
advertised_at: 1_745_500_000_000,
status: "Active",
};
}
function seedOcrAdvert(chain: MockChainProvider): void {
const utxo: Utxo = {
ref: ADVERT_REF,
address: ADVERT_SCRIPT_ADDRESS,
lovelace: 2_000_000n,
assets: {},
datumHex: encodeAdvertDatum(makeOcrAdvert()),
scriptRef: null,
};
chain.seed(utxo);
}
it("locks escrowLockFloor(datum, economicTotal), strictly above the raw economic total", async () => {
const chain = new MockChainProvider();
chain.advanceSlot(1_745_500_000);
seedOcrAdvert(chain);
const buyer = buildBuyerWalletKey();
const result = await buildPostOcrEscrowTx({
chain,
buyerKey: buyer,
advertRef: ADVERT_REF,
request: validRequest(),
payment_lovelace: PRICE,
});
const escrowUtxo = await chain.queryUtxo(result.escrowOutputRef);
expect(escrowUtxo).not.toBeNull();
const datum = decodeEscrowDatum(escrowUtxo!.datumHex!);
const expectedFloor = escrowLockFloor(datum, ECONOMIC_TOTAL);
expect(escrowUtxo!.lovelace).toBe(expectedFloor);
expect(escrowUtxo!.lovelace).toBeGreaterThan(ECONOMIC_TOTAL);
});
});