Skip to content

Bureau — Blue Team (defensive)

Meridian

Smart meters and PMUs (Phasor Measurement Units) are vulnerable to falsified readings, which can be used for billing fraud or to mask manipulation of grid state. Meridian signs every reading at the meter and surfaces five common grid-fraud patterns as contradictions in the published record.

Posture: 🔵 Blue Team (defensive)   ·   Status: alpha

What it does

Meridian watches the smart grid. It ingests three kinds of signed observations: smart-meter kWh readings (kWh = kilowatt-hour, the unit your power bill is measured in), PMU samples (PMU = Phasor Measurement Unit, a GPS-time-synchronized device installed at substations that samples voltage and current 30–120 times per second so operators can watch the grid breathe in real time), and per-meter manufacturer fingerprints (a physical-identity record listing the meter's CT/PT ratio – current/potential transformer ratio – its power-supply EMI signature, and its time-base drift).

Five fraud classes are detected. Energy-theft – a meter under-reports while the upstream substation total minus all the OTHER meters on the same feeder shows energy left the substation that nobody is billing for. Billing-tamper – a meter's signed reading history goes backwards (later kWh < earlier kWh), which is impossible for a real meter because cumulative kWh is monotonic by physics. Firmware-rollback – a meter announces older firmware than the manufacturer's last signed manifest declared installed (a Stuxnet-style attack signature). PMU-time-drift – a PMU's stamped time differs from its Celeste-anchored ground truth by more than 1 millisecond, which breaks the IEEE C37.118 Total Vector Error budget the grid relies on. Phase-mismatch – a PMU's voltage angle disagrees with its k-of-n neighboring PMUs on the same bus by more than the configured tolerance.

Who would use it

  • A power utility's NERC-CIP compliance team filing audit-ready records of grid measurement integrity.
  • A regulator (FERC, regional reliability council) building public datasets of grid event integrity.
  • A microgrid operator (university campus, military base, industrial park) producing tamper-evident records for a power-purchase agreement audit.
  • An energy-market operator (ISO/RTO) wanting independent corroboration of submitted meter data underlying settlements.
  • A consumer-advocacy organization investigating allegations of utility-side billing fraud.
  • A forensic engineer reconstructing a major outage where an attacker is suspected of falsifying readings.

What you'll need

  • The Pluck CLI installed (pnpm add -g @sizls/pluck-cli).
  • For meters: any AMI (Advanced Metering Infrastructure) head-end able to push signed readings into Meridian's library API. The package exposes observeReading, observePmuSample, observeFingerprint, recordSubstationTotal for embedding in existing utility infra.
  • For PMUs: a C37.118-compliant PMU at each substation, time-synced to GPS. Common models include the SEL-487E, GE N60, or open-source OpenPMU.
  • A Celeste deployment for ground-truth time anchoring.
  • A signed firmware-manifest stream from each meter's manufacturer (delivered via SBOM-AI).

Step-by-step

The alpha runs an in-memory demo on synthetic substation totals + meters + PMU samples. Live capture (an AMI bridge, a C37.118 packet bridge, a daemon ingesting both) ships in a follow-up. To see the engine work today:

Shell
pluck bureau meridian demo

You'll see something like:

meridian/demo: registering meter fingerprints + substation totals...
meridian/demo: tamper proofs emitted = 3
meridian/demo: proofId=4f2a7c… kind=firmware-rollback targetId=8ab1c…
meridian/demo: proofId=92cc11… kind=energy-theft targetId=92efba…
meridian/demo: proofId=72ed44… kind=pmu-time-drift targetId=BUS-401
meridian/demo: proofs notarized (stub) = 3

The synthesized run seeds a manufacturer fingerprint manifest, two substation totals on FEEDER-7, four meter readings (one legit, one with firmware-rollback, two halves of an energy-theft pair) and two PMU samples (one with > 1 ms drift). Meridian emits three tamper proofs.

Run it yourself

Drop this into a Node 18+ project (npm install @sizls/pluck-bureau-meridian @sizls/pluck-bureau-core tsx):

TypeScript
// index.ts
import { createHash } from "node:crypto";
import {
  createMeridianSystem,
  fingerprintPrivateKey,
  meterIdHashOf,
  signCanonicalBody,
} from "@sizls/pluck-bureau-meridian";
import { generateOperatorKey } from "@sizls/pluck-bureau-core";

const flush = (n = 20) =>
  new Promise<void>((res) => {
    let i = 0;
    const tick = () => (++i >= n ? res() : setImmediate(tick));
    setImmediate(tick);
  });

function sign<T extends Record<string, unknown>>(body: T, idKey: string, key: string, seed = ""): any {
  const skel = { schemaVersion: 1, ...body };
  const id = createHash("sha256").update(JSON.stringify(skel) + seed).digest("hex");
  const signed = signCanonicalBody({ ...skel, [idKey]: id }, key);
  return { ...skel, [idKey]: id, signature: signed.signature };
}

async function main() {
  const operator = generateOperatorKey();
  const meterAKey = (generateOperatorKey()).privateKeyPem;
  const meterAFp = fingerprintPrivateKey(meterAKey);
  const meterBKey = (generateOperatorKey()).privateKeyPem;
  const meterBFp = fingerprintPrivateKey(meterBKey);
  const witnessKey = (generateOperatorKey()).privateKeyPem;
  const witnessFp = fingerprintPrivateKey(witnessKey);
  const manufacturerKey = (generateOperatorKey()).privateKeyPem;
  const manufacturerFp = fingerprintPrivateKey(manufacturerKey);

  const feederId = "FEEDER-7";
  const meterIdA = meterIdHashOf("UTIL-NORTH", "MTR-A-001");
  const meterIdB = meterIdHashOf("UTIL-NORTH", "MTR-B-001");

  const meridian = createMeridianSystem({
    signingKey: operator.privateKeyPem,
    disablePausePoll: true, disableLogging: true,
  });

  try {
    // Manufacturer manifest declares meter A on firmware 2.0.0.
    meridian.observeFingerprint(sign({
      meterIdHash: meterIdA, ctPtRatio: 200,
      psuEmiSignature: "a".repeat(64), timeBaseDriftPpm: 1.2,
      installedFirmwareVersion: "2.0.0",
      signedAt: "2026-04-25T00:00:00Z", manufacturerFingerprint: manufacturerFp,
    }, "fingerprintId", manufacturerKey));

    // Substation feeder delta = 50 kWh.
    meridian.recordSubstationTotal({ feederId, totalKwh: 100, asOf: "2026-04-26T00:00:00Z" });
    meridian.recordSubstationTotal({ feederId, totalKwh: 150, asOf: "2026-04-26T01:00:00Z" });

    // Meter A: firmware-rollback + reading 0 → 30 kWh.
    meridian.observeReading(sign({
      meterIdHash: meterIdA, feederId, kWh: 0, firmwareVersion: "2.0.0",
      asOf: "2026-04-26T00:00:00Z", meterFingerprint: meterAFp,
    }, "readingId", meterAKey, "a-start"));
    meridian.observeReading(sign({
      meterIdHash: meterIdA, feederId, kWh: 30, firmwareVersion: "1.5.0",
      asOf: "2026-04-26T01:00:00Z", meterFingerprint: meterAFp,
    }, "readingId", meterAKey, "a-rollback"));

    // Meter B: theft (only 5 kWh out of expected ~20).
    meridian.observeReading(sign({
      meterIdHash: meterIdB, feederId, kWh: 0, firmwareVersion: "2.0.0",
      asOf: "2026-04-26T00:00:00Z", meterFingerprint: meterBFp,
    }, "readingId", meterBKey, "b-start"));
    meridian.observeReading(sign({
      meterIdHash: meterIdB, feederId, kWh: 5, firmwareVersion: "2.0.0",
      asOf: "2026-04-26T01:00:00Z", meterFingerprint: meterBFp,
    }, "readingId", meterBKey, "b-theft"));

    // PMU samples – second has 50ms drift (> 1ms tolerance).
    meridian.observePmuSample(sign({
      busId: "BUS-401", pmuId: "PMU-401-A", frequencyHz: 60.0,
      voltageMagnitude: 1.0, voltageAngleRad: 0.5,
      observedAt: "2026-04-26T00:00:00.000Z",
      celesteAnchorAt: "2026-04-26T00:00:00.000Z", witnessFingerprint: witnessFp,
    }, "sampleId", witnessKey));
    meridian.observePmuSample(sign({
      busId: "BUS-401", pmuId: "PMU-401-B", frequencyHz: 60.0,
      voltageMagnitude: 1.0, voltageAngleRad: 0.51,
      observedAt: "2026-04-26T00:00:00.050Z",
      celesteAnchorAt: "2026-04-26T00:00:00.000Z", witnessFingerprint: witnessFp,
    }, "sampleId", witnessKey));

    await flush();
    const proofs = meridian.facts.tamperProofs();
    console.log(`tamper proofs: ${proofs.length}`);
    for (const p of proofs) console.log(`  kind=${p.kind}`);
  } finally {
    await meridian.shutdown();
  }
}

main().catch((err) => { console.error(err); process.exit(1); });

Run with tsx index.ts. Expected output:

tamper proofs: 3
  kind=firmware-rollback
  kind=energy-theft
  kind=pmu-time-drift

▶ Open in StackBlitz – runs in your browser, no install required.

What you get

A signed Meridian.Tamper cassette per contradiction containing:

  • The verbatim offending observations – meter readings, PMU samples, fingerprints – each signed by their original signer.
  • The contradiction kind.
  • The cross-checked substation total (for energy-theft) or signed manufacturer manifest entry (for firmware-rollback).
  • A Rekor uuid pinning the bundle to the public log.

Consumer service-address NEVER appears in a signed body. Only meterIdHash = sha256(utility-code || meter-serial). The consumer-to-serial mapping stays in the utility's local registry. A regulator, an ISO/RTO settlements team, or a court can re-run the math against the carried bodies (e.g. for energy-theft, recompute substationDelta - sum(otherMeterDeltas) against the suspect meter's reported delta).

