4. Automated Validation & Rate Limits

Summary: validation happens before PoV. We accept a window only if the payload is canonical JSON, values are plausible, the tuple (device_id, start_ts, end_ts) does not overlap previous windows, and replay/duplicates are blocked with idempotency and rate limits. Anything that fails is rejected or quarantined before attestations.

What this layer does

It protects the pipeline from bad or abusive input. It verifies schema and canonical form, window rules, value sanity, duplicate/replay, and throughput per device_id and operator. Only then do we compute evidenceHash, derive claimId, and move on to attestation.

Validation rules

1

Json

The body must be the already-canonical object. We re-serialize and compare. If it changes, reject as NON_CANONICAL_JSON. Keys like batch_id, device_id, start_ts, end_ts, quantity_wh, nonce are required.

2

Window Integrity

start_ts < end_ts. Duration within policy (example: 900–86400 seconds). No overlaps for the same device_id. If a prior window intersects, reject as OVERLAPPING_WINDOW. Allow idempotent replays only when batch_id and the bytes match exactly.

3

Plausibility

quantity_wh must be ≥ 0 and within device bounds (example: ≤ rated_wh_per_window × 1.15). Flag sudden spikes against rolling median (example: > 150%) for quarantine (QUARANTINED) rather than hard reject.

4

Duplicates & replay

Reject reused nonce for the same device_id (REPLAY_NONCE). Reject repeated batch_id unless payload bytes match (DUPLICATE_BATCH). Reject tuple duplicates on (device_id, start_ts, end_ts) unless idempotent (DUPLICATE_TUPLE).

5

Rate limits

Protect backends and attestors. Example defaults:

• per device_id: 1 window per 60 s sustained; burst 5; hourly max 120 → else RATE_LIMITED

• per operator: sliding window caps by site count; excess goes to a delayed queue

6

Hashing & IDs

After passing checks, compute evidenceHash = SHA-256(utf8(canonical_json)) and derive claimId using the tuple (device_id, start_ts, end_ts, quantity_wh, evidenceHash). These values must reproduce exactly in attestation and at the PoV Gate.

Example: minimal validator (TypeScript)

type Window = {
  batch_id: `0x${string}`;
  device_id: `0x${string}`;
  start_ts: number; // UTC seconds
  end_ts: number;   // UTC seconds
  quantity_wh: number;
  nonce: `0x${string}`;
};

type Now = () => number; // ms

export function validateWindow(
  payloadCanonicalJson: string,
  headers: { xDeviceId: string; xWindowId: string; xNonce: string; xTimestamp: number },
  opts: {
    now: Now;
    minDurSec: number; maxDurSec: number;
    maxSkewMs: number;
    ratedWhPerWindow: number;
    lastWindows: Array<{start_ts:number; end_ts:number}>;
    seenBatchIds: Set<string>;
    seenNoncesByDevice: Map<string, Set<string>>;
    idempotentStore: Map<string, string>; // batch_id -> canonical JSON
  }
) {
  // 1) Canonical match
  const parsed = JSON.parse(payloadCanonicalJson) as Window;
  const reCanon = JSON.stringify(parsed, Object.keys(parsed).sort().reduce((o,k)=>(o[k]=parsed[k as keyof Window],o),{} as any)).replace(/\s+/g,'');
  if (reCanon !== payloadCanonicalJson) throw new Error("NON_CANONICAL_JSON");

  // 2) Header congruence
  if (headers.xDeviceId !== parsed.device_id) throw new Error("SCHEMA_INVALID");
  if (headers.xWindowId !== parsed.batch_id) throw new Error("SCHEMA_INVALID");
  if (headers.xNonce !== parsed.nonce) throw new Error("SCHEMA_INVALID");

  // 3) Window integrity
  if (!(parsed.start_ts < parsed.end_ts)) throw new Error("SCHEMA_INVALID");
  const dur = parsed.end_ts - parsed.start_ts;
  if (dur < opts.minDurSec || dur > opts.maxDurSec) throw new Error("OUT_OF_BOUNDS");

  // 4) Timestamp skew
  const skew = Math.abs(opts.now() - headers.xTimestamp);
  if (skew > opts.maxSkewMs) throw new Error("TIMESTAMP_SKEW");

  // 5) Overlap check for this device
  for (const w of opts.lastWindows) {
    const overlap = Math.max(0, Math.min(parsed.end_ts, w.end_ts) - Math.max(parsed.start_ts, w.start_ts));
    if (overlap > 0) throw new Error("OVERLAPPING_WINDOW");
  }

  // 6) Plausibility
  if (parsed.quantity_wh < 0) throw new Error("NEGATIVE_QUANTITY");
  if (parsed.quantity_wh > opts.ratedWhPerWindow * 1.15) throw new Error("OUT_OF_BOUNDS");

  // 7) Duplicate batch id (idempotent upsert)
  if (opts.seenBatchIds.has(parsed.batch_id)) {
    const prior = opts.idempotentStore.get(parsed.batch_id);
    if (prior !== payloadCanonicalJson) throw new Error("DUPLICATE_BATCH");
  }

  // 8) Nonce replay per device
  const nonces = opts.seenNoncesByDevice.get(parsed.device_id) ?? new Set<string>();
  if (nonces.has(parsed.nonce)) throw new Error("REPLAY_NONCE");

  // if all good, record idempotently
  opts.seenBatchIds.add(parsed.batch_id);
  nonces.add(parsed.nonce);
  opts.seenNoncesByDevice.set(parsed.device_id, nonces);
  opts.idempotentStore.set(parsed.batch_id, payloadCanonicalJson);

  return parsed; // safe to hash and attest
}

Clear outcomes

If a rule fails, return a specific error like NON_CANONICAL_JSON, OVERLAPPING_WINDOW, TIMESTAMP_SKEW, NEGATIVE_QUANTITY, OUT_OF_BOUNDS, DUPLICATE_BATCH, REPLAY_NONCE, or RATE_LIMITED. If a value is suspicious but not clearly wrong (for example, sudden spike), place the window in quarantine (QUARANTINED) for attestor review instead of hard rejecting.

Conformance

Write canonical JSON and submit with signed headers; ensure start_ts < end_ts and no overlaps per device_id; keep quantity_wh within device bounds; prevent nonce reuse and duplicate batch_id except for true idempotency; enforce device/operator rate limits; only after passing, compute evidenceHash and claimId and proceed to attest.

Last updated