Cal Hacks 13.0 · ARGUS

A CRC tells you the bits arrived intact. It doesn't tell you they're true.

ARGUS catches telemetry that passes every integrity check but describes physically impossible motion, and fights an AI attacker trained to fool it.

GitHubScrub the SES-9 flightDevpost — link pendingDemo video — link pending
Truth Reported Link valid Alert
SES-9 · held out · PPOsame attack
Base detector23.8 kmundetected for 315 s
Hardened r20.6 kmcaught at 34 s
Clean false alerts0 of 18

Both detectors, 2.58 flight-hours. The test half is also 0 of 9.

Held-out PPO6/6

Hardened catches round 1 in 41 s, mean lie 0.72 km.

Real flights31

Falcon 9 webcast launches at 10 Hz, plus NASA SMAP/MSL.

NASA benchmark72.8%

Event-level F1. Telemanom is 83.6%. We are below it.

02 — The attack

Every check passes. The rocket is somewhere else.

A man-in-the-middle sits on the telemetry link, rewrites the numbers inside a frame, and re-encodes it with a valid CRC-16. The frame the operator decodes is not the frame the vehicle sent.

Sync marker, checksum, sequence number, and mission clock all pass. The link monitor stays green. Forged frames pass these checks by construction: the attacker recomputes the CRC.

The picture on the console is then kilometres from the vehicle, and nothing on the link says so. The readout the console already uses is the whole pitch: link valid, picture off.

SYNC1ACFFC1DPASS
CRC-16CCITT-FALSE validPASS
SEQnext sequencePASS
TIMET+290.0 sPASS
ALTreported − truth = 3.7 kmFAIL
LINK 100% VALIDPICTURE OFF BY 3.7 km

SES-9 replay, PPO, T+290.0 s. Base score on this frame is 0.04, under τ 0.66. The sync word is the codec constant. The checksum is valid by construction — this drawing does not invent a CRC value.

Placeholder · console screenshot

Drop a capture of the live mission console at apps/site/public/images/console.png. The ▶ Demo button runs SES-9: bias, then PPO with the base detector silent, then a hot-swap to hardened. Nothing is invented here in its place.

Source: vbw/link/codec.py · apps/site/public/data/ses9_ppo.json

03 — Detection

Four checks. Only one of them is about the bits.

L0 asks whether the frame arrived intact. L1 asks whether the motion is physically possible. L2 asks whether it matches how this vehicle flies. L3 asks whether it looks like what attackers produce.

L0 · link

Sync, CRC, sequence, time

CRC-16/CCITT-FALSE over the frame, a monotonic sequence, a mission clock that only moves forward. Every forged frame is built to pass. That is the point of the project, not a bug in the monitor.

L1 · physics

11 windowed rules

Vertical velocity has to match the change in altitude. Over 10 seconds the webcast's own glitches reach 81.9 m/s, so the threshold sits at 122.9 m/s. Over 60 seconds those glitches cancel: the clean maximum is 13.6 m/s and the threshold is 20.4 m/s. Dynamic pressure has to match altitude through a fitted atmosphere, which pins altitude lies inside the atmosphere. The trajectory also has to stay in the corridor every training launch has flown.

L2 · learned dynamics

GRU, last 2 seconds

A GRU predicts the next frame from the last 2 s (10 Hz). Robust z-scores of the prediction error catch steps immediately and slow drifts through an EWMA. The live path runs the ONNX model on CPU, in about 1.1 ms per frame.

L3 · discriminator

Trained on attacker output

A 1D-CNN over the last 4 s of L1 and L2 features (25 of them). The corridor feature is left out, so "near the edge of the training envelope" is not treated as forgery. It keeps its own alert: over its logit threshold of 4.33 on 40 of 50 frames. The shipped model is hardened r2.

Alerting

Calibrated so clean flights stay quiet

L1 and L2 combine with a noisy-OR. An alert needs that score above τ = 0.66 on 3 of 5 frames. τ is the worst clean training-or-validation peak, 0.6546 on ABS-2A, lifted to two decimals. L3 does not enter the noisy-OR. On some clean webcast stretches L1 and L2 already sit just under τ, so folding L3 in would have cried wolf. No clean held-out flight alerts: 0 of 18 for the base detector and 0 of 18 for hardened, 2.58 flight-hours.

Source: vbw/link/codec.py · vbw/detector/physics.py · l2.py · l3.py · alerting.py

04 — Attack ladder

From a dumb offset to an attacker that learned the detector.

Six held-out launches. Each attack starts at 35% of the flight, above the atmosphere, and runs to the end. Latency is the mean time to the first alert. The lie is the mean of each run's largest altitude error before that alert.

01

Constant bias

A fixed altitude offset, full size on the first forged frame. It breaks vertical velocity against the change in altitude on the spot.

