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.

OMM TLE TDM RFM TDM CAT CDM CSM ROC SIT EPM PNM OEM OCM STF PUR SDN IPFS / libp2p Satellites LEO / MEO / GEO Space Sensors SSA / SDA Ground Sensors Radar / Optical / RF Ops Centers CSpOC / ESOC / TsUP / CNSA / JAXA Data Consumers Analysts / Researchers / Insurers Commercial Launch / Comms / EO Independent CubeSats / Universities Marketplace Buy / Sell / Subscribe Space Data Network Ecosystem Decentralized peer-to-peer exchange of standardized space data Data flow Data packet OMM CDM Schema types

Network Components

ComponentDescriptionUse Case
Full NodeServer-side Go applicationInfrastructure providers, relay operators
Edge RelayWebSocket bridge for browsersConnecting browser clients to the network
JavaScript SDKBrowser/Node.js client libraryWeb 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
FlagDefaultDescription
--config~/.spacedatanetwork/config.yamlPath to configuration file
--wasm~/.spacedatanetwork/wasm/hd-wallet.wasmPath 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
FlagDefaultDescription
--config~/.spacedatanetwork/config.yamlPath to configuration file
--wasm~/.spacedatanetwork/wasm/hd-wallet.wasmPath to hd-wallet WASM binary
--show-mnemonicfalseAlso 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.

Scripting Tip

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.

What is an Edge Relay?

Edge relays accept WebSocket connections from browsers, translate to libp2p protocols, participate in GossipSub, and forward messages bidirectionally.

Command Line Options

OptionDefaultDescription
--listen:8080HTTP/WebSocket listen address
--ws-path/wsWebSocket endpoint path
--max-connections500Maximum 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

OptionTypeDefaultDescription
relaysstring[]Built-inWebSocket relay URLs
relayStrategy'failover' | 'round-robin''failover'Relay selection strategy
identityIdentityGeneratedEd25519 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 (prefix 16Uiu2...).
  • 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)]
CurveEphemeral KeyOverhead
secp256k133 bytes61 bytes
P-25633 bytes61 bytes
P-38449 bytes77 bytes
X2551932 bytes60 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.

OperationBackendAsync
AES-256-CTR, HKDF, SHA-256WASM (Crypto++/OpenSSL)No
X25519 ECDHWASMNo
secp256k1 ECDH / ECDSAWASMNo
P-256 ECDH / ECDSAWeb CryptoYes
P-384 ECDH / ECDSAWeb CryptoYes
Ed25519 sign/verifyWASMNo
HMAC-SHA256JS (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:

  1. SDN_KEY_PASSWORD environment variable (highest priority)
  2. security.key_password in config.yaml
  3. Machine-derived password — deterministic Argon2id hash of hostname:GOARCH:GOOS with the user's home directory as salt (allows unattended startup)
Machine-Derived Passwords

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": { }
  }
}
CodeHTTP StatusDescription
INVALID_REQUEST400Malformed request
UNAUTHORIZED401Missing/invalid auth
FORBIDDEN403Insufficient permissions
NOT_FOUND404Resource not found
VALIDATION_ERROR422Schema validation failed
RATE_LIMITED429Too many requests
INTERNAL_ERROR500Server 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.

CategorySchemasPurpose
OrbitOMM, OEM, OCM, OSMOrbital elements and ephemeris
ConjunctionCDM, CSMClose approach and collision data
TrackingTDM, RFMObservation and measurement data
CatalogCAT, SITObject catalogs and identification
EntityEPM, PNMOrganization and personnel info
ManeuverMET, MPEManeuver planning and execution
PropagationHYP, EME, EOO, EOPPropagation parameters
ReferenceLCC, LDM, CRM, CTRLaunch and reference data
OtherATM, BOV, IDM, PLD, PRG, REC, ROC, SCM, TIM, VCMSpecialized 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

