# Can you run a GNN without anyone seeing the data? Journal of our experiments on privacy-preserving GNN inference on microcontrollers

> Source: <https://dev.to/skondho_kata/can-you-run-a-gnn-without-anyone-seeing-the-data-journal-of-our-experiments-on-privacy-preserving-l14>
> Published: 2026-08-10 02:56:24+00:00

I have been keenly interested in how to make Graph Neural Networks (GNN) run securely on resource-constrained devices. The question driving this work is deceptively simple: can three microcontrollers jointly compute GNN inference on traffic data such that *no single device ever sees the plaintext*, not the input features, not the model weights, not the intermediate activations? This post is about the findings I have found so far in this independent research endeavor.

A standard GCN layer [1] computes: `H' = σ(A · H · W + b)`

, where `A`

is the normalized adjacency matrix (graph topology), `H`

is the node feature matrix, `W`

is the learned weight matrix, and `σ`

is an activation function. In a traffic signal coordination setting, `A`

encodes which intersections connect to each other, `H`

holds the per-intersection sensor readings (queue lengths, phase states, flow rates), and the output dictates signal timing decisions.

The privacy concern is real: traffic flow data reveals movement patterns, congestion bottlenecks, and potentially individual vehicle trajectories. If multiple jurisdictions or agencies need to collaboratively optimize signals at their shared borders, no party may want to expose their raw data to the others. Secure Multi-Party Computation (MPC) lets them jointly compute the model output without any single party seeing the full picture.

We use **Replicated Secret Sharing (RSS)** [2] in a 3-party semi-honest setting. Each value `x`

is split into three shares `(x₁, x₂, x₃)`

such that `x₁ + x₂ + x₃ = x mod 2⁶⁴`

. Party `i`

holds two of the three shares `(xᵢ, xᵢ₊₁)`

, so any single party sees only random-looking numbers. Reconstruction requires combining shares from at least two parties.

```
┌─────────────────────────────────────────────────────┐
│             REPLICATED SECRET SHARING               │
│                                                     │
│  Secret x split into (x₁, x₂, x₃)                 │
│  where x₁ + x₂ + x₃ = x  (mod 2⁶⁴)               │
│                                                     │
│  Party 0 holds: (x₁, x₂)                           │
│  Party 1 holds: (x₂, x₃)                           │
│  Party 2 holds: (x₃, x₁)                           │
│                                                     │
│  → No single party can recover x                    │
│  → Secure addition is FREE (no communication)       │
│  → Secure multiplication costs 1 round of comm      │
└─────────────────────────────────────────────────────┘
```

The arithmetic properties are what make this practical. Secure addition of two shared values requires zero communication: each party just locally adds their shares. Secure multiplication is more expensive. It requires one round of communication between parties using a PRF-based resharing protocol to maintain the share structure, but the cost is bounded and predictable.

Here's the critical observation that makes GNN inference on microcontrollers feasible. In a traffic network, the *graph topology* is public knowledge. Everyone knows which intersections connect to which. The adjacency matrix `A`

is not secret. Only the node features `H`

(sensor readings) and the model weights `W`

(the learned policy) need protection.

This means the message-passing step `A · H`

is a **public-matrix × secret-matrix multiply**, which requires **zero communication**. Each party can locally compute it from their shares alone. Only the feature transformation step `H · W`

(secret × secret) requires the full RSS multiplication protocol.

Compared to CryptGNN [3] which treats everything as secret, this roughly halves the communication cost. In a 2-layer GCN, we have:

```
Layer 1: A·X (free) → (A·X)·W₁ (1 round) → activation (1 round)
Layer 2: A·H (free) → (A·H)·W₂ (1 round)
Total:   3 communication rounds (vs 5+ if A were secret)
```

The target hardware is **ESP32-S3** microcontrollers, 240MHz dual-core Xtensa processors with WiFi and ~320KB usable SRAM. Three boards form the 3-party RSS computation, communicating over TCP/WiFi. For the model, I use a 2-layer GCN trained on a 20-node synthetic traffic grid (4×5 intersection layout):

```
4×5 Grid Topology:        Model Architecture:

○─○─○─○─○                 GCN(16 → 16 → 4)
│ │ │ │ │                  • 20 nodes, 16 input features
○─○─○─○─○                 • 16 hidden features, 4 output classes
│ │ │ │ │                  • Polynomial activation: 0.1x² + 0.5x + 0.1
○─○─○─○─○                 • 340 total parameters
│ │ │ │ │                  • Q20 fixed-point arithmetic
○─○─○─○─○
```

The input features per node are: one-hot phase encoding (4 dims), queue lengths per direction (4 dims), flow rates (4 dims), occupancy, and time-of-day (sin/cos encoding), 16 features total. The model predicts which direction has the highest traffic demand.

An important design choice: we use a **polynomial activation** `0.1x² + 0.5x + 0.1`

instead of ReLU. ReLU requires a comparison (`x > 0`

), which is expensive in MPC because comparisons need bit-decomposition. A degree-2 polynomial can be evaluated using only one secure multiplication (for the `x²`

term), keeping the activation cost to a single communication round. Training with the polynomial activation achieves 52.1% accuracy vs 54.0% with ReLU, a modest 1.9 percentage point drop that we consider acceptable for the massive reduction in protocol complexity.

The full system goes from training through to secure inference verification:

```
Train GCN → Export Q20 Fixed-Point → Generate RSS Shares → Secure Inference → Verify Against Plaintext
```

The Python side handles training (`train_model.py`

), fixed-point export with RSS share generation (`export_model.py`

), and offline analysis (`analyze.py`

+ `rss_emulator.py`

). The C side implements the actual protocol: RSS primitives, secure matrix operations, and the 2-layer GCN inference pipeline. Both sides are designed to produce bit-identical results for a given set of shares.