Base6/6 · 0.6 s · 3.0 km
Hardened6/6 · 0.6 s · 3.0 km
02

Slow drift (30 m/s)

Altitude ramps slowly enough to hide inside a short window. The 60-second physics rule, and the hardened detector, close it much sooner than the base detector does.

Base6/6 · 137 s · 4.1 km
Hardened6/6 · 18.5 s · 0.55 km
03

Replay

Earlier true frames played back under fresh sequence numbers and a fresh mission clock.

Base6/6 · 0.6 s · 5.4 km
Hardened6/6 · 0.6 s · 5.4 km
04

Freeze

Every channel holds the last pre-attack value. The vehicle keeps flying; the link does not.

Base6/6 · 0.6 s · 0.52 km
Hardened6/6 · 0.6 s · 0.52 km
05

Coordinated

A kinematically consistent fake: the velocity channels move with the position channels, so the obvious one-variable checks stay quieter.

Base6/6 · 5.3 s · 0.13 km
Hardened6/6 · 3.6 s · 0.09 km
06

PPO agent (round 1)

Trained against the base detector. It may move altitude, vertical velocity, and speed, and it grows a lie the base score stays under threshold for a long time.

Base6/6 · 190 s · 12.3 km
Hardened6/6 · 41 s · 0.72 km
07

PPO agent, round 2

The same learner retrained against hardened r1. It settles on a smaller lie. Base misses it on one held-out launch; hardened r2 does not.

Mean latency is over the 5 launches that alerted. Base never alerted on JCSAT-16.

Base5/6 · 153 s · 2.0 km
Hardened6/6 · 36 s · 0.38 km

Source: results/detector_eval_r2.json

05 — Arms race

Each new attacker hid longer. Each new defender caught both.

PPO round 1 walks 12.3 km past the base detector. Hardened r1 cuts that to 66 s. Retrained against r1, PPO round 2 survives it 2.5× longer (167 s versus 66 s) with a smaller lie. Hardened r2, trained on that output and now shipped, catches both.

Six pairings · bar length is mean latency · every row is 0/18 clean false alerts
AttackerDefenderDetectedLatencyLie before alert
PPO r1 (trained vs base)base6/6190 s12.3 km
PPO r1 (trained vs base)hardened r16/666 s1.95 km
PPO r2 (trained vs hardened r1)base5/6153 s2.0 km
PPO r2 (trained vs hardened r1)hardened r16/6167 s1.97 km
PPO r2 (trained vs hardened r1)hardened r26/636 s0.38 km
PPO r1 (trained vs base)hardened r26/641 s0.72 km

The first reward charged the detector's score on every step, so the best policy was to never lie, or to get caught immediately. Charging only for score above a margin, and making an alert much more expensive than a slow lie, is what produced an attacker that actually lies. A policy that was also allowed to nudge downrange was caught in seconds by the tightest rule, so the shipped agent lies about altitude and the velocities that go with it.

Source: results/arms_race.json

06 — Replay

Scrub SES-9. The shaded gap is the lie.

Holdout launch, seed 0, the same frames the detector scores. PPO is armed at T+190 s. Bias, for contrast, is a 3 km step at T+60 s. This is one flight, not the six-launch mean.

Mission timeT+224.3 s
Truth altitude113.2 km
Reported altitude113.8 km
The lie0.61 km
Base score 0.02
Hardened · alert0.77
L1 / L2 base0.01 / 0.00
L3 probability0.99
Altitude · cyan truth, amber reported, shaded lie
0671342010136272409545attackhardenedbase
The lie, zoomed · reported minus truth, km
0.01020300136272409545attackhardenedbase
Scores · gray base, cyan hardened, red threshold τ 0.66
0.00.30.71.00136272409545attackhardenedbase

This replay: base alerts at 315 s with a 23.8 km lie; hardened alerts at 34 s with a 0.61 km lie. Onset is T+190.0 s, the live demo's arm time. The six-launch eval starts each attack at 35% of that flight and records SES-9 base 315 s / 23.8 km and hardened 34 s / 0.6 km. Alert marks are the persisted decision, not the first sample over τ.

Downsampled from 10 Hz to about 2 Hz. Alert edges are kept. Arrow keys move the slider. Drag a chart. 1,099 samples from 5,450 frames.

Source: apps/site/public/data/ses9_ppo.json · ses9_bias.json

07 — Results

Held-out launches. Zero false alerts. The cost of a frame.