What it can't do

  • Up to 20,000 live readings, 20,000 PMU samples, 10,000 fingerprints, 4,096 substation totals, and 1,024 tamper proofs in memory before FIFO eviction. Operators wanting larger windows must persist to durable storage.
  • kWh capped at 10^12 (1 TWh – generous industrial headroom). Frequency capped at 1 kHz, voltage magnitude capped at 10^6.
  • Collusive theft where the substation meter is also tampered is not detected. If the upstream PMU/total is itself lying, the contradiction can't be observed. Pair with Celeste-anchored PMUs at the substation feed and Fingerprint-anchored substation hardware identity for upstream protection.
  • Live capture and daemon mode ship in a follow-up.

A real-world example

A regional municipal utility brings Meridian online ahead of a new dynamic-pricing tariff. Six months in, Meridian fires three energy-theft proofs in one warehouse district: the substation total minus all other meters shows roughly 14 kW left every business day at lunch. The utility's revenue-protection team correlates with the Meridian timestamps and finds a high-end restaurant that grew its electric load profile by 14 kW in the same window – but whose own meter reading didn't budge. The utility opens an investigation; the signed bundles go in the regulatory filing requesting reactivation of an inspection right; the restaurant operator settles for back-billed energy + penalties. The records cost the utility nothing extra to produce – the meters were already signing readings; Meridian just notarized the contradiction.


