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.
Both detectors, 2.58 flight-hours. The test half is also 0 of 9.
Hardened catches round 1 in 41 s, mean lie 0.72 km.
Falcon 9 webcast launches at 10 Hz, plus NASA SMAP/MSL.
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.
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.
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.
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.
Replay
Earlier true frames played back under fresh sequence numbers and a fresh mission clock.
Freeze
Every channel holds the last pre-attack value. The vehicle keeps flying; the link does not.
Coordinated
A kinematically consistent fake: the velocity channels move with the position channels, so the obvious one-variable checks stay quieter.
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.
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.
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.
| Attacker | Defender | Detected | Latency | Lie before alert |
|---|---|---|---|---|
| PPO r1 (trained vs base) | base | 6/6 | 190 s | 12.3 km |
| PPO r1 (trained vs base) | hardened r1 | 6/6 | 66 s | 1.95 km |
| PPO r2 (trained vs hardened r1) | base | 5/6 | 153 s | 2.0 km |
| PPO r2 (trained vs hardened r1) | hardened r1 | 6/6 | 167 s | 1.97 km |
| PPO r2 (trained vs hardened r1) | hardened r2 | 6/6 | 36 s | 0.38 km |
| PPO r1 (trained vs base) | hardened r2 | 6/6 | 41 s | 0.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.
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.
| Attack | Base detected | Base latency | Base lie | Hardened detected | Hardened latency | Hardened lie |
|---|---|---|---|---|---|---|
| Constant bias | 6/6 | 0.6 s | 3.0 km | 6/6 | 0.6 s | 3.0 km |
| Slow drift (30 m/s) | 6/6 | 137 s | 4.1 km | 6/6 | 18.5 s | 0.55 km |
| Replay | 6/6 | 0.6 s | 5.4 km | 6/6 | 0.6 s | 5.4 km |
| Freeze | 6/6 | 0.6 s | 0.52 km | 6/6 | 0.6 s | 0.52 km |
| Coordinated | 6/6 | 5.3 s | 0.13 km | 6/6 | 3.6 s | 0.09 km |
| PPO agent (round 1) | 6/6 | 190 s | 12.3 km | 6/6 | 41 s | 0.72 km |
| PPO agent, round 2 | 5/6 | 153 s | 2.0 km | 6/6 | 36 s | 0.38 km |
| Attack | Base detected | Base latency | Base lie | Hardened detected | Hardened latency | Hardened lie |
|---|---|---|---|---|---|---|
| Constant bias | 3/6 | 240 s | 1.2 km | 6/6 | 6.9 s | 1.2 km |
| Slow drift (30 m/s) | 3/6 | 239 s | 3.5 km | 6/6 | 132 s | 1.58 km |
| Replay | 6/6 | 0.6 s | 5.4 km | 6/6 | 0.6 s | 5.4 km |
| Freeze | 6/6 | 0.6 s | 0.52 km | 6/6 | 0.6 s | 0.52 km |
| Coordinated | 6/6 | 49 s | 0.49 km | 6/6 | 3.6 s | 0.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.
| Streams | Model | CPU | FP32 | FP16 | INT8 |
|---|---|---|---|---|---|
| 4,096 | L2 GRU | 53.9 | 2.7 | 1.8 | 2.6 |
| 16,384 | L2 GRU | 232.4 | 11.3 | 6.7 | 10.7 |
| 4,096 | L3 CNN | 19.0 | 2.5 | 2.3 | 2.4 |
| 16,384 | L3 CNN | 80.0 | 10.9 | 9.2 | 10.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.
| Set | Precision | Recall | F1 | TP / FP / FN |
|---|---|---|---|---|
| SMAP, ours | 75.7% | 76.8% | 76.3% | 53 / 17 / 16 |
| MSL, ours | 90.0% | 50.0% | 64.3% | 18 / 2 / 18 |
| Combined, ours | 78.9% | 67.6% | 72.8% | 71 / 19 / 34 |
| Telemanom, SMAP | 85.5% | 85.5% | 85.5% | — |
| Telemanom, MSL | 92.6% | 69.4% | 79.3% | — |
| Telemanom, combined | 87.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.
- 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.
- Rajeev Hotlani
- Kapilan Karunakaran
- github.com/kapilank1/valid-but-wrong
- Devpost — URL not added yet
- Demo video — URL not added yet