Full intensity · 6 held-out launches · onset at 35% of the flight
AttackBase detectedBase latencyBase lieHardened detectedHardened latencyHardened lie
Constant bias6/60.6 s3.0 km6/60.6 s3.0 km
Slow drift (30 m/s)6/6137 s4.1 km6/618.5 s0.55 km
Replay6/60.6 s5.4 km6/60.6 s5.4 km
Freeze6/60.6 s0.52 km6/60.6 s0.52 km
Coordinated6/65.3 s0.13 km6/63.6 s0.09 km
PPO agent (round 1)6/6190 s12.3 km6/641 s0.72 km
PPO agent, round 25/6153 s2.0 km6/636 s0.38 km
Intensity 0.4 · same launches and onset · PPO ignores intensity, so it is not repeated
AttackBase detectedBase latencyBase lieHardened detectedHardened latencyHardened lie
Constant bias3/6240 s1.2 km6/66.9 s1.2 km
Slow drift (30 m/s)3/6239 s3.5 km6/6132 s1.58 km
Replay6/60.6 s5.4 km6/60.6 s5.4 km
Freeze6/60.6 s0.52 km6/60.6 s0.52 km
Coordinated6/649 s0.49 km6/63.6 s0.04 km

False alerts

0 of 18

Clean held-out flights, 6 launches × 3 noise seeds, 2.58 flight-hours, 92,910 frames. The same for both detectors. The test half, which no threshold was fit on, is 0 of 9 over 1.27 h (45,750 frames). No attacked run alerted before its attack began.

Cost

1.1 ms · 1.3 ms

Base and hardened, per frame, on one CPU core. A frame at 10 Hz is 100 ms, so the detector is a small fraction of the period. The C++ edge monitor, measured separately, is in the engineering section.

  • Mean latency is over the 5 launches that alerted. Base never alerted on JCSAT-16.
  • Mean latency is over the 3 launches that alerted. Base never alerted on SES-9, JCSAT-16, IRIDIUM-NEXT-5.
  • Mean latency is over the 3 launches that alerted. Base never alerted on SES-9, JCSAT-16, IRIDIUM-NEXT-5.

Display rounding: latency to 0.1 s, then to the nearest second from that tenth once it is at least 20 (40.5 rounds to 41). Lies of 2 km or more to 0.1 km; smaller lies to 0.01 km. Freeze is 0.52 km here; the README prints 0.5 km. Unrounded values stay in the JSON files named under each section.

Source: results/detector_eval_r2.json

08 — Engineering

A laptop GPU, a 30-microsecond core, and a benchmark we lose.

TensorRT is a fleet benchmark, not the live path

The demo scores one vehicle on ONNX Runtime, on CPU. NVIDIA GeForce RTX 5060 Laptop GPU, TensorRT 11.4.0.106, plugged in. FP16 scores 16,384 streams of L2 and L3 together in 15.9 ms, inside a 20 ms frame at 50 Hz: 35× on L2 and 8.7× on L3 versus the CPU, with 0 detection decisions changed. INT8 (minmax calibration) is slower than FP16 on these small models and flips 3.0% of L3 decisions. On battery the same FP16 pair takes 21.5 ms at 16,384 streams and 6.0 ms at 4,096. One stream at a time the GPU loses: L2 is 567.8 µs on FP16 versus 57.2 µs on CPU, and L3 is 239.5 µs versus 38.9 µs.

p50 milliseconds, copy in and out included · AC power · shipped L3 r2
StreamsModelCPUFP32FP16INT8
4,096L2 GRU53.92.71.82.6
16,384L2 GRU232.411.36.710.7
4,096L3 CNN19.02.52.32.4
16,384L3 CNN80.010.99.210.4

C++ edge monitor

The base detector (L1 + L2, not L3) reimplemented in C++. Bit-exact with Python on 16,766 frames: max absolute error 0, alert-frame mismatches 0. 30 µs per frame p50 (L1 3.1 µs, L2 26.4 µs), 32,550 frames/s on one core. The Python reference in that bench is 1.19 ms/frame, about 40× slower. Not measured yet: live UDP end to end (scripts/edge_eval.py with vbw_edge.exe + scripts/udp_replay.py); test_l1.cpp per-rule parity.

NASA SMAP / MSL

82 labeled streams, 81 unique channels, 105 labeled anomalies. Event-level scoring, the Telemanom protocol (Telemanom (Hundman et al., KDD 2018)). Combined we are 10.8 F1 points below the paper. That shows the learned layer is a working anomaly detector. It is not a state-of-the-art claim.

Event-level precision, recall, F1 · Telemanom F1 is derived from the paper's precision and recall
SetPrecisionRecallF1TP / FP / FN
SMAP, ours75.7%76.8%76.3%53 / 17 / 16
MSL, ours90.0%50.0%64.3%18 / 2 / 18
Combined, ours78.9%67.6%72.8%71 / 19 / 34
Telemanom, SMAP85.5%85.5%85.5%—
Telemanom, MSL92.6%69.4%79.3%—
Telemanom, combined87.5%80.0%83.6%—