Before worrying about secret sharing, the first question is whether the float-to-fixed-point conversion introduces unacceptable error. We use Q16 fixed-point (16 fractional bits) for the offline analysis with an 8-node ring graph as a controlled test bed.

| Node | Float Output | Q16 Output | Absolute Error |
|---|---|---|---|
| 0 | 0.293579 | 0.293488 | 0.000091 |
| 1 | 0.273324 | 0.273224 | 0.000100 |
| 2 | 0.276208 | 0.276123 | 0.000085 |
| 3 | 0.302083 | 0.302002 | 0.000081 |
| 4 | 0.328208 | 0.328125 | 0.000083 |
| 5 | 0.354583 | 0.354492 | 0.000091 |
| 6 | 0.357505 | 0.357422 | 0.000083 |
| 7 | 0.337120 | 0.337036 | 0.000084 |

The mean quantization error is **0.0000875** and the max is **0.0001002**. Both output features show identical values per node (the test model uses uniform weights that produce symmetric outputs), confirming the fixed-point pipeline is numerically faithful. All errors are sub-0.01% relative to the output magnitudes, well within acceptable bounds for traffic signal decisions.

Figure 1: Output comparison across nodes showing near-identical float and Q16 fixed-point values. The bars are visually indistinguishable, confirming minimal quantization degradation.

The error is also spatially uniform across nodes. This matters: if quantization error concentrated at specific graph positions, it could systematically bias decisions at those intersections.

Figure 2: Per-node quantization error heatmap. Error values are uniformly distributed across all nodes and output features, staying within the 0.00008 to 0.0001 range.

The RSS protocol correctness was verified through four escalating tests in the Python emulator:

| Test | Description | Result |
|---|---|---|
| Share/Reconstruct | Split 42, reconstruct from shares | ✅ PASS |
| Secure Addition | 100 + 200 = 300, zero communication | ✅ PASS |
| Secure Multiplication | 7 × 6 = 42, with PRF resharing | ✅ PASS |
| Random Multiplication | 100 random pairs, full ring arithmetic | 100/100 ✅ |

The last test is the important one. We generate 100 random integer pairs in `[0, 10000)`

, secret-share both operands, perform secure multiplication with the full PRF-based resharing protocol, reconstruct, and verify against the plaintext product modulo `2⁶⁴`

. All 100 pass, confirming that the resharing logic correctly maintains the share invariant `x₁ + x₂ + x₃ = x mod 2⁶⁴`

through multiplication.

The C desktop emulator spawns three processes (one per party) communicating over localhost TCP sockets. Each party holds its own shares of the features and weights, runs the full GCN inference protocol (5 steps: public matmul, secure matmul, polynomial activation, public matmul, secure matmul), and then opens the output shares to reconstruct the final result.

All 3 parties produce **identical reconstructed outputs**, confirming protocol correctness across the inter-process communication boundary. The desktop emulation completes in approximately **0.39 ms average** across the three parties (Party 0: 0.43 ms, Party 1: 0.39 ms, Party 2: 0.35 ms).

Figure 3: Inference time per party in the desktop emulator. The slight variation is expected from OS scheduling; the important thing is that all three parties complete within a tight band.

The per-party timing spread (~0.08 ms) is purely from OS scheduling jitter on the desktop. On dedicated ESP32-S3 hardware without a preemptive OS, we expect tighter synchronization.

The Python RSS emulator (`rss_emulator.py`

) replicates the exact same protocol as the C implementation:

```
Step 1: A * X      ← public × shared, FREE
Step 2: (A*X) * W₁ ← shared × shared, 1 communication round
Step 3: Activation  ← polynomial, 1 communication round
Step 4: A * H₁     ← public × shared, FREE
Step 5: (A*H₁) * W₂ ← shared × shared, 1 communication round
```

Both implementations use identical truncation semantics (arithmetic right shift on signed 64-bit values), the same PRF-based resharing (splitmix64 hash for deterministic randomness), and the same polynomial activation coefficients (Q-format `0.1`

, `0.5`

, `0.1`

). This dual-stack approach, Python for rapid prototyping and verification, C for deployment, lets us catch protocol bugs early. If the Python emulator and the C binary produce different outputs for the same shares, something is wrong in the C implementation.

The ESP32-S3 firmware is written in C with ESP-IDF. Each board connects to the others over WiFi/TCP, loads its pre-generated share files (`model_shares_p{0,1,2}.bin`

), and runs the inference protocol. The PRF uses mbedtls hardware-accelerated AES on the ESP32-S3 (replacing the OpenSSL backend used on desktop). The transport layer abstracts over POSIX sockets (desktop) vs lwIP TCP (ESP32), so the core protocol code is identical on both platforms.

This is fully built and compiles, but the actual on-device deployment and benchmarking is the next step. This will be the next part.

This is an ongoing experiment. This blog serves as a journal of my progress so far and will continually be updated once new findings are found. A few concrete next steps:

Hopefully we'll be able to get a paper through this independent endeavor.

[1] T. N. Kipf, M. Welling, "Semi-Supervised Classification with Graph Convolutional Networks," ICLR 2017.

[2] T. Araki, J. Furukawa, Y. Lindell, A. Nof, K. Ohara, "High-Throughput Semi-Honest Secure Three-Party Computation with an Honest Majority," CCS 2016.

[3] R. Ran, W. Wang, Q. Gang, J. Rao, "CryptGNN: Fast Privacy-Preserving Graph Neural Network Inference," 2022.

[4] ESP-IDF Programming Guide, Espressif Systems, [https://docs.espressif.com/projects/esp-idf/](https://docs.espressif.com/projects/esp-idf/).