SchemaBuilderDescription
OMMNewOMMBuilder()Orbit Mean-Elements Message
EPMNewEPMBuilder()Entity Profile Message
PNMNewPNMBuilder()Publish Notification Message
CATNewCATBuilder()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:

OperationTimeThroughput
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 FieldvCard Property
DNFN (Formatted Name)
LEGAL_NAMEORG (Organization)
FAMILY_NAME, GIVEN_NAMEN (Structured Name)
EMAILEMAIL
TELEPHONETEL
ADDRESSADR
JOB_TITLETITLE
OCCUPATIONROLE
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)
QR Code Sizes

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.

Key Design Principle

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:

PriorityLevelDescription
1 (highest)Source+SchemaSpecific peer + specific schema
2SourceAll schemas from a specific peer
3SchemaSpecific schema from any peer
4 (lowest)SystemGlobal 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

OptionTypeDescription
AutoFetchboolAutomatically fetch content by CID
AutoPinboolAutomatically pin fetched content
TTLdurationTime-to-live for pinned content
PriorityintQueue priority (higher = more important)
TrustedboolMark 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):

SchemaOperationThroughputLatencyMemory
OMMSerialize~3.5M ops/sec~341ns288 B/op
OMMDeserialize~257M ops/sec~4.6ns0 B/op
EPMSerialize~2M ops/sec~614ns592 B/op
EPMDeserialize~255M ops/sec~4.7ns0 B/op
PNMSerialize~5.7M ops/sec~208ns288 B/op
PNMDeserialize~250M ops/sec~4.7ns0 B/op
CATSerialize~4.6M ops/sec~268ns224 B/op
CATDeserialize~255M ops/sec~4.7ns0 B/op
Zero-Copy Deserialization

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/
SchemaTypical SizeDescription
OMM~200-400 bytesOrbit mean elements
EPM~300-800 bytesEntity profile (varies with fields)
PNM~100-200 bytesPublish notification (lightweight)
CAT~150-300 bytesCatalog 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

TestDescription
TestEPMToVCardEPM FlatBuffer to vCard string conversion
TestVCardToEPMvCard string to EPM FlatBuffer conversion
TestEPMVCardRoundtripEPM → vCard → EPM preserves all fields
TestVCardEPMRoundtripvCard → EPM → vCard preserves all fields
TestVCardToQRvCard string to QR code PNG
TestQRToVCardQR code PNG to vCard string
TestEPMToQRDirect EPM to QR code conversion
TestQRToEPMDirect QR code to EPM conversion
TestEPMQRFullRoundtripComplete: EPM → vCard → QR → vCard → EPM
TestVCardQRRoundtripvCard → QR → vCard with various content

Field Mapping

Complete mapping between EPM FlatBuffer fields and vCard 4.0 properties:

EPM FieldvCard PropertyFormat
DNFNSingle value
LEGAL_NAMEORGSingle value
FAMILY_NAMEN (part 1)family;given;additional;prefix;suffix
GIVEN_NAMEN (part 2)
ADDITIONAL_NAMEN (part 3)
HONORIFIC_PREFIXN (part 4)
HONORIFIC_SUFFIXN (part 5)
EMAILEMAILSingle value
TELEPHONETELSingle value
JOB_TITLETITLESingle value
OCCUPATIONROLESingle value
ADDRESS.STREETADR (part 3);;street;locality;region;postal;country
ADDRESS.LOCALITYADR (part 4)
ADDRESS.REGIONADR (part 5)
ADDRESS.POSTAL_CODEADR (part 6)
ADDRESS.COUNTRYADR (part 7)
KEYS (Signing)X-SIGNING-KEYCustom extension
KEYS (Encryption)X-ENCRYPTION-KEYCustom extension
MULTIFORMAT_ADDRESSURLIPNS/IPFS addresses
ALTERNATE_NAMESX-ALTERNATE-NAMECustom 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:

OperationThroughputLatency
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
Use Cases

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.