Deployment guide · Patent Pending

PTP NIC hardware-timestamping

For Vision Block customers running tight sensor-fusion pipelines on dedicated deployments, the api-server's PTP source can deliver a sub-microsecond envelope when paired with a privileged sidecar that has NIC access. This page is the deployment recipe for that bridge.

Shared tenants stop here. Azure App Service, Replit, and other shared-tenant hosts cannot run the sidecar — no /dev/ptp*, no CAP_NET_RAW. The api-server runs the shipped software path (~100 µs envelope) and surfaces an anchorWarnings entry honestly. The sub-µs path requires a dedicated deployment with NIC access.

What you provision

  1. An IEEE 1588v2 grandmaster on the same LAN/VPC. Bring your own — bare-metal linuxptp ptp4l, AWS Time Sync PTP, or Google Cloud Hyperdisk PTP all work.
  2. The tblk-ptp-sidecar binary. Audited single-file Rust source for linux/amd64 + linux/arm64, built from source on your own infrastructure against the pinned toolchain (no public release channel — you own the binary you run). Holds CAP_NET_RAW, reads Linux SO_TIMESTAMPING cmsg control messages from its slave socket, and publishes per-packet NIC stamps on a local Unix-domain socket. The bring-your-own Rust/C/Go path is still supported — the wire protocol is documented below — but the in-tree binary is the recommended starting point.
  3. The @workspace/ptp-hwstamp addon ships in the api-server. It auto-loads at boot when SPIRAL_PTP_HWSTAMP_SIDECAR is set, connects to your sidecar over the Unix socket, and registers itself with the temporalspiral hardware-stamping seam.

Environment variables

SPIRAL_PTP_INTERFACE=eth0
SPIRAL_PTP_DOMAIN=0
SPIRAL_PTP_MODE=multicast        # or unicast
SPIRAL_PTP_HARDWARE_TIMESTAMPING=required   # refuse to wire on missing addon
SPIRAL_PTP_HWSTAMP_SIDECAR=/var/run/temporalblock-ptp.sock

With =required and the sidecar reachable, the meta.spiral.anchorWarnings field reports "PTP adapter is running with NIC hardware-timestamping (hw:linuxptp-uds)" and the per-sample sourceLabel becomes ptp:eth0:0+hw:linuxptp-uds. With =preferred (the default) the api-server boots either way: present sidecar → hardware path; absent → the shipped software path with its honest envelope warning.

Per-cloud caveats

  • AWS Time Sync PTP (169.254.169.123 on c6i / c7g / m6i / m7g and newer): PHC exposed at /dev/ptp0. Envelope ≤100 ns on healthy nitro hosts. Older instance families do not expose the PHC — the addon will be unavailable there.
  • Google Cloud Hyperdisk PTP (C3 / C3D / N4): PHC exposed at /dev/ptp_hyperdisk with --enable-confidential-storage. Envelope ~250 ns end-to-end.
  • Bare-metal grandmaster (in-tenant rack with ptp4l + phc2sys on a dedicated NIC): the most accurate option (≤50 ns). The sidecar attaches SO_TIMESTAMPING directly to its slave socket, or reads phc2sys's SHM segment.
  • Azure App Service / Replit / shared tenants: not supported. Use the shipped software path; the envelope warning is honest. If you need sub-µs, migrate to a dedicated deployment.

Sidecar wire protocol

Newline-delimited JSON over SOCK_STREAMUnix-domain socket. One frame per RX/TX stamp; one frame per line; malformed frames are dropped silently so a single bad line can't kill the bridge.

{"v":1,
 "type":"rx",                  // or "tx"
 "msgType":0,                  // PTPv2: 0=SYNC, 1=DELAY_REQ,
                               //        8=FOLLOW_UP, 9=DELAY_RESP
 "seqId":1234,                 // PTPv2 sequenceId, 0..65535
 "unixNs":"1748073600123456789",  // string: exceeds JS safe int
 "mechanism":"so_timestamping" // operator-readable tag
}

The production binary tblk-ptp-sidecar implements this protocol in ~700 lines of audited Rust (source under lib/ptp-hwstamp-sidecar/) and is the recommended deployment path. The legacy in-process reference at lib/ptp-hwstamp/src/sidecar.ts is now used only by CI smoke tests and as a copy-paste starting point for operators writing their own implementation in C/Go.

Building and installing the sidecar

Build tblk-ptp-sidecar from source on your own infrastructure against the pinned toolchain in lib/ptp-hwstamp-sidecar/rust-toolchain.toml, then record the resulting binary's SHA256 in your internal change-management and install. The binary links statically against musl, so the same build works across glibc + musl hosts. temporalBLOCK does not publish prebuilt binaries through GitHub Releases or any other public channel.

ARCH=$(uname -m)        # x86_64 or aarch64
TARGET=${ARCH}-unknown-linux-musl

cd lib/ptp-hwstamp-sidecar
cargo build --release --target ${TARGET}

ASSET=target/${TARGET}/release/tblk-ptp-sidecar
sha256sum ${ASSET}        # pin this digest in your change-management

install -m 0755 ${ASSET} /usr/local/bin/tblk-ptp-sidecar
useradd --system --no-create-home --shell /usr/sbin/nologin tblk-ptp
install -m 0644 systemd/tblk-ptp-sidecar.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now tblk-ptp-sidecar.service

For supply-chain auditing, generate a CycloneDX SBOM locally with cargo cyclonedx and store it alongside the pinned digest. The disabled stub at .github/workflows/ptp-sidecar-release.yml has no triggers and exists only to make the no-public-release posture explicit in the tree.

Sidecar systemd unit (reference)

Pair the addon's aggressive ~10 ms reconnect with a tight Restart=always systemd policy and the bounded hold-over window described below, and a sidecar crash or redeploy lands back inside the cap before the api-server has to fail-closed. Reference unit (adapt the binary path and user to your build):

[Unit]
Description=temporalBLOCK PTP hardware-stamping sidecar
After=network-online.target
Wants=network-online.target

[Service]
Type=notify
ExecStart=/usr/local/bin/tblk-ptp-sidecar \
  --interface eth0 \
  --socket /var/run/temporalblock-ptp.sock
User=tblk-ptp
AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN
Restart=always
RestartSec=100ms
WatchdogSec=5s

[Install]
WantedBy=multi-user.target

Hold-over window. Set SPIRAL_PTP_HW_HOLDOVER_MS (default 0, fail-closed) to a small bounded value (e.g. 250) and the api-server will publish the last-known offset for up to that many milliseconds when the sidecar briefly drops, with the rtt envelope widened by SPIRAL_PTP_HW_HOLDOVER_DRIFT_PPM (default 1 ppm, typical commercial TCXO) times the elapsed time. The sourceLabel grows a +holdover suffix during the window so consumers see the cached posture honestly, and a distinct "holding over" anchorWarnings entry is pushed the first time the cache engages. After the cap elapses the source returns to fail-closed and the existing "became unavailable mid-flight" warning fires.

Verifying the bridge

  1. Boot the api-server with the env knobs above. Watch the startup log for "registered NIC hardware-stamping addon".
  2. Call GET /api/v1/spiral with any valid API key and inspect meta.spiral.anchorWarnings. You should see the hw:linuxptp-udsactivation message and no "not wired" or"software-timestamping" entries.
  3. If the sidecar restarts mid-flight under =required, the provider's available() flips to false and subsequent exchanges fail-closed with PtpHardwareStampUnavailableError rather than silently widening the envelope by 1000× to the userland ~100 µs reading — this is the contract the =required posture promises. A distinct "became unavailable mid-flight" anchorWarnings entry is pushed the first time the fail-closed path engages, and stays sticky for the rest of the process so the SLA-breach window stays visible to operators even after the sidecar recovers. The breaker re-probes on the next sync cycle.
  4. Under =preferred, the same mid-flight loss falls back to the userland envelope (honest ~100 µs warning surfaced). Choose =required when your downstream consumers genuinely cannot tolerate the SW envelope; choose =preferred when best-effort sub-µs is good enough.

Implementation reference: lib/ptp-hwstamp/README.md in the customer dedicated-deployment repo bundle.