mtok-relay 0.1.8 → 0.1.10

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.
Files changed (2) hide show
  1. package/dist/mtok-relay.mjs +69 -11
  2. package/package.json +1 -1
@@ -4833,6 +4833,8 @@ function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS
4833
4833
  }
4834
4834
  return {
4835
4835
  durable,
4836
+ retentionMs,
4837
+ // the window this store is authoritative for; the runtime also uses it as the on-chain draw-age bound (#580).
4836
4838
  has(key) {
4837
4839
  return map.has(key);
4838
4840
  },
@@ -4930,7 +4932,9 @@ function createOnchainVerifier({
4930
4932
  // a few times before giving up. Bounded so a genuinely-missing tx still fails fast.
4931
4933
  receiptRetries = 3,
4932
4934
  receiptRetryMs = 700,
4933
- sleepImpl = (ms) => new Promise((r) => setTimeout(r, ms))
4935
+ sleepImpl = (ms) => new Promise((r) => setTimeout(r, ms)),
4936
+ nowMs = () => Date.now()
4937
+ // injectable clock for the optional draw-age guard (verifyDrawPaid maxPaidAgeMs).
4934
4938
  } = {}) {
4935
4939
  const urls = (rpcUrls?.length ? rpcUrls : String(rpcUrl || "").split(",")).map((s) => String(s).trim()).filter(Boolean);
4936
4940
  const configured = Boolean(urls.length && usdcAddress);
@@ -4992,7 +4996,7 @@ function createOnchainVerifier({
4992
4996
  for (let i = 0; i < logs.length; i++) {
4993
4997
  if (consumed && consumed.has(i)) continue;
4994
4998
  const l = logs[i];
4995
- if (lc(l.address) !== usdc || l.topics?.[0] !== TRANSFER_TOPIC || l.topics.length !== 3) continue;
4999
+ if (lc(l.address) !== usdc || lc(l.topics?.[0]) !== TRANSFER_TOPIC || l.topics.length !== 3) continue;
4996
5000
  if (topicToAddress(l.topics[2]) !== want) continue;
4997
5001
  if (wantFrom && topicToAddress(l.topics[1]) !== wantFrom) continue;
4998
5002
  sawRecipient = true;
@@ -5029,22 +5033,51 @@ function createOnchainVerifier({
5029
5033
  requestHash,
5030
5034
  sellerWallet,
5031
5035
  feeRecipient,
5032
- minSellerAtomic = 0n
5036
+ minSellerAtomic = 0n,
5037
+ // Optional draw-age guard (#580). A DrawPaid receipt is a PERMANENT chain fact, so a
5038
+ // buyer who holds the request preimage can replay the same /chunk body long after
5039
+ // paying; if the relay's local redemption record was pruned (retention lapse) or lost
5040
+ // (in-memory restart), served.has() misses and a stale payment buys a fresh inference.
5041
+ // When set, refuse a payment whose block is older than maxPaidAgeMs: a legitimate
5042
+ // first-serve or honest retry happens seconds-to-minutes after payDraw, never days.
5043
+ maxPaidAgeMs
5033
5044
  } = {}) {
5034
5045
  if (!await assertChain()) return { ok: false, reason: "wrong_chain" };
5035
5046
  const contract = lc(contractAddress);
5036
5047
  if (!contract) return { ok: false, reason: "contract_not_configured" };
5037
5048
  const got = await fetchReceipt(txHash);
5038
5049
  if (got.error) return { ok: false, reason: got.error };
5039
- const log = (got.receipt.logs || []).find(
5050
+ const drawLogs = (got.receipt.logs || []).filter(
5040
5051
  (l) => lc(l.address) === contract && isDrawPaidTopic(l.topics?.[0])
5041
5052
  );
5042
- if (!log) return { ok: false, reason: "no_draw_paid_event" };
5043
- let event;
5044
- try {
5045
- event = decodeDrawPaidLog(log);
5046
- } catch {
5047
- return { ok: false, reason: "malformed_draw_paid_event" };
5053
+ if (!drawLogs.length) return { ok: false, reason: "no_draw_paid_event" };
5054
+ let event = null;
5055
+ let matchedLog = null;
5056
+ let sawDecodable = false;
5057
+ for (const l of drawLogs) {
5058
+ let e;
5059
+ try {
5060
+ e = decodeDrawPaidLog(l);
5061
+ } catch {
5062
+ continue;
5063
+ }
5064
+ sawDecodable = true;
5065
+ if (bookingId != null && e.bookingId !== String(bookingId)) continue;
5066
+ if (n != null && e.n !== Number(n)) continue;
5067
+ event = e;
5068
+ matchedLog = l;
5069
+ break;
5070
+ }
5071
+ if (!event) {
5072
+ if (!sawDecodable) return { ok: false, reason: "malformed_draw_paid_event" };
5073
+ for (const l of drawLogs) {
5074
+ try {
5075
+ event = decodeDrawPaidLog(l);
5076
+ matchedLog = l;
5077
+ break;
5078
+ } catch {
5079
+ }
5080
+ }
5048
5081
  }
5049
5082
  if (buyerAgentId != null && event.buyerAgentId !== String(buyerAgentId)) return { ok: false, reason: "buyer_agent_mismatch" };
5050
5083
  if (sellerAgentId != null && event.sellerAgentId !== String(sellerAgentId)) return { ok: false, reason: "seller_agent_mismatch" };
@@ -5054,6 +5087,26 @@ function createOnchainVerifier({
5054
5087
  if (n != null && event.n !== Number(n)) return { ok: false, reason: "draw_n_mismatch" };
5055
5088
  if (requestHash != null && lc(event.requestHash) !== lc(requestHash)) return { ok: false, reason: "request_hash_mismatch" };
5056
5089
  if (BigInt(event.sellerUsdAtomic) < BigInt(minSellerAtomic)) return { ok: false, reason: "amount_too_low" };
5090
+ const maxAge = Number(maxPaidAgeMs);
5091
+ if (Number.isFinite(maxAge) && maxAge > 0) {
5092
+ const bn = matchedLog?.blockNumber;
5093
+ let paidAtMs = null;
5094
+ if (bn != null) {
5095
+ const blockTag = typeof bn === "string" && bn.startsWith("0x") ? bn : "0x" + BigInt(bn).toString(16);
5096
+ for (let i = 0; i <= receiptRetries; i++) {
5097
+ try {
5098
+ const block = await rpc("eth_getBlockByNumber", [blockTag, false]);
5099
+ if (block?.timestamp != null) {
5100
+ paidAtMs = Number(BigInt(block.timestamp)) * 1e3;
5101
+ break;
5102
+ }
5103
+ } catch {
5104
+ }
5105
+ if (i < receiptRetries) await sleepImpl(receiptRetryMs);
5106
+ }
5107
+ }
5108
+ if (paidAtMs != null && nowMs() - paidAtMs > maxAge) return { ok: false, reason: "payment_too_old" };
5109
+ }
5057
5110
  const consumed = /* @__PURE__ */ new Set();
5058
5111
  let sellerTransfer = null;
5059
5112
  if (sellerWallet) {
@@ -5169,7 +5222,12 @@ async function createRelayRuntime(config) {
5169
5222
  n,
5170
5223
  requestHash,
5171
5224
  sellerWallet: config.settlementAddr,
5172
- feeRecipient: platform.feeAddress
5225
+ feeRecipient: platform.feeAddress,
5226
+ // #580: refuse a payment older than the redemption window. served.has() is the
5227
+ // primary one-serve guard, but it's pruned by age and lost on an in-memory restart;
5228
+ // this on-chain age bound closes the re-serve hole those cases open (a stale replay
5229
+ // buying a fresh inference). An honest retry is seconds-to-minutes old, never days.
5230
+ maxPaidAgeMs: served.retentionMs
5173
5231
  });
5174
5232
  } catch (e) {
5175
5233
  return send(res, 402, { error: "payment_unverified", detail: e.message });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtok-relay",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Reference seller relay for mtok.market — accepts on-chain-prepaid chunk draws and serves inference from an upstream you control. Run with: npx mtok-relay --offer <id> --model <id> --upstream <url>.",
5
5
  "type": "module",
6
6
  "bin": {