For developers

Predicate URIs

URIWhat it attests
https://pluck.run/Meridian.Reading/v1One meter-signed kWh reading (meter signing-key fingerprint, kWh, firmware version, asOf, feederId).
https://pluck.run/Meridian.Pmu/v1One witness-signed PMU sample (busId, frequencyHz, voltageMagnitude, voltageAngleRad, observedAt, Celeste-anchored ground-truth time).
https://pluck.run/Meridian.Fingerprint/v1One manufacturer-signed per-meter physical fingerprint (CT/PT ratio, PSU EMI signature, time-base drift in ppm, declared-installed firmware).
https://pluck.run/Meridian.Tamper/v1One Bureau-signed tamper proof carrying verbatim offending observations.

Programs composed

  • Celeste – provides the GPS-anchored ground-truth time the PMU drift detector compares each sample's observedAt against
  • SBOM-AI – provides the signed firmware-manifest stream the firmware-rollback detector cross-checks against
  • Fingerprint – provides the per-(meter, hardware) physical fingerprint pattern the manufacturer signs
  • Sigstore Rekor – proofs are notarized as DSSE in-toto envelopes

Threat model + adversary

20K/20K/10K/4K/1K caps in memory before FIFO eviction. kWh cap 1e12 (1 TWh), frequency cap 1 kHz, voltage cap 1e6. Meridian doesn't detect collusive theft where the substation meter is also tampered (pair with Celeste-anchored substation PMUs + Fingerprint-anchored substation hardware identity). Consumer service-address never appears in a signed body – only meterIdHash.

Verify a published cassette

Shell
pluck bureau verify <bundle-dir>

cosign verify-blob \
  --key <pubkey.pem> \
  --signature <signature.sig> \
  --type https://pluck.run/Meridian.Tamper/v1 \
  <body.json>

The package exports validateMeridianTamperProof, verifyCanonicalBody, and predicateTypeForTamperKind.

See also

Edit this page on GitHub
Previous
Turbine

Ready to build?

Install Pluck and follow the Quick Start guide to wire MCP-first data pipelines into your agents and fleets in minutes.

Get started →