Hundman et al., KDD 2018. We do not quote point-adjusted F1. It is in the JSON and it is optimistic.

Tests and calibration

221 Python tests collected in the default environment. The TensorRT module (FP32 and FP16) is skipped unless those packages and a GPU are present. 75 console test cases. 3 C++ suites: frame, config, and detector parity. τ = 0.66 was set from clean training and validation flights only. The test half never moved a threshold.

Source: results/trt_bench.json · results/edge_bench.json · results/bench_summary.json

09 — Architecture

From a real launch to a spoken alert.

Sources are encoded, a man-in-the-middle may rewrite them and re-sign a valid CRC, and four layers judge the frame. PPO drives the attacker and, after training, the discriminator.

Falcon 931 launches · 10 HzNASA SMAP / MSL82 streams · 105 anomaliesQuad simSITL bridge not flownCodecsync · seq · CRC-16MITMrewrite · valid CRCL0checks passL111 physics rulesL2GRU · 2 sL3CNN · attacker outputAlert3 of 5 · voicePPO red teamtwo rounds · drives MITMMission consoleNext.js · Cartesia voiceTiger DataTimescale hypertables
  • Live path. Python 3.12, ONNX Runtime on CPU, FastAPI and a WebSocket.
  • Attacker. Scripted ladder plus Stable-Baselines3 PPO, two rounds.
  • Console. Next.js mission console. Cartesia speaks alerts when a key is set.
  • Store. Tiger Data / TimescaleDB hypertables and a one-second aggregate. Verified live.
  • GPU. TensorRT FP16/INT8 bench for L2 and L3 only. Not in the live demo.
  • Edge. C++ base detector, bit-exact on the golden frames. Live UDP not yet measured.

Diagram of the README "How it works" flow. No Kubernetes. Vultr was not deployed.

10 — What actually ran

Sponsor tech, only where it is true.

Tiger Data · live

TimescaleDB 2.31.0

Hypertables for frames and verdicts, a verdicts_1s continuous aggregate, refresh and compression policies. A live demo run wrote 8,961 rows in 64 batches with 0 errors. Run history, a 300-bucket timeline, and analytics were served from Tiger Cloud.

Cartesia · live

sonic-3.6

The console speaks each alert in a mission-control line. The 4 alerts of a live demo were spoken through Cartesia. The words follow the verdict. An example of the shape, not a measured confidence: "Flight, Guidance. Altitude disagrees with vertical velocity. Possible spoofed telemetry. Confidence 94 percent."

Cursor

Built in Cursor

The SpaceXSI track requires the project to be built in Cursor. This site and the detector work were done there, including with Grok. There is no testimonial and no invented user.

NVIDIA · benchmark

TensorRT 11.4.0.106

FP16 and INT8 engines on NVIDIA GeForce RTX 5060 Laptop GPU. 16,384 streams in 15.9 ms, 0 decisions changed at FP16. The live demo stays on CPU.

Source: docs/INTEGRATIONS.md

11 — Limits

What this does not show.

The interesting claims are next to the places they fail. These are the ones we will not sand off.

Below Telemanom

Combined event-level F1 is 72.8% against Telemanom's 83.6%, a gap of 10.8 points, mostly recall on MSL. The benchmark is evidence the learned layer works. It is not a claim of state of the art.

Slow drift above the atmosphere

At 40% intensity, base catches bias and drift on 3/6 and 3/6 launches (mean 240 s and 239 s). Hardened catches both on 6/6: bias in 6.9 s, drift only after 132 s with a 1.58 km lie. A slow altitude walk-off under about 20 m/s, above the atmosphere, stays inside the webcast data's own inconsistency.

Webcast, not a bus dump

Falcon 9 numbers are read off the broadcast overlay. They are real and only loosely self-consistent, which is why every physics rule is windowed and why τ is 0.66 rather than something tight.

No live ArduPilot

The quadcopter path runs on our simulator. The MAVLink SITL bridge exists and has never been flown against real ArduPilot. The C++ monitor's live UDP path is also unmeasured: live UDP end to end (scripts/edge_eval.py with vbw_edge.exe + scripts/udp_replay.py); test_l1.cpp per-rule parity.

Two rounds, not an unseen attacker

Hardened r2 was trained on PPO round 2's output. The numbers against that attacker are on held-out launches, not against an attacker the discriminator has never seen. A third round could find a new gap.

Not in the live path

TensorRT scores the learned layers only, many vehicles at once. There is no Kubernetes cluster and no Vultr deployment. Docker compose is in the repo and was not what the verified demo ran on.

12 — Team

Rajeev Hotlani and Kapilan Karunakaran.

Cal Hacks 13.0.