Can you run a GNN without anyone seeing the data? Journal of our experiments on privacy-preserving GNN inference on microcontrollers An independent research project demonstrated that three ESP32-S3 microcontrollers can jointly compute Graph Neural Network inference on traffic data using Replicated Secret Sharing, ensuring no single device sees the plaintext input, model weights, or intermediate activations. By treating the graph topology as public, the message-passing step requires zero communication, reducing the total communication rounds to three for a two-layer GCN, making privacy-preserving GNN inference feasible on resource-constrained devices. 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/ .