Documentation
Complete guides for running SDN nodes, using the JavaScript SDK, and integrating with the API.
Overview
Space Data Network (SDN) is a decentralized peer-to-peer network for exchanging standardized space situational awareness data. Built on libp2p with FlatBuffers serialization, SDN enables real-time sharing of orbital data, conjunction warnings, and tracking information.
Network Ecosystem
The diagram below shows how satellites, sensors, operations centers, independent contributors, commercial operators, and data consumers interconnect through the decentralized SDN mesh. All participants exchange standardized Space Data Standards messages peer-to-peer with no central server.
Network Components
| Component | Description | Use Case |
|---|---|---|
| Full Node | Server-side Go application | Infrastructure providers, relay operators |
| Edge Relay | WebSocket bridge for browsers | Connecting browser clients to the network |
| JavaScript SDK | Browser/Node.js client library | Web applications, desktop apps |
Quick Start
Desktop Application
Download the SDN Desktop app for macOS, Windows, or Linux. It includes a bundled IPFS node and the full SDN Web UI.
# Clone and build from source
git clone https://github.com/DigitalArsenal/space-data-network.git
cd space-data-network
npm run install:all
npm run desktop
The desktop app provides a graphical interface for managing peers, browsing schemas, monitoring network stats, and configuring subscriptions.
Server (Go)
Run a headless SDN node for production deployments, data providers, and always-on infrastructure.
# Initialize configuration
./spacedatanetwork init
# Start the node
./spacedatanetwork daemon
See Full Node Setup and Edge Relay Deployment for detailed configuration.
Browser (JavaScript)
Integrate SDN directly into web applications using the JavaScript SDK.
import { SDNNode } from './sdn-js/dist/esm/index.js';
const node = new SDNNode();
await node.start();
// Subscribe to Orbital Mean-Elements Messages
node.subscribe('OMM', (data, peerId) => {
console.log(`Received OMM from ${peerId}:`, data);
});
// Publish data
await node.publish('OMM', {
OBJECT_NAME: 'ISS (ZARYA)',
OBJECT_ID: '1998-067A',
EPOCH: '2025-01-24T12:00:00.000Z',
MEAN_MOTION: 15.5,
ECCENTRICITY: 0.0001,
INCLINATION: 51.6,
RA_OF_ASC_NODE: 200.0,
ARG_OF_PERICENTER: 100.0,
MEAN_ANOMALY: 50.0
});
Installation
JavaScript SDK
# npm
npm --prefix sdn-js install
# yarn
npm --prefix sdn-js install
Server Binary
# Linux amd64
curl -Lo spacedatanetwork \
https://github.com/DigitalArsenal/space-data-network/releases/latest/download/spacedatanetwork-linux-amd64
# macOS arm64 (Apple Silicon)
curl -Lo spacedatanetwork \
https://github.com/DigitalArsenal/space-data-network/releases/latest/download/spacedatanetwork-darwin-arm64
chmod +x spacedatanetwork
Full Node Setup
Run a full SDN node to contribute to the network's routing, relay traffic for firewalled peers, and store pinned content.
Requirements
- Public IP address - Required for other peers to connect directly
- Open ports: 4001/tcp (libp2p), 4001/udp (QUIC), 8080/tcp (HTTP API)
- Storage: Minimum 1GB recommended
- Memory: 512MB minimum, 2GB+ for busy nodes
Configuration
# Initialize default config
./spacedatanetwork init
This creates ~/.spacedatanetwork/ with:
config.yaml— Node configuration (YAML format)keys/mnemonic— BIP-39 mnemonic, encrypted at rest with XChaCha20-Poly1305 (see Key Security)keys/node.key— Serialized libp2p private key (backward compatibility)data/— SQLite database and pinned content
Configuration File
The server uses YAML configuration at ~/.spacedatanetwork/config.yaml (override with --config flag).
mode: full
network:
listen:
- /ip4/0.0.0.0/tcp/4001
- /ip4/0.0.0.0/tcp/8080/ws
- /ip4/0.0.0.0/udp/4001/quic-v1
bootstrap:
- /dnsaddr/bootstrap.digitalarsenal.io/p2p/QmBootstrap1
enable_relay: true
max_connections: 1000
storage:
path: /home/sdn/.spacedatanetwork/data
max_size: 10GB
gc_interval: 1h
security:
# key_password: "" # Optional: explicit mnemonic password
# Default: machine-derived via Argon2
# Override: SDN_KEY_PASSWORD env var
admin:
enabled: true
listen_addr: "0.0.0.0:443"
require_auth: true
session_expiry: 24h
tls_enabled: true
tls_cert_file: /etc/ssl/certs/your-cert.pem
tls_key_file: /etc/ssl/private/your-key.pem
ipfs_api_url: "http://127.0.0.1:5002"
# HD wallet users (xpub-based authentication)
users:
- xpub: "xpub6..."
trust_level: admin
name: Operator
Running as a Service
Create /etc/systemd/system/spacedatanetwork.service:
[Unit]
Description=Space Data Network Node
After=network.target
[Service]
Type=simple
User=sdn
ExecStart=/usr/local/bin/spacedatanetwork daemon
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
sudo systemctl enable spacedatanetwork
sudo systemctl start spacedatanetwork
CLI Commands
The spacedatanetwork binary provides several subcommands for node management and identity inspection.
daemon
Start the SDN server node (default command).
spacedatanetwork daemon --config /path/to/config.yaml
| Flag | Default | Description |
|---|---|---|
--config | ~/.spacedatanetwork/config.yaml | Path to configuration file |
--wasm | ~/.spacedatanetwork/wasm/hd-wallet.wasm | Path to hd-wallet WASM binary |
show-identity
Display the node’s cryptographic identity derived from its BIP-39 mnemonic. Prints PeerID to stdout (for scripting) and full details to stderr.
spacedatanetwork show-identity \
--config /path/to/config.yaml \
--wasm /path/to/hd-wallet.wasm
| Flag | Default | Description |
|---|---|---|
--config | ~/.spacedatanetwork/config.yaml | Path to configuration file |
--wasm | ~/.spacedatanetwork/wasm/hd-wallet.wasm | Path to hd-wallet WASM binary |
--show-mnemonic | false | Also display the decrypted mnemonic (use with caution) |
Example output:
$ spacedatanetwork show-identity --wasm /opt/sdn/wasm/hd-wallet.wasm
=== SDN Node Identity ===
XPub: xpub6DKCyLbCHZLFR...FZ3Wa
PeerID: 16Uiu2HAm1LbvwjEH...Fm45
Signing Key: 0d80e1fd5f...42fc8c (Ed25519)
Encryption Key: 08ea56d043...191741 (X25519)
Derivation Paths:
Identity: m/44'/0'/0' (secp256k1 → PeerID + XPub)
Signing: m/44'/0'/0'/0'/0' (Ed25519, SLIP-10)
Encryption: m/44'/0'/0'/1'/0' (X25519, SLIP-10)
# Get just the PeerID for scripting:
PEER_ID=$(spacedatanetwork show-identity --wasm /opt/sdn/wasm/hd-wallet.wasm 2>/dev/null)
derive-xpub
Derive an xpub from a BIP-39 mnemonic for use in the users config section. The xpub is the node’s public identity — safe to share and used for authentication.
spacedatanetwork derive-xpub --wasm /path/to/hd-wallet.wasm
You will be prompted to enter a mnemonic. The derived xpub is printed to stdout.
Both show-identity and derive-xpub print their primary output (PeerID or xpub) to stdout and diagnostics to stderr, making them easy to use in scripts with standard redirects.
init
Initialize a new SDN data directory with default configuration and a fresh BIP-39 mnemonic (encrypted at rest).
spacedatanetwork init --config /path/to/config.yaml
Edge Relay Deployment
Edge relays bridge browser clients (which can only use WebSocket/WebRTC) to the full libp2p network.
Edge relays accept WebSocket connections from browsers, translate to libp2p protocols, participate in GossipSub, and forward messages bidirectionally.
Command Line Options
| Option | Default | Description |
|---|---|---|
--listen | :8080 | HTTP/WebSocket listen address |
--ws-path | /ws | WebSocket endpoint path |
--max-connections | 500 | Maximum concurrent WebSocket connections |
--cors-origins | * | Allowed CORS origins |
Production with TLS (nginx)
server {
listen 443 ssl;
server_name relay.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location /ws {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
}
Client Configuration
import { SDNNode } from './sdn-js/dist/esm/index.js';
const node = new SDNNode({
relays: [
'wss://relay.digitalarsenal.io/ws',
'wss://relay2.digitalarsenal.io/ws'
],
relayStrategy: 'failover' // or 'round-robin'
});
await node.start();
Docker Deployment
Full Node
docker run -d \
--name spacedatanetwork \
-p 4001:4001 \
-p 4001:4001/udp \
-v sdn-data:/root/.spacedatanetwork \
ghcr.io/digitalarsenal/spacedatanetwork:latest
Edge Relay (Kubernetes)
apiVersion: apps/v1
kind: Deployment
metadata:
name: sdn-edge
spec:
replicas: 3
selector:
matchLabels:
app: sdn-edge
template:
spec:
containers:
- name: edge
image: ghcr.io/digitalarsenal/spacedatanetwork-edge:latest
ports:
- containerPort: 8080
resources:
requests:
memory: "256Mi"
cpu: "250m"
---
apiVersion: v1
kind: Service
metadata:
name: sdn-edge
spec:
type: LoadBalancer
sessionAffinity: ClientIP
ports:
- port: 443
targetPort: 8080
selector:
app: sdn-edge
JavaScript SDK
Installation
npm --prefix sdn-js install
Browser (ES Modules)
<script type="module">
import { SDNNode } from './sdn-js/dist/esm/index.js';
const node = new SDNNode();
await node.start();
</script>
Browser (UMD)
<script src="./sdn-js/dist/umd/sdn.min.js"></script>
<script>
const node = new SDN.SDNNode();
node.start().then(() => console.log('Connected!'));
</script>
SDNNode Options
| Option | Type | Default | Description |
|---|---|---|---|
relays | string[] | Built-in | WebSocket relay URLs |
relayStrategy | 'failover' | 'round-robin' | 'failover' | Relay selection strategy |
identity | Identity | Generated | Ed25519 identity |
storage | 'memory' | 'indexeddb' | 'memory' | Data storage backend |
Methods
// Start/stop
await node.start();
await node.stop();
// Subscribe to schema
node.subscribe('OMM', (data, peerId, signature) => {
console.log(data.OBJECT_NAME);
});
// Unsubscribe
node.unsubscribe('OMM');
// Publish data
await node.publish('OMM', { ...ommData });
// Get connected peers
const peers = await node.getPeers();
// Get network stats
const stats = await node.getStats();
Events
node.on('connected', () => console.log('Connected'));
node.on('disconnected', () => console.log('Disconnected'));
node.on('peer:connect', (peerId) => console.log(`Peer: ${peerId}`));
node.on('error', (error) => console.error(error));
Identity Management
import { Identity } from './sdn-js/dist/esm/index.js';
// Generate new identity
const identity = await Identity.generate();
console.log(identity.peerId); // Qm...
console.log(identity.publicKey); // Base64 public key
// From seed phrase (BIP-39)
const identity = await Identity.fromMnemonic(
'abandon abandon abandon...'
);
// Export/Import (encrypted)
const exported = await identity.export('password123');
const imported = await Identity.import(exported, 'password123');
Astrodynamics Library
The SDK includes a complete astrodynamics library for propagation and conjunction assessment.
import { propagate, sgp4 } from './sdn-js/dist/esm/astro/index.js';
// SGP4 propagation from TLE
const state = sgp4(tle, epochMinutes);
// { position: [x, y, z], velocity: [vx, vy, vz] }
// Propagate OMM
const states = propagate(omm, {
start: new Date('2025-01-24T00:00:00Z'),
end: new Date('2025-01-25T00:00:00Z'),
step: 60 // seconds
});
import { screenConjunctions, computePc } from './sdn-js/dist/esm/astro/index.js';
// Screen for close approaches
const conjunctions = screenConjunctions(primaryOMM, secondaryOMMs, {
threshold: 5000, // meters
timeWindow: 7 * 24 * 3600 // 7 days
});
// Compute collision probability
const pc = computePc(cdm);
import { eciToEcef, ecefToLla } from './sdn-js/dist/esm/astro/index.js';
// ECI to ECEF
const ecef = eciToEcef(eci, gmst);
// ECEF to Lat/Lon/Alt
const lla = ecefToLla(ecef);
// { lat: 51.5, lon: -0.1, alt: 400000 }
Identity & Key Derivation
Every SDN node derives its full cryptographic identity from a BIP-39 mnemonic generated client-side. No central authority issues keys — identity is fully decentralized. Using SLIP-10 hierarchical deterministic derivation with the standard BIP-44 Bitcoin derivation path (coin type 0), one seed produces all the keys a node needs.
Derivation Tree
BIP-39 Mnemonic (generated client-side)
│
▼ PBKDF2
512-bit Seed
│
├── BIP-32 secp256k1 Master Key
│ │
│ └── m/44'/0'/0' → Identity Key (secp256k1)
│ ├── xpub → Master Network Identity
│ └── PeerID → libp2p address (16Uiu2...)
│
└── SLIP-10 Ed25519 Master Key
│
├── m/44'/0'/0'/0'/0' → Ed25519 Signing Key (auth, data signatures)
│
└── m/44'/0'/0'/1'/0' → X25519 Encryption Key (ECDH, ECIES)
The xpub (extended public key) at m/44'/0'/account' serves as the node's master network identity. It encodes a secp256k1 public key from which the libp2p PeerID is deterministically derived. Anyone with the xpub can compute the PeerID without access to private key material.
The Ed25519 signing key is bound to the xpub identity on first login via TOFU (Trust On First Use) and is used for authentication and data signatures. The X25519 encryption key provides end-to-end encryption between peers.
Why BIP-44?
SDN reuses the standard BIP-44 HD wallet path structure with Bitcoin's coin type 0:
- Deterministic, wallet-native identity. By using a BIP-44-style path (
m/44'/0'/account'/change'/index'), SDN nodes can derive signing and encryption keys from the same BIP-39 mnemonic a user already has for their cryptocurrency wallets. One seed, many independent key trees. - Multi-account support. The
account'segment enables a single mnemonic to manage multiple SDN identities (e.g., account 0 for an operator, account 1 for a sensor, account 2 for an analytics service), each with its own independent signing and encryption key pair.
JavaScript SDK
import { initHDWallet, deriveIdentity, mnemonicToSeed } from './sdn-js/dist/esm/index.js';
// Initialize WASM module
await initHDWallet();
// Generate or load mnemonic
const mnemonic = 'abandon abandon abandon ...'; // generated client-side
const seed = await mnemonicToSeed(mnemonic);
// Derive full identity (signing + encryption keys)
const identity = await deriveIdentity(seed, 0); // account 0
console.log(identity.signingKey.publicKey); // Ed25519 pub (32 bytes)
console.log(identity.encryptionKey.publicKey); // X25519 pub (32 bytes)
console.log(identity.signingKeyPath); // m/44'/0'/0'/0'/0'
console.log(identity.encryptionKeyPath); // m/44'/0'/0'/1'/0'
Go Server
// The server automatically derives identity from its encrypted mnemonic on startup.
// The mnemonic is encrypted at rest with XChaCha20-Poly1305 (see Key Security).
//
// Files in ~/.spacedatanetwork/keys/:
// mnemonic - BIP-39 mnemonic (encrypted at rest)
// node.key - Serialized libp2p private key (backward compat)
//
// Inspect the node identity without starting the daemon:
// spacedatanetwork show-identity --wasm /path/to/hd-wallet.wasm
hw, _ := wasm.NewHDWalletModule(ctx, "/path/to/hd-wallet.wasm")
seed, _ := hw.MnemonicToSeed(ctx, mnemonic, "")
identity, _ := hw.DeriveIdentity(ctx, seed, 0)
xpub, _ := hw.DeriveXPub(ctx, seed, 0)
fmt.Println(xpub) // BIP-32 xpub (secp256k1)
fmt.Println(identity.PeerID) // libp2p PeerID (from secp256k1)
fmt.Println(identity.SigningKeyPath) // m/44'/0'/0'/0'/0'
fmt.Println(identity.EncryptionKeyPath) // m/44'/0'/0'/1'/0'
Secp256k1 Identity (xpub & PeerID)
The secp256k1 identity key at m/44'/0'/account' is the root of a node's network identity. It is derived using standard BIP-32 hierarchical deterministic derivation on the secp256k1 curve — the same curve and derivation used by Bitcoin, Ethereum, and most cryptocurrency wallets.
xpub (Extended Public Key)
The xpub is a Base58Check-encoded extended public key (prefix xpub6...) that encodes:
- The 33-byte compressed secp256k1 public key
- A 32-byte chain code for child key derivation
- Depth, parent fingerprint, and child index metadata
The xpub serves as the node's portable identity. It is safe to share publicly and is used in the users config section for authentication:
users:
- xpub: "xpub6DKCyLbCHZLFR4XpFg26royZdkxExSMHTjNorEgkn1kgvQbLF5sts9RfNt3PbGhphVUh7WsFQ5H6GJBh4LhmRL27oSPt1qDkJ5mAr6FZ3Wa"
trust_level: admin
name: Operator
PeerID from secp256k1
The libp2p PeerID is deterministically derived from the compressed secp256k1 public key:
secp256k1 pubkey (33 bytes, compressed)
│
▼ Protobuf encode (libp2p crypto key format)
PublicKey { Type: Secp256k1, Data: [33 bytes] }
│
▼ SHA-256 multihash (identity CID for keys ≤42 bytes uses inline)
PeerID: 16Uiu2HAm1LbvwjEH...Fm45
Secp256k1 PeerIDs have a 16Uiu2... prefix, distinguishing them from Ed25519 PeerIDs (12D3Koo...). Because the PeerID is derived purely from the xpub's public key, anyone with an xpub can compute the corresponding PeerID without any private key material.
Why secp256k1 for Identity?
- Wallet compatibility — The same BIP-39 mnemonic used for Bitcoin/Ethereum wallets produces the SDN identity key. No separate key management needed.
- xpub portability — BIP-32 xpubs are a widely-understood, standardized format. Operators can share an xpub to grant access, and any SDN node can verify identity from it.
- 1:1 xpub↔PeerID mapping — Every xpub maps to exactly one PeerID and vice versa, enabling deterministic peer discovery from identity alone.
- Multi-account isolation — Different
account'indices produce completely independent identities from the same seed, each with its own xpub, PeerID, signing key, and encryption key.
Computing PeerID from xpub
// JavaScript (hd-wallet-wasm)
import init from 'hd-wallet-wasm';
const { libp2p } = await init();
const peerId = libp2p.peerIdFromXpub('xpub6DKCyLb...');
// → "16Uiu2HAm1LbvwjEH...Fm45"
// Go server CLI
spacedatanetwork show-identity --wasm /path/to/hd-wallet.wasm
# PeerID printed to stdout
// Go (programmatic)
hw, _ := wasm.NewHDWalletModule(ctx, wasmPath)
seed, _ := hw.MnemonicToSeed(ctx, mnemonic, "")
identity, _ := hw.DeriveIdentity(ctx, seed, 0) // account 0
fmt.Println(identity.PeerID()) // "16Uiu2HAm..."
Signing & Authentication
SDN uses a two-layer identity model:
- Network Identity (secp256k1) — The secp256k1 key at
m/44'/0'/account'produces the xpub and libp2p PeerID. The PeerID is a multihash of the compressed secp256k1 public key, giving every xpub a unique, deterministic network address (prefix16Uiu2...). - Digital Signatures (Ed25519) — The Ed25519 key at
m/44'/0'/account'/0'/0'signs data (SDS schemas, messages, authentication challenges). Recipients verify signatures using the corresponding 32-byte public key.
The Ed25519 signing key is bound to the secp256k1 identity via TOFU (Trust On First Use): on the first wallet-based login, the server records the Ed25519 public key alongside the xpub. Subsequent logins require both the correct xpub and a valid Ed25519 challenge-response signature.
// Sign data with the identity's Ed25519 key
const { sign, verify } = await import('./sdn-js/dist/esm/index.js');
const message = new TextEncoder().encode('ISS conjunction alert');
const signature = await sign(identity.signingKey.privateKey, message);
// Anyone with the public key (or PeerID) can verify
const valid = await verify(identity.signingKey.publicKey, message, signature);
console.log(valid); // true
// Go: sign and verify using the DerivedIdentity
sig, _ := identity.Sign([]byte("ISS conjunction alert"))
ok, _ := identity.Verify([]byte("ISS conjunction alert"), sig)
// ok == true
Encryption (X25519 ECDH)
The encryption key at path m/44'/0'/account'/1'/0' is derived from the same seed as the signing key but at a different derivation path (change index 1 instead of 0). This produces an X25519 key pair used for:
- ECDH Key Exchange — Two peers combine their X25519 keys to derive a shared secret without transmitting it.
- AES-256-GCM Encryption — The shared secret encrypts data end-to-end. Only the intended recipient can decrypt.
import { encryptBytes, decryptBytes, deriveIdentity } from './sdn-js/dist/esm/index.js';
// Alice and Bob each derive their identity
const alice = await deriveIdentity(aliceSeed, 0);
const bob = await deriveIdentity(bobSeed, 0);
// Alice encrypts a message for Bob using his X25519 public key
const plaintext = new TextEncoder().encode('Classified orbital data');
const encrypted = await encryptBytes(
plaintext,
bob.encryptionKey.publicKey, // Bob's X25519 public key
alice.encryptionKey.privateKey // Alice's X25519 private key
);
// Bob decrypts using Alice's public key and his private key
const decrypted = await decryptBytes(
encrypted,
alice.encryptionKey.publicKey, // Alice's X25519 public key
bob.encryptionKey.privateKey // Bob's X25519 private key
);
console.log(new TextDecoder().decode(decrypted)); // 'Classified orbital data'
Key Separation
Using separate derivation paths for signing and encryption follows cryptographic best practice. A compromised encryption key does not affect the signing key (and vice versa), even though both derive from the same master seed.
ECIES (Elliptic Curve Integrated Encryption)
The underlying hd-wallet-wasm library provides a unified ECIES API that combines ECDH key agreement, HKDF-SHA256 key derivation, and AES-256-GCM authenticated encryption into a single encrypt/decrypt interface.
- Supported curves: secp256k1, P-256 (NIST), P-384 (NIST), X25519
- FIPS mode: When enabled, restricts to P-256/P-384 and routes HKDF + AES-GCM through OpenSSL FIPS provider
- Wire format:
[ephemeral_pubkey][iv (12B)][ciphertext][tag (16B)]
| Curve | Ephemeral Key | Overhead |
|---|---|---|
| secp256k1 | 33 bytes | 61 bytes |
| P-256 | 33 bytes | 61 bytes |
| P-384 | 49 bytes | 77 bytes |
| X25519 | 32 bytes | 60 bytes |
AES-CTR Mode
Counter mode encryption is available for streaming use cases. Unlike AES-GCM, CTR mode does not provide authentication — pair with HMAC-SHA256 if message integrity is required. Supports AES-128, AES-192, and AES-256 (key size selects variant). Uses 16-byte IV.
FlatBuffers Field Encryption
The flatc-wasm package provides per-field AES-256-CTR encryption for FlatBuffer data via the encryption.mjs module. Fields marked with the (encrypted) attribute are transparently encrypted with unique keys derived per field and per record.
import {
loadEncryptionWasm, EncryptionContext,
x25519GenerateKeyPair, encryptionHeaderFromJSON,
} from 'flatc-wasm';
await loadEncryptionWasm();
// Sender creates an ECIES encryption session
const ctx = EncryptionContext.forEncryption(recipientPubKey, {
algorithm: 'x25519', // or 'secp256k1'
context: 'sdn-orbit-v1' // HKDF domain separation
});
// Encrypt fields in a FlatBuffer
ctx.encryptScalar(buffer, offset, length, fieldId, recordIndex);
const headerJSON = ctx.getHeaderJSON(); // send to recipient
// Recipient decrypts
const decCtx = EncryptionContext.forDecryption(
myPrivateKey,
encryptionHeaderFromJSON(headerJSON),
'sdn-orbit-v1'
);
decCtx.decryptScalar(buffer, offset, length, fieldId, recordIndex);
The module also provides authenticated encryption (AES-CTR + HMAC-SHA256), standalone HKDF, SHA-256, and signature operations for secp256k1, P-256, P-384, and Ed25519. P-256/P-384 operations use the Web Crypto API for FIPS-grade implementations.
| Operation | Backend | Async |
|---|---|---|
| AES-256-CTR, HKDF, SHA-256 | WASM (Crypto++/OpenSSL) | No |
| X25519 ECDH | WASM | No |
| secp256k1 ECDH / ECDSA | WASM | No |
| P-256 ECDH / ECDSA | Web Crypto | Yes |
| P-384 ECDH / ECDSA | Web Crypto | Yes |
| Ed25519 sign/verify | WASM | No |
| HMAC-SHA256 | JS (via WASM SHA-256) | No |
Key Security
The BIP-39 mnemonic is the root secret from which all node keys are derived. SDN encrypts it at rest using XChaCha20-Poly1305 authenticated encryption with a key derived from Argon2id.
Encryption at Rest
File format: salt (32 bytes) || nonce (24 bytes) || ciphertext
Permissions: 0600 (owner-only read/write)
Cipher: XChaCha20-Poly1305 (AEAD)
KDF: Argon2id (time=3, memory=64 MiB, threads=4, keyLen=32)
On first startup (or when a plaintext mnemonic is detected), the server automatically encrypts the mnemonic file in place. Subsequent reads decrypt transparently.
Password Resolution
The encryption password is resolved in order of priority:
SDN_KEY_PASSWORDenvironment variable (highest priority)security.key_passwordinconfig.yaml- Machine-derived password — deterministic Argon2id hash of
hostname:GOARCH:GOOSwith the user's home directory as salt (allows unattended startup)
The default machine-derived password prevents trivial offline reads of the mnemonic file but is not a substitute for a strong explicit password. For production deployments, always set SDN_KEY_PASSWORD or security.key_password.
Configuration Example
# Option 1: Environment variable (recommended for production)
export SDN_KEY_PASSWORD="your-strong-passphrase"
spacedatanetwork daemon
# Option 2: Config file
security:
key_password: "your-strong-passphrase"
# Option 3: Machine-derived (default, for unattended startup)
# No configuration needed — password is derived automatically
Auto-Migration
If the server detects a plaintext mnemonic file (from an older installation), it automatically encrypts it using the resolved password. The original plaintext is overwritten with the encrypted blob. This migration is logged as a warning at startup.
IPFS Pinning Mechanics
SDN uses IPFS-style content addressing to identify data. Every piece of data published on the network gets a Content Identifier (CID): a cryptographic hash that uniquely identifies the content.
How Content Addressing Works
1. Data (e.g., an OMM FlatBuffer) → SHA-256 hash → CID
bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi
2. The CID is announced on the DHT (Kademlia distributed hash table)
3. Any node can retrieve the data by requesting the CID from the network
4. The hash guarantees integrity: if the data changes, the CID changes
Pinning = Persistent Availability
By default, data flowing through the network is transient — nodes process and forward it but don't store it permanently. Pinning is the act of committing to store a CID and serve it to the network on demand.
- Self-hosted nodes can pin any data for free — you control your own storage.
- SpaceAware.io (our hosted premium node) charges a per-GB/month fee for persistent pinning, providing guaranteed availability and SLA-backed uptime.
- Operators running their own marketplace nodes can set their own pinning fees or offer pinning for free.
Signed Pinning
When a node pins data, it signs the pin announcement with its Ed25519 signing key. This creates a verifiable chain:
Author signs data → CID (content hash) → Pin announcement (signed)
Verifiers can confirm:
1. The data matches the CID (content integrity)
2. The CID was published by a specific PeerID (author attribution)
3. The pin was committed by a specific node (availability commitment)
Because the signing key and PeerID are the same key (see Signing & IPFS), author attribution is built into the protocol — no separate identity layer is needed.
REST API
The HTTP API is available on the configured API port (default: 8080).
Base URL
http://localhost:8080/api/v1
Node Information
GET /id # Get node identity
GET /version # Get software version
GET /stats # Get node statistics
Peers
GET /peers # List connected peers
GET /peers/{peerId} # Get specific peer info
POST /peers/connect # Connect to a peer
DELETE /peers/{peerId} # Disconnect from a peer
PubSub
GET /pubsub/topics # List subscribed topics
POST /pubsub/subscribe # Subscribe to a topic
DELETE /pubsub/subscribe/{topic} # Unsubscribe
POST /pubsub/publish # Publish a message
GET /pubsub/messages # Get recent messages
Content
GET /content/{cid} # Retrieve content by CID
POST /content # Add content to local storage
GET /content/{cid}/pin # Check if content is pinned
POST /content/{cid}/pin # Pin content locally
DELETE /content/{cid}/pin # Unpin content
GET /pins # List all pinned CIDs
DHT
GET /dht/providers/{cid} # Find providers for a CID
POST /dht/provide/{cid} # Announce this node provides a CID
GET /dht/findpeer/{peerId} # Find addresses for a peer
WebSocket API
Connect
ws://localhost:8080/ws
Subscribe
{
"type": "subscribe",
"topic": "OMM"
}
Publish
{
"type": "publish",
"topic": "OMM",
"data": { ... }
}
Receive Message
{
"type": "message",
"topic": "OMM",
"cid": "bafy...",
"from": "Qm...",
"timestamp": "2025-01-24T12:00:00Z",
"data": { ... }
}
Error Handling
All errors return JSON:
{
"error": {
"code": "INVALID_REQUEST",
"message": "Missing required field: topic",
"details": { }
}
}
| Code | HTTP Status | Description |
|---|---|---|
INVALID_REQUEST | 400 | Malformed request |
UNAUTHORIZED | 401 | Missing/invalid auth |
FORBIDDEN | 403 | Insufficient permissions |
NOT_FOUND | 404 | Resource not found |
VALIDATION_ERROR | 422 | Schema validation failed |
RATE_LIMITED | 429 | Too many requests |
INTERNAL_ERROR | 500 | Server error |
Rate Limiting
Default limits: 100 requests/minute for reads, 10 requests/minute for writes.
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1706097600
Schema Reference
SDN supports all Space Data Standards (SDS) schemas. All data on the network must conform to these schemas.
| Category | Schemas | Purpose |
|---|---|---|
| Orbit | OMM, OEM, OCM, OSM | Orbital elements and ephemeris |
| Conjunction | CDM, CSM | Close approach and collision data |
| Tracking | TDM, RFM | Observation and measurement data |
| Catalog | CAT, SIT | Object catalogs and identification |
| Entity | EPM, PNM | Organization and personnel info |
| Maneuver | MET, MPE | Maneuver planning and execution |
| Propagation | HYP, EME, EOO, EOP | Propagation parameters |
| Reference | LCC, LDM, CRM, CTR | Launch and reference data |
| Other | ATM, BOV, IDM, PLD, PRG, REC, ROC, SCM, TIM, VCM | Specialized data types |
OMM - Orbit Mean-Elements Message
Standard format for mean orbital elements (TLE-equivalent).
interface OMM {
OBJECT_NAME: string; // Satellite name
OBJECT_ID: string; // International designator
CENTER_NAME: string; // Central body (EARTH)
REF_FRAME: string; // Reference frame (TEME)
TIME_SYSTEM: string; // Time system (UTC)
MEAN_ELEMENT_THEORY: string; // Theory (SGP4)
EPOCH: string; // Epoch (ISO 8601)
MEAN_MOTION: number; // Rev/day
ECCENTRICITY: number; // Dimensionless
INCLINATION: number; // Degrees
RA_OF_ASC_NODE: number; // Degrees
ARG_OF_PERICENTER: number; // Degrees
MEAN_ANOMALY: number; // Degrees
// ... additional optional fields
}
OEM - Orbit Ephemeris Message
High-precision state vectors over time.
interface OEM {
OBJECT_NAME: string;
OBJECT_ID: string;
CENTER_NAME: string;
REF_FRAME: string;
START_TIME: string;
STOP_TIME: string;
EPHEMERIS: EphemerisDataLine[];
}
interface EphemerisDataLine {
EPOCH: string;
X: number; // km
Y: number; // km
Z: number; // km
X_DOT: number; // km/s
Y_DOT: number; // km/s
Z_DOT: number; // km/s
}
CDM - Conjunction Data Message
Standard format for close approach warnings.
interface CDM {
CCSDS_CDM_VERS: string;
CREATION_DATE: string;
ORIGINATOR: string;
MESSAGE_ID: string;
// Conjunction info
TCA: string; // Time of Closest Approach
MISS_DISTANCE: number; // meters
RELATIVE_SPEED: number; // m/s
COLLISION_PROBABILITY?: number; // 0-1
// Object 1
OBJECT1_OBJECT_DESIGNATOR: string;
OBJECT1_OBJECT_NAME: string;
OBJECT1_INTERNATIONAL_DESIGNATOR: string;
// ... state and covariance
// Object 2
OBJECT2_OBJECT_DESIGNATOR: string;
// ... same fields as Object 1
}
EPM - Entity Profile Manifest
Organization/entity identification with cryptographic keys.
interface EPM {
ENTITY_ID: string; // Unique identifier
ENTITY_NAME: string; // Organization name
ENTITY_TYPE: string; // OPERATOR, AGENCY, etc.
LEGAL_NAME?: string;
CONTACT_EMAIL?: string;
PUBLIC_KEY: string; // Ed25519 public key (base64)
KEY_ALGORITHM: string; // ED25519
CREATED: string; // ISO 8601
UPDATED: string; // ISO 8601
}
Schema Validation
import { SchemaRegistry } from './sdn-js/dist/esm/index.js';
// Validate data against schema
const errors = SchemaRegistry.validate('OMM', data);
if (errors.length > 0) {
console.error('Validation failed:', errors);
}
// List all schemas
const schemas = SchemaRegistry.list();
// ['OMM', 'OEM', 'CDM', 'EPM', ...]
Converting from TLE
import { tleToOMM } from './sdn-js/dist/esm/index.js';
const tle = [
'1 25544U 98067A 25024.50000000 .00016717 00000-0 10270-3 0 9029',
'2 25544 51.6400 200.0000 0001000 100.0000 50.0000 15.50000000000000'
];
const omm = tleToOMM(tle);
Go FlatBuffer Builders
The internal/sds package provides fluent builders for creating FlatBuffer messages that conform to Space Data Standards.
Available Builders
| Schema | Builder | Description |
|---|---|---|
| OMM | NewOMMBuilder() | Orbit Mean-Elements Message |
| EPM | NewEPMBuilder() | Entity Profile Message |
| PNM | NewPNMBuilder() | Publish Notification Message |
| CAT | NewCATBuilder() | Catalog Entry |
OMM Builder Example
import "github.com/spacedatanetwork/sdn-server/internal/sds"
data := sds.NewOMMBuilder().
WithObjectName("ISS (ZARYA)").
WithObjectID("1998-067A").
WithNoradCatID(25544).
WithEpoch("2024-01-15T12:00:00.000Z").
WithMeanMotion(15.49).
WithEccentricity(0.0001215).
WithInclination(51.6434).
WithRaOfAscNode(178.1234).
WithArgOfPericenter(85.5678).
WithMeanAnomaly(274.9012).
Build()
EPM Builder Example
data := sds.NewEPMBuilder().
WithDN("John Doe").
WithLegalName("Acme Corporation").
WithFamilyName("Doe").
WithGivenName("John").
WithEmail("john@acme.com").
WithTelephone("+1-555-0100").
WithAddress("123 Main St", "Springfield", "IL", "62701", "USA").
WithJobTitle("Chief Engineer").
WithKeys("signingKey123", "encryptionKey456").
Build()
Performance
FlatBuffers provide zero-copy deserialization with excellent performance:
| Operation | Time | Throughput |
|---|---|---|
| OMM Serialize | ~341ns | ~3.5M ops/sec |
| OMM Deserialize | ~4.6ns | ~257M ops/sec |
| EPM Serialize | ~614ns | ~2M ops/sec |
| EPM Deserialize | ~4.7ns | ~255M ops/sec |
| PNM Serialize | ~208ns | ~5.7M ops/sec |
| PNM Deserialize | ~4.7ns | ~250M ops/sec |
For complete throughput benchmarks including PNM and CAT schemas, batch processing, and parallel tests, see Throughput Benchmarks.
vCard/QR Code Conversion
The internal/vcard package provides bidirectional conversion between EPM FlatBuffers, vCard 4.0 format, and QR codes.
EPM to vCard Field Mapping
| EPM Field | vCard Property |
|---|---|
| DN | FN (Formatted Name) |
| LEGAL_NAME | ORG (Organization) |
| FAMILY_NAME, GIVEN_NAME | N (Structured Name) |
| TELEPHONE | TEL |
| ADDRESS | ADR |
| JOB_TITLE | TITLE |
| OCCUPATION | ROLE |
| KEYS (Signing) | X-SIGNING-KEY |
| KEYS (Encryption) | X-ENCRYPTION-KEY |
Conversion Examples
import "github.com/spacedatanetwork/sdn-server/internal/vcard"
// EPM to vCard string
vcardStr, err := vcard.EPMToVCard(epmBytes)
// vCard string to EPM
epmBytes, err := vcard.VCardToEPM(vcardStr)
// EPM to QR Code (PNG)
pngData, err := vcard.EPMToQR(epmBytes, 256) // 256x256 pixels
// QR Code to EPM
epmBytes, err := vcard.QRToEPM(pngData)
// Full roundtrip: EPM -> vCard -> QR -> vCard -> EPM
qrPNG, _ := vcard.EPMToQR(originalEPM, 512)
recoveredEPM, _ := vcard.QRToEPM(qrPNG)
The default size is 256x256 pixels. Maximum supported is 4096x4096. For typical entity profiles, 256-512 pixels is sufficient.
For complete test coverage of the EPM/vCard/QR conversion pipeline, see EPM/QR Conversion Tests.
PNM Tip/Queue System
The tip/queue system uses Publish Notification Messages (PNM) as the core messaging mechanism for content discovery. This is a fundamental design principle: not all pinned data should be broadcast. Instead, nodes announce content availability via lightweight PNM messages, allowing subscribers to selectively fetch and pin content based on configurable policies.
PNM-based messaging decouples content storage from content notification. When you pin data locally, you don't automatically broadcast it to everyone. Instead, you publish a PNM "tip" that announces the content's CID, schema type, and your signature. Subscribers decide whether to fetch based on their trust settings and schema preferences.
Why PNM-Based Messaging?
- Bandwidth efficiency: PNM messages are tiny (~100-200 bytes) compared to actual content (KB to MB)
- Selective consumption: Nodes only fetch content they're interested in
- Trust-based filtering: Configure which peers and schema types to auto-fetch
- Spam resistance: Malicious nodes can't flood the network with large payloads
- Verifiable announcements: PNMs include cryptographic signatures for authenticity
Architecture
Publisher Subscriber
| |
|-- Pin content locally |
|-- Create PNM with CID + sig |
|-- Broadcast PNM on /sdn/PNM ------|--> Receive PNM (lightweight notification)
| |-- Check config for peer + schema
| |-- If trusted + autoFetch: fetch by CID
| |-- If autoPin: pin with TTL
| |
| [Content NOT sent unless | [Content fetched on-demand
| explicitly requested] | based on subscriber policy]
Configuration Levels
Configuration supports per-source AND per-schema settings with priority resolution:
| Priority | Level | Description |
|---|---|---|
| 1 (highest) | Source+Schema | Specific peer + specific schema |
| 2 | Source | All schemas from a specific peer |
| 3 | Schema | Specific schema from any peer |
| 4 (lowest) | System | Global defaults |
Configuration Example
import "github.com/spacedatanetwork/sdn-server/internal/pubsub"
config := pubsub.NewTipQueueConfig()
// System defaults
config.DefaultAutoFetch = false
config.DefaultAutoPin = false
config.DefaultTTL = 24 * time.Hour
// Per-schema: always fetch conjunction data
config.SetSchemaDefault("CDM", &pubsub.SchemaConfig{
AutoFetch: true,
AutoPin: true,
TTL: 48 * time.Hour,
Priority: 10, // Critical data
})
// Per-source: trust a partner
config.SetSourceOverride("trusted-peer-id", &pubsub.SourceConfig{
Trusted: true,
AutoFetch: pubsub.BoolPtr(true),
TTL: pubsub.DurationPtr(72 * time.Hour),
})
// Per-source+schema: custom handling
config.SetSourceSchemaOverride("trusted-peer-id", "OMM", &pubsub.SchemaConfig{
AutoFetch: true,
AutoPin: true,
TTL: 168 * time.Hour, // 1 week
})
TipQueue Usage
// Create and configure
tq := pubsub.NewTipQueue(config)
tq.SetTopicManager(topicManager)
tq.SetFetcher(fetcher)
tq.SetPinner(pinner)
// Handle received tips
tq.OnTip(func(tip *pubsub.Tip, cfg pubsub.ResolvedConfig) {
log.Printf("Tip from %s: CID=%s Schema=%s",
tip.PeerID, tip.CID, tip.SchemaType)
})
// Start listening
tq.Subscribe()
// Publish your own content
tq.PublishTip(ctx, pubsub.PublishOptions{
CID: "bafybei...",
SchemaType: "OMM",
Signature: "0x...",
})
Configurable Options
| Option | Type | Description |
|---|---|---|
| AutoFetch | bool | Automatically fetch content by CID |
| AutoPin | bool | Automatically pin fetched content |
| TTL | duration | Time-to-live for pinned content |
| Priority | int | Queue priority (higher = more important) |
| Trusted | bool | Mark source as trusted |
Throughput Benchmarks
The SDN server includes comprehensive benchmark tests for measuring SpaceDataStandards message throughput. All benchmarks use the current SDS FlatBuffer schemas.
Running Benchmarks
# Run all SDS benchmarks
cd sdn-server
go test -bench=. ./internal/sds/
# Run specific schema benchmarks
go test -bench=BenchmarkOMM ./internal/sds/
go test -bench=BenchmarkEPM ./internal/sds/
go test -bench=BenchmarkPNM ./internal/sds/
go test -bench=BenchmarkCAT ./internal/sds/
# Run with memory allocation stats
go test -bench=. -benchmem ./internal/sds/
# Run schema comparison suite
go test -bench=BenchmarkSchemaComparison ./internal/sds/
Benchmark Results
Actual throughput on Apple M3 Ultra (results vary by hardware):
| Schema | Operation | Throughput | Latency | Memory |
|---|---|---|---|---|
| OMM | Serialize | ~3.5M ops/sec | ~341ns | 288 B/op |
| OMM | Deserialize | ~257M ops/sec | ~4.6ns | 0 B/op |
| EPM | Serialize | ~2M ops/sec | ~614ns | 592 B/op |
| EPM | Deserialize | ~255M ops/sec | ~4.7ns | 0 B/op |
| PNM | Serialize | ~5.7M ops/sec | ~208ns | 288 B/op |
| PNM | Deserialize | ~250M ops/sec | ~4.7ns | 0 B/op |
| CAT | Serialize | ~4.6M ops/sec | ~268ns | 224 B/op |
| CAT | Deserialize | ~255M ops/sec | ~4.7ns | 0 B/op |
FlatBuffers' 0 B/op for deserialization demonstrates true zero-copy access. The data is read directly from the buffer without allocating new memory, resulting in ~250M ops/sec throughput.
Batch Processing
# Test batch message processing (100 messages)
go test -bench=BenchmarkBatchProcessing ./internal/sds/
# Test parallel processing
go test -bench=BenchmarkParallelProcessing ./internal/sds/
Message Sizes
# Report message sizes in bytes
go test -bench=BenchmarkMessageSize ./internal/sds/
| Schema | Typical Size | Description |
|---|---|---|
| OMM | ~200-400 bytes | Orbit mean elements |
| EPM | ~300-800 bytes | Entity profile (varies with fields) |
| PNM | ~100-200 bytes | Publish notification (lightweight) |
| CAT | ~150-300 bytes | Catalog entry |
Roundtrip Tests
Roundtrip tests verify that data survives serialization and deserialization without loss.
Running Roundtrip Tests
# Run all SDS roundtrip tests
go test -v -run=Roundtrip ./internal/sds/
# Run vCard roundtrip tests
go test -v -run=Roundtrip ./internal/vcard/
# Run with race detection
go test -race -run=Roundtrip ./internal/...
What's Tested
- OMM roundtrip: All orbital elements preserved through serialize/deserialize
- EPM roundtrip: Entity profiles with addresses, keys, alternate names
- PNM roundtrip: Publish notifications with CID, signatures, timestamps
- CAT roundtrip: Catalog entries with orbital parameters
- vCard roundtrip: EPM to vCard to EPM conversion
- QR code roundtrip: EPM to QR to EPM conversion
Example Test Output
=== RUN TestOMMRoundtrip
--- PASS: TestOMMRoundtrip (0.00s)
=== RUN TestEPMRoundtripWithAddress
--- PASS: TestEPMRoundtripWithAddress (0.00s)
=== RUN TestEPMVCardRoundtrip
--- PASS: TestEPMVCardRoundtrip (0.00s)
=== RUN TestEPMQRFullRoundtrip
--- PASS: TestEPMQRFullRoundtrip (0.02s)
PASS
EPM / vCard / QR Code Conversion Tests
The complete conversion pipeline enables Entity Profile Messages to be shared as standard vCards and embedded in QR codes for easy scanning.
Conversion Flow
┌─────────────────────────────────────────────────────────────────┐
│ Full Conversion Pipeline │
└─────────────────────────────────────────────────────────────────┘
EPM (FlatBuffer) vCard 4.0 String
┌──────────────┐ ┌──────────────────┐
│ DN │ │ BEGIN:VCARD │
│ LEGAL_NAME │ ──EPMToVCard──► │ VERSION:4.0 │
│ FAMILY_NAME │ │ FN:John Doe │
│ GIVEN_NAME │ │ N:Doe;John;;; │
│ EMAIL │ ◄──VCardToEPM── │ ORG:Acme Corp │
│ TELEPHONE │ │ EMAIL:john@... │
│ ADDRESS │ │ ADR:;;123 Main...│
│ KEYS[] │ │ X-SIGNING-KEY:...│
└──────────────┘ └──────────────────┘
│ │
│ │
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ EPMToQR │ │ VCardToQR │
│ (direct) │ │ │
└──────────────┘ └──────────────────┘
│ │
└────────────┬───────────────────────┘
▼
┌──────────────────┐
│ QR Code PNG │
│ (scannable) │
└──────────────────┘
│
┌────────────┴───────────────────────┐
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ QRToEPM │ │ QRToVCard │
│ (direct) │ │ │
└──────────────┘ └──────────────────┘
Running EPM/QR Tests
# Run all vCard tests
go test -v ./internal/vcard/
# Run QR-specific tests
go test -v -run=QR ./internal/vcard/
# Run the full roundtrip test
go test -v -run=TestEPMQRFullRoundtrip ./internal/vcard/
# Run benchmarks
go test -bench=. ./internal/vcard/
Test Coverage
| Test | Description |
|---|---|
TestEPMToVCard | EPM FlatBuffer to vCard string conversion |
TestVCardToEPM | vCard string to EPM FlatBuffer conversion |
TestEPMVCardRoundtrip | EPM → vCard → EPM preserves all fields |
TestVCardEPMRoundtrip | vCard → EPM → vCard preserves all fields |
TestVCardToQR | vCard string to QR code PNG |
TestQRToVCard | QR code PNG to vCard string |
TestEPMToQR | Direct EPM to QR code conversion |
TestQRToEPM | Direct QR code to EPM conversion |
TestEPMQRFullRoundtrip | Complete: EPM → vCard → QR → vCard → EPM |
TestVCardQRRoundtrip | vCard → QR → vCard with various content |
Field Mapping
Complete mapping between EPM FlatBuffer fields and vCard 4.0 properties:
| EPM Field | vCard Property | Format |
|---|---|---|
| DN | FN | Single value |
| LEGAL_NAME | ORG | Single value |
| FAMILY_NAME | N (part 1) | family;given;additional;prefix;suffix |
| GIVEN_NAME | N (part 2) | |
| ADDITIONAL_NAME | N (part 3) | |
| HONORIFIC_PREFIX | N (part 4) | |
| HONORIFIC_SUFFIX | N (part 5) | |
| Single value | ||
| TELEPHONE | TEL | Single value |
| JOB_TITLE | TITLE | Single value |
| OCCUPATION | ROLE | Single value |
| ADDRESS.STREET | ADR (part 3) | ;;street;locality;region;postal;country |
| ADDRESS.LOCALITY | ADR (part 4) | |
| ADDRESS.REGION | ADR (part 5) | |
| ADDRESS.POSTAL_CODE | ADR (part 6) | |
| ADDRESS.COUNTRY | ADR (part 7) | |
| KEYS (Signing) | X-SIGNING-KEY | Custom extension |
| KEYS (Encryption) | X-ENCRYPTION-KEY | Custom extension |
| MULTIFORMAT_ADDRESS | URL | IPNS/IPFS addresses |
| ALTERNATE_NAMES | X-ALTERNATE-NAME | Custom extension (multiple) |
QR Code Specifications
- Default size: 256x256 pixels
- Maximum size: 4096x4096 pixels
- Error correction: Medium level (recovers ~15% damage)
- Format: PNG
- Typical capacity: ~1KB of vCard data
Conversion Throughput
Benchmark results on Apple M3 Ultra:
| Operation | Throughput | Latency |
|---|---|---|
| EPM to vCard | ~336K ops/sec | ~3.6\u00b5s |
| vCard to EPM | ~250K ops/sec | ~4.4\u00b5s |
| vCard to QR | ~1K ops/sec | ~1.6ms |
| QR to vCard | ~2.2K ops/sec | ~0.6ms |
| Full Roundtrip (EPM\u2192QR\u2192EPM) | ~673 ops/sec | ~1.7ms |
Code Example
import "github.com/spacedatanetwork/sdn-server/internal/vcard"
import "github.com/spacedatanetwork/sdn-server/internal/sds"
// Create an Entity Profile
epmBytes := sds.NewEPMBuilder().
WithDN("Dr. Jane Smith").
WithLegalName("Space Agency").
WithFamilyName("Smith").
WithGivenName("Jane").
WithEmail("jane@spaceagency.gov").
WithTelephone("+1-555-0199").
WithAddress("1 Rocket Way", "Houston", "TX", "77058", "USA").
WithJobTitle("Chief Scientist").
WithKeys("0xed25519signingkey", "0xed25519encryptionkey").
Build()
// Convert to vCard string
vcardStr, err := vcard.EPMToVCard(epmBytes)
// Result:
// BEGIN:VCARD
// VERSION:4.0
// FN:Dr. Jane Smith
// N:Smith;Jane;;;
// ORG:Space Agency
// EMAIL:jane@spaceagency.gov
// TEL:+1-555-0199
// TITLE:Chief Scientist
// ADR:;;1 Rocket Way;Houston;TX;77058;USA
// X-SIGNING-KEY:0xed25519signingkey
// X-ENCRYPTION-KEY:0xed25519encryptionkey
// END:VCARD
// Generate QR code (256x256 PNG)
pngData, err := vcard.EPMToQR(epmBytes, 256)
os.WriteFile("entity-profile.png", pngData, 0644)
// Scan QR code back to EPM
scannedEPM, err := vcard.QRToEPM(pngData)
// scannedEPM contains the original entity profile
Entity profiles in QR codes enable quick onboarding: new operators can scan a QR code to import an organization's public keys, contact info, and IPNS addresses. This is particularly useful for establishing trust relationships at conferences, coordination meetings, or during on-site visits.