Veydrin Community Network
The technical standard governing how VCN-enabled applications discover each other, sign and route data packets, synchronize state across devices, and operate without any central server. Built for the world, starting where infrastructure is scarce.
The VCN Protocol governs the technical layer of the Veydrin Community Network, how devices running VCN-enabled applications find each other, exchange signed data packets, and form a regional mesh without any server that the Veydrin Order operates. The one external infrastructure dependency is the Tier 3 bootstrap endpoint (Cloudflare Workers + KV), used only during early regional adoption before the peer mesh fills in. Its source is published and it can be self-hosted by any community.
The protocol has two concerns addressed in separate sections:
This spec governs software behavior. The organizational governance layer, the nine rules, node federation, trust badge, and enforcement framework, is a separate document: see the VCN Community Spec.
All VCN member applications must implement VODS v1.0 as the data interoperability baseline. VODS defines the document envelope, privacy floor, and export schema that VCN packets carry as payloads. The VCN Protocol defines how those packets move. They are siblings, neither depends on the other, but both are required for full compliance.
On first launch of any VCN-enabled application, the vcn_node package generates an Ed25519 key pair locally on the device. No network connection is required or used. No server is involved at any point.
The node ID is the Base64-URL-encoded Ed25519 public key, 43 characters in length without padding. This is the key itself, no hash, no truncation, no information loss.
node_id ::= base64url( ed25519_public_key ) example ::= "X9kLm2vQpR7wN4yZ1cFbDhEaG8sT0uIo3jK6xV5nMeA"
Every packet emitted by a node is signed with that node's Ed25519 private key. The signature covers the canonical JSON serialization of the packet body (all fields except the signature field itself). Recipients verify the signature before processing any packet. Packets that fail signature verification are silently discarded, no error is surfaced to the user.
A user may export their node identity to migrate to a new device or back up their keys. The export format is a JSON file encrypted with a user-chosen passphrase using AES-256-GCM:
{
"vcn_identity_version": "1.0",
"node_id": "Base64-URL public key",
"private_key_enc": "AES-256-GCM encrypted private key (Base64-URL)",
"nonce": "12-byte GCM nonce (Base64-URL)",
"salt": "16-byte KDF salt (Base64-URL)",
"kdf": "PBKDF2-SHA512",
"kdf_iterations": 310000,
"created_at": unix_ms
}
The passphrase is converted to a 256-bit AES key using PBKDF2-SHA512 with the following parameters:
The passphrase is never stored or transmitted. Import requires the correct passphrase. The existing key pair is replaced on import, apps MUST warn users before overwriting an existing identity. Implementations SHOULD enforce a minimum passphrase length of 12 characters.
The source_app envelope field identifies the originating application. The following identifiers are registered. Third-party VCN implementations must use a distinct identifier not listed here:
| source_app value | Application | Status |
|---|---|---|
eluvorim | Eluvorim, Community Ledger | Reserved |
melisae | Melisae, Beekeeping Management | Active |
chinara | Chinara, Soilless Cultivation | Reserved |
numaka | Numaka, Permaculture | Reserved |
arcalox | Arcalox, Personal Resilience Logging | Reserved |
omnavar | Omnavar, Grant Discovery & Management | Active |
vcn | VCN infrastructure packets (bulletin, update, message, contact_exchange) | Active |
apiara | Apiara, superseded by melisae | Deprecated |
Status values. Active, in use by a released application; receivers MUST accept it. Reserved, allocated to a planned application and MUST NOT be used by any other implementation. Deprecated, no longer emitted by any current release; receivers MUST continue to accept it so that records and packets from installs predating the rename remain valid, and MUST NOT treat it as equivalent to its successor for deduplication.
Registry maintenance. This table is normative. Renaming an application is a change to this registry and MUST be applied in the same pass to: this table, the type map in §4, the deep link scheme for the affected packet types, and every VRS relay's allowed-application set. An identifier is never reused for a different application.
source_app is set at compile time as a package constant in each consuming app's vcn_node configuration. It is not user-configurable.
Added in VCN Protocol v1.1.
When a user creates an organization within a VCN application, a separate Ed25519 keypair MUST be generated for org ownership. This keypair is independent from the node identity defined in §2. The org founder key is used exclusively to sign org-scoped broadcasts. It MUST NOT be used for community packets, messages, or any other purpose.
If the org founder key were the same as the node identity, any observer could correlate org broadcasts with the founder's pseudonymous community activity. Separating the keys ensures that knowing who leads an organization does not deanonymize their personal mesh activity.
founder_node.org_signature field in the payload, computed by signing the broadcast title with the org founder private key.org_signature against the stored founder_node public key.| Field | Type | Req? | Description |
|---|---|---|---|
org_scope | string | req | Org slug. Lowercase ASCII, digits, hyphens only. Max 64 characters. |
org_name | string | opt | Human-readable org name for display. Max 128 characters. |
org_signature | string | req | Base64-URL Ed25519 signature (86 characters, no padding) of org_scope + : + broadcast title, signed with the org founder private key. If the title is empty, sign org_scope alone. |
Receiving nodes MUST verify org_signature against the stored org founder public key before displaying the broadcast. If verification fails, the packet MUST be discarded silently. If the receiving node has no stored founder key for the given org_scope (not a member), the packet MUST be ignored.
Organizations share membership via QR code. The QR data MUST be a JSON object:
{
"type": "vcn_org",
"slug": "honey-for-humanity",
"name": "Honey for Humanity",
"founder_node": "Base64-URL org founder public key"
}
Scanning this QR code MUST create a local org membership with the provided slug, name, and founder key. The founder_node field contains the org founder PUBLIC key (not the VCN node_id), enabling broadcast verification. QR data MUST NOT exceed 2 KB.
Org founder public key: InjrkeV51Aox9zEKu61feig4GKFo9vy_tiN-gxPwe_k
Vector 1: Sign honey-for-humanity:Meeting this Saturday
Signature: HkhLse1ZtInb45yOMnlvcUwcUYE4rgF8Y39NbM2H5GK_3BWZnmPvVgbdNyBQQboPtkz-6K1bAGSlv9QMONDCAA
Vector 2 (empty title): Sign honey-for-humanity
Signature: 3wW0eCsWEyvPUl01AbGyTN5P4vfCv81Ibi6_HmXJ6wnMwbDvOW7-41Rx6B0ifrkr51wSCjxvaP91EwEMSML4Bw
Any correct Ed25519 implementation given the public key above MUST successfully verify both signatures.
Key compromise: If the org founder private key is compromised, the attacker can send verified broadcasts to all members. There is no revocation mechanism. The org founder SHOULD create a new org with a new keypair and redistribute the QR code. The old org slug becomes untrusted.
Spoofing: Any node can include org_scope in a packet payload. Without a valid org_signature, receiving nodes discard it. The signature is the proof, not the field.
Added in VCN Protocol v1.1.
VCN uses the area_tag field as a routing channel. In addition to geographic area tags, VCN defines the following special channel patterns:
| Pattern | Purpose | Scope |
|---|---|---|
{area_tag} | Local community packets (bulletins, surplus, needs, skills) | Geographic area |
_dm | Direct messages between specific nodes | Global (filtered by recipient node_id) |
_org_{slug} | Organization broadcasts | Global (filtered by org membership) |
_sync_{identifier} | Cross-device data sync for applications | Global (filtered by sync group membership) |
Special channels (prefixed with _) are not geographic. They route globally via Tier 3 and propagate via BLE when devices are in proximity. The _sync_ pattern enables VCN to function as a serverless cross-device synchronization layer for any application.
{country}_{region}._dm: fixed string. Direct messages include to and from fields in the payload for recipient filtering._org_{slug}: slug follows the same rules as org_scope (lowercase ASCII, digits, hyphens, max 64 chars)._sync_{identifier}: identifier is a node_id or application-defined group key. Max 64 characters.For the _dm channel, Tier 3 endpoints SHOULD support a to query parameter to filter messages server-side by recipient node_id. Without this, clients receive all global DMs and filter locally, which does not scale. The recommended endpoint: GET /vcn/packets?area_tag=_dm&to={node_id}.
Nodes exchange contact information via QR code. The QR data MUST be a JSON object:
{
"type": "vcn_contact",
"node_id": "Base64-URL Ed25519 public key",
"name": "Display name or null",
"enc_key": "Base64-URL X25519 public key or null",
"ts": 1775200000000
}
The enc_key field (added in v1.2) carries the node's X25519 encryption public key for end-to-end encrypted messaging. Contacts exchanged without enc_key fall back to plaintext messaging. The TOFU (Trust On First Use) mechanism backfills the key when the first encrypted message arrives from a contact.
Scanning adds the contact locally. The scanner SHOULD also emit a contact_exchange packet (including enc_key) so the scannee receives the scanner's identity and encryption key on next sync (two-way exchange).
BLE discovery MUST use a universal service identifier (vcn.mesh) rather than an area-scoped identifier. All VCN devices connect to all nearby VCN devices regardless of area tag. The Bloom filter handshake (§6) exchanges ALL stored packets across ALL channels. Channel filtering occurs at the application layer, not the transport layer.
Every VCN packet, regardless of originating app or content type, uses the following envelope. The payload field is app-specific; its schema is determined by packet_type (see §4).
{
"vcn_version": "1.0", // REQUIRED, protocol version
"source_app": "eluvorim", // REQUIRED, registered app ID
"source_node": "X9kLm2vQ...", // REQUIRED, Base64-URL Ed25519 public key
"packet_id": "uuid-v4", // REQUIRED, globally unique, UUID v4
"packet_type": "goods", // REQUIRED, registered type string
"area_tag": "ph_cebu", // REQUIRED, routing region identifier
"timestamp": 1741478400000, // REQUIRED, Unix milliseconds UTC (emission)
"clock_asserted": true, // REQUIRED v1.4, does the emitter vouch for timestamp?
"transit": { ... } // REQUIRED v1.4, MUTABLE, excluded from signature
"ttl": 72, // REQUIRED, hours. Immutable hop budget; transit.hops_remaining decrements, not this.
"location": { /* see §5 */ }, // OPTIONAL, geohash location schema
"payload": { /* type-specific */ }, // REQUIRED, app data
"deep_link": "veydrin://...", // OPTIONAL, in-app navigation URI
"signature": "Base64-URL..." // REQUIRED, Ed25519 signature
}
The signature is computed over the canonical form of the packet. Canonical form is defined as:
signature field excluded from the serialized input.transit block excluded from the serialized input (added in v1.4). Packets predating v1.4 carry no transit block, so this exclusion is a no-op for them and the canonical form of an existing packet is unchanged.canonical_input ::= json_compact_sorted( packet \ { signature, transit } )
signature ::= base64url( ed25519_sign( private_key, canonical_input ) )
A JSON Schema for the packet envelope is published alongside this document at vcn-packet-v1.4.schema.json.
This vector is reproducible from a fixed Ed25519 seed, so any implementation can regenerate every value below and confirm it matches. A conforming implementation MUST produce the same canonical input for this packet and MUST verify the signature against the node's public key.
Private seed (hex, 32 bytes): 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
node_id (Base64-URL Ed25519 public key): A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg
enc_key (Base64-URL X25519 public key, HKDF-SHA256 derived from the seed with info vcn-x25519-encryption-v1): Rg7Gsuhq_nru9sXwh4eH_uWafk2nn4_O8yREGJrYoFc
Canonical input (sorted keys at all levels, compact, UTF-8, 479 bytes; this exact byte string is what gets signed):
{"area_tag":"gcpv","clock_asserted":true,"enc_key":"Rg7Gsuhq_nru9sXwh4eH_uWafk2nn4_O8yREGJrYoFc","location":{"coords_available":false,"geohash":"gcpv","geohash_precision":4},"packet_id":"3f2504e0-4f89-41d3-9a0c-0305e82c3301","packet_type":"bulletin","payload":{"body":"Community meeting Saturday at the market.","title":"Meeting Saturday"},"source_app":"melisae","source_node":"A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg","timestamp":1700000000000,"ttl":168,"vcn_version":"1.0"}
Signature (Base64-URL, no padding, 86 characters):
Bup0U49fNpALvR2GafYgaqxPW0dURRC4LXs8_yQZ3KPZEZFSNitlT8gfF_Gc6CtcsKtZxtBCvwpb94MIDwOzBw
Full signed packet (the canonical input with the signature field added):
{"area_tag":"gcpv","clock_asserted":true,"enc_key":"Rg7Gsuhq_nru9sXwh4eH_uWafk2nn4_O8yREGJrYoFc","location":{"coords_available":false,"geohash":"gcpv","geohash_precision":4},"packet_id":"3f2504e0-4f89-41d3-9a0c-0305e82c3301","packet_type":"bulletin","payload":{"body":"Community meeting Saturday at the market.","title":"Meeting Saturday"},"source_app":"melisae","source_node":"A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg","timestamp":1700000000000,"ttl":168,"vcn_version":"1.0","signature":"Bup0U49fNpALvR2GafYgaqxPW0dURRC4LXs8_yQZ3KPZEZFSNitlT8gfF_Gc6CtcsKtZxtBCvwpb94MIDwOzBw"}
To verify: Base64-URL-decode node_id to the 32-byte public key, Base64-URL-decode the signature to 64 bytes, and check the Ed25519 signature over the UTF-8 bytes of the canonical input. Any change to any signed field alters the canonical input and MUST cause verification to fail. This vector was generated by the reference implementation and independently verified by a second Ed25519 library.
Every node maintains a seen_packets set of packet_id values. Before processing or forwarding any received packet, the node checks this set. If the packet_id is present, the packet is silently discarded. If not, the packet_id is added and processing proceeds. The set is pruned of entries older than max_ttl (720 hours) to prevent unbounded growth.
Added in v1.4. Every field outside the transit block is immutable from emission through delivery and is covered by the emitter's signature. All state that legitimately changes as a packet crosses the mesh lives inside transit, which is excluded from the canonical form (§3) and therefore from the signature.
"ttl": 168, // IMMUTABLE, signed. The ORIGINAL hop budget. Never modified in transit.
"transit": { // MUTABLE, unsigned. Forwarders modify this and nothing else.
"hops_remaining": 165, // REQUIRED. Initialised to ttl by the emitter, decremented on each forward.
"elapsed_ms": 10800000, // REQUIRED. Milliseconds since emission, accumulated in transit.
"prev_node": "X9kLm..." // OPTIONAL. Node ID of the immediately preceding forwarder.
}
ttl was a signed field that forwarders were required to decrement (§11 W2), while the canonical form excluded only signature (§3). Those two rules cannot both hold: once a forwarder decrements ttl, the receiving node recomputes a canonical form that no longer matches what the emitter signed, so verification fails, and §11 R1 requires that such packets be silently discarded. The practical consequence was that a packet could not survive more than one hop, and the failure produced no error anywhere. Freezing ttl and moving the mutable counter into an unsigned transit block resolves the contradiction and restores multi-hop forwarding.
This is an additive change. ttl keeps its name, position, type and range, and remains inside the signature; only its meaning narrows from "current hop budget" to "original hop budget." Packets emitted before v1.4 carry no transit block, so the revised canonical form produces byte-identical input for them and existing signatures continue to verify. A v1.4 node therefore interoperates with an older node in both directions, and two v1.4 nodes forward correctly across an arbitrary number of hops.
transit. The emitter's signature MUST remain byte-identical.transit.hops_remaining by exactly 1, and MUST NOT forward a packet whose hops_remaining has reached 0.transit.hops_remaining exceeds the signed ttl. This bounds hop-count inflation to the value the emitter authorised.transit block MUST synthesise one, setting hops_remaining to the signed ttl and elapsed_ms per the rules below.transit block is unsigned, so a malicious forwarder can still inflate or deflate its own copy of the hop counter and the elapsed time. What v1.4 changes is that this can no longer invalidate the emitter's signature or silently destroy the packet: tampering degrades routing for that branch instead of removing the packet from the network. Treat transit as a routing hint, never as a security property. Hop-by-hop authentication of the transit block is deferred to a future revision.
Added in v1.4. A packet's age determines whether it has expired. Age MUST NOT be derived from wall-clock arithmetic alone, because the emitting node's clock may be wrong by an arbitrary amount and many target devices have no reliable time source at all.
The emitter declares whether its clock is trustworthy, in the immutable clock_asserted field:
clock_asserted: true, the emitter believes timestamp is accurate, for example because it has been set from a network time source.clock_asserted: false, the emitter cannot vouch for timestamp. Receivers MUST NOT use it for expiry or freshness.Every forwarder adds the time it held the packet to transit.elapsed_ms before passing it on. A node that cannot measure elapsed time reliably MUST leave the value unchanged rather than guess.
packet_age(p):
if p.clock_asserted and receiver_clock_is_trusted:
wall_age = now - p.timestamp
return max(wall_age, p.transit.elapsed_ms) // elapsed_ms is a floor
else:
return p.transit.elapsed_ms
expired(p): packet_age(p) > p.ttl * 3_600_000 // ttl is expressed in hours
Taking the maximum of the two measures means a forged-early timestamp cannot make a packet look fresher than the mesh has demonstrably observed it to be, while a node with no clock at all still expires packets correctly. Receivers MUST still reject packets whose timestamp is more than 5 minutes in the future when clock_asserted is true.
elapsed_ms establishes how old a packet is, which is sufficient for expiry and storage trimming. It does not establish the order in which packets were created, and receivers MUST NOT sort by timestamp and present the result as causal sequence, with unsynchronised clocks a reply can carry an earlier timestamp than the message it answers. Causal ordering is a separate concern and is not solved in v1.4.
| Packet type | Default TTL (hours) | Max TTL (hours) |
|---|---|---|
| goods / services / skills / need / groupbuy | 72 | 720 |
| surplus | 720 (30 days) | 2160 (90 days) |
| harvest / hive / observation | 168 (7 days) | 720 |
| bulletin / update | 168 | 720 |
| message | 72 | 168 |
All TTL values are user-configurable within the stated max. Apps should expose TTL as a simple duration selector, not a raw number.
surplus payload includes an expires unix timestamp set by the originator. TTL governs how many hops a packet will travel before it stops propagating. expires governs whether the listing is still active at the application layer. Both must be checked: a packet that has not expired but whose transit.hops_remaining has reached 0 stops propagating; a packet that is still propagating but whose expires has passed must not be displayed as an active listing. Receiving apps discard surplus packets whose expires timestamp is in the past.
| packet_type | Handler app | Deep link scheme |
|---|---|---|
goods | Eluvorim | veydrin://eluvorim/listing/{id} |
services | Eluvorim | veydrin://eluvorim/listing/{id} |
skills | Eluvorim | veydrin://eluvorim/listing/{id} |
need | Eluvorim | veydrin://eluvorim/need/{id} |
groupbuy | Eluvorim | veydrin://eluvorim/groupbuy/{id} |
surplus | Eluvorim / Numaka / Melisae / Chinara | veydrin://{app}/surplus/{id} |
harvest | Chinara | veydrin://chinara/harvest/{id} |
hive | Melisae | veydrin://melisae/hive/{id} |
observation | Numaka | veydrin://numaka/observation/{id} |
bulletin | VCN (any app) | veydrin://vcn/bulletin/{id} |
update | VCN (any app) | veydrin://vcn/update/{id} |
message | VCN (any app) | veydrin://vcn/message/{id} |
contact_exchange | VCN (any app) | N/A (processed internally) |
Area tags are human-readable routing region identifiers. Format rules:
An area tag belongs to exactly one of three classes. The class is determined by the tag's own form, so any node can classify a tag it has never seen without consulting a registry.
| Class | Form | How a node obtains it |
|---|---|---|
| Derived | g{geohash}, the literal g followed by a 4-character geohash, e.g. gw3gv | Computed on-device. No user input. |
| Named | Any other conforming string, e.g. ph_cebu | Joined from a list of named areas already seen in traffic, or entered directly. |
| Reserved | Begins with _, e.g. _dm, _org_{slug} | Assigned by the protocol. See §2.2. |
Every conforming implementation MUST be able to compute a derived area tag, and MUST use one as the initial area on first launch. The derivation is fixed so that two applications in the same place produce byte-identical tags without coordination:
derived_area_tag(lat, lon) ::= "g" + geohash(lat, lon, precision = 4)
The geohash uses the standard Base32 alphabet defined in §5, lowercased, truncated to exactly 4 characters (~40 km). Implementations MUST NOT vary the precision, prefix, or alphabet. Where no location is available, the node has no derived tag and participates only in named areas it has been given.
us_richmond_va and richmond and never see each other, and two different applications naming the same place had no reason to agree at all. Packets routed correctly and delivered nowhere, with no error surfaced. A derived tag makes co-location sufficient for interoperability and removes the most common cause of an inexplicably empty community view.
Named tags remain fully supported and are how a community adopts an identity of its own, how an area larger or smaller than a geohash cell is expressed, and how people who are not co-located share a channel. A node MAY subscribe to any number of named areas alongside its derived one.
Implementations SHOULD offer named areas by presenting those already observed in local traffic, and SHOULD offer to adopt a peer's area during contact exchange, so that joining an existing community requires recognition rather than recall. Implementations MUST also provide a path to enter a named tag directly, without it having been seen first: this is the only way to reach a community that has no local presence yet, and the only way to convene one that is not tied to a place. That path SHOULD NOT be the primary affordance on the area screen, and it MUST NOT be the default first-launch experience.
A conforming implementation MUST always display the tag currently in use, in full, wherever the area is shown. A node's area determines what it receives and, for unencrypted community traffic, who can read what it posts; a user who cannot see which area they are in cannot reason about either.
The type map lookup is the first delivery filter. The second is the area tag. Even if packet_type maps to an installed handler, the packet is only delivered if the receiving node's configured area_tag matches the packet's area_tag field. Matching is exact string equality in v1.0. No wildcards. No hierarchical prefix matching. See §6 for the forwarding and sync logic that governs how packets move between nodes before delivery.
When a Veydrin app fires a deep link and the target app is not installed, the firing app must catch the failed intent and present a non-intrusive prompt offering the target app's Codeberg release page or F-Droid listing. It must not error, crash, or surface a system-level "no app" dialog.
All string fields are UTF-8. Fields marked optional may be omitted entirely, receiving apps must tolerate their absence gracefully.
{ "title": string, "value": number|null, "notes": string|null,
"currency": string|null, "image_hash": string|null }
{ "title": string, "notes": string|null, "offer": string|null }
{ "title": string, "min_count": integer, "unit_value": number|null,
"deadline": unix_ms|null, "notes": string|null }
{ "species": string, "quantity": string, "unit": string|null,
"contact": string, "expires": unix_ms, "intent": "trade"|"sell"|"give"|"barter"|"ask",
"notes": string|null, "image_hash": string|null }
{ "crop": string, "quantity": number, "unit": string,
"value": number|null, "harvest_date": unix_ms, "notes": string|null,
"image_hash": string|null }
{ "product": string, "quantity": number, "unit": string,
"value": number|null, "notes": string|null, "ohds_ref": string|null }
{ "guild_type": string, "layer": string, "succession_state": string|null,
"notes": string|null, "image_hash": string|null }
{ "title": string, "body": string,
"target_app": string|null, "version": string|null, "url": string|null }
Messages use the _dm channel (see §2.2). The to field enables server-side recipient filtering. Two payload variants exist:
Plaintext (legacy/fallback):
{ "to": string, // recipient node_id
"from": string, // sender node_id
"text": string }
Encrypted (v1.2):
{ "to": string, // recipient node_id
"from": string, // sender node_id
"enc": string, // Base64-URL: nonce(12) + AES-256-GCM ciphertext + mac(16)
"enc_key": string, // sender's X25519 public key (Base64-URL, no padding)
"v": 1 }
Encrypted messages use X25519 ECDH key agreement (derived from Ed25519 node keys via HKDF-SHA256 with info string vcn-x25519-encryption-v1) + AES-256-GCM. The sender's enc_key is included so the recipient can decrypt without prior contact exchange. See §10 for the full encryption protocol.
Senders MUST encrypt when the recipient's encryption key is known. Senders MUST fall back to plaintext when the recipient's encryption key is unavailable (pre-v1.2 contacts).
{ "target": string, // node_id of the intended recipient
"name": string|null,
"enc_key": string|null }
Emitted after scanning a contact QR to enable two-way discovery. The enc_key field (v1.2) carries the sender's X25519 encryption public key. Processed internally by the VCN layer, not displayed to users.
Any community packet type (bulletin, surplus, need, skills) MAY include org broadcast fields per §2.1:
{ ...type_payload,
"org_scope": string, // org slug
"org_name": string|null, // display name
"org_signature": string }
Location is a first-class field in the VCN packet format. Every app that attaches location to a packet uses this schema. Exact GPS coordinates never appear in any VCN packet under any circumstances, the geohash is the privacy floor.
A geohash converts GPS coordinates into a short text code. Longer codes describe smaller, more precise areas. The code for any location is a prefix of the code for any smaller area it contains, enabling natural geographic hierarchy without central configuration.
| Precision | Code length | Approx. area | Use in VCN |
|---|---|---|---|
| Province / large region | 3 chars | ~150 km radius | Rural / sparse network fallback |
| Metro / greater area | 4 chars | ~40 km radius | Default routing key |
| District / neighborhood | 5 chars | ~5 km radius | Dense urban opt-in |
| Street level | 6 chars | ~1 km | Not used in routing |
{
"location": {
"geohash": "w3gv", // routing key at chosen precision
"geohash_precision": 4, // number of chars used
"coords_available": false // exact coords never in packet unless explicit opt-in
}
}
Geohash cells have hard edges. A device near a cell boundary should store and forward packets for all 8 neighboring cells automatically, in addition to its own cell. This prevents listings from being invisible to users just across a geohash boundary. No user configuration required, this is automatic behavior in vcn_node.
All VCN-enabled apps that display location use OpenStreetMap tiles via flutter_map. Offline tile caching is required, users must be able to download their region's tiles for use without connectivity. The app-level community map is an emergent property of VCN packets with location fields: each app registers which packet types it renders as map pins. The shared OSM layer makes the mesh visible.
All six tiers use the same packet format, signing, verification, deduplication, and type-map routing logic. They are complementary, not alternatives. A device uses all available tiers simultaneously. Tiers are listed in priority order for the target population.
All VCN communication over any network transport (Tiers 2, 3, 5, and 6) must use TLS 1.3 minimum. Certificate validation is enforced. No fallback to plaintext under any circumstances. Tier 1 (physical proximity via Android Nearby Connections API) and Tier 4 (QR) do not use TLS, they are local channel transports that operate entirely off-network.
hive packets, it has no delivery handler, but it must still relay them to peers. Discarding at delivery is correct. Discarding at forwarding is a protocol violation that silently breaks the mesh.
// FORWARDING, happens first, regardless of local handler
forward_packet(p):
if p.transit.hops_remaining <= 0: drop
if p.packet_id in seen_packets: drop
seen_packets.add(p.packet_id)
deliver_packet(p) // deliver BEFORE decrement, handler sees the hop count as received
p.transit.hops_remaining -= 1
p.transit.elapsed_ms += time_held
if p.transit.hops_remaining > 0:
queue_for_all_transports(p)
// DELIVERY, only for packets we can handle locally
deliver_packet(p):
handler = type_map[p.packet_type]
if handler == null: return // no handler, not an error, packet still forwarded
if p.area_tag != local_area_tag: return
if handler_app_not_installed: return
route_to(handler, p)
When two nodes connect via a peer-to-peer tier (physical proximity, dedicated node, or QR), they negotiate what to exchange using a Bloom filter handshake. This avoids retransmitting packets the other node already has. This applies to Tiers 1, 2, and 4 only, Tier 3 (Cloudflare Workers) uses incremental HTTP polling via ?since=unix_ms and does not use this handshake.
// Both sides exchange their seen_packets Bloom filters
A → B: SYNC_HELLO { area_tag, bloom_filter(seen_packet_ids), node_id }
B → A: SYNC_HELLO { area_tag, bloom_filter(seen_packet_ids), node_id }
// Each side sends only packets the other node probably lacks
A → B: SYNC_PACKETS [ packets not in B's bloom filter ]
B → A: SYNC_PACKETS [ packets not in A's bloom filter ]
// Session closes
A ↔ B: SYNC_DONE
Parameters: 10,000 capacity, 1% false positive rate. This yields a bit array of 95,851 bits (~12 KB) with 7 hash functions. The hash algorithm is double hashing using SHA-256: h(i, item) = (SHA256(item)[0..15] + i * SHA256(item)[16..31]) mod m for i = 0..6. The bit array is transmitted as Base64-URL-encoded bytes, little-endian bit order.
False positives cause a missed sync for that packet, it will arrive in the next session. No data loss, only slight delay. The filter is rebuilt from the live seen_packets set at the start of each session.
Sync messages are length-prefixed JSON frames over the established transport connection (BLE, WiFi Direct TCP, or dedicated node TCP). Each frame is:
[4 bytes: payload length as big-endian uint32][N bytes: UTF-8 JSON payload]
Message types:
SYNC_HELLO, {"type": "sync_hello", "vcn_version": "1.0", "area_tag": "...", "bloom": "Base64-URL...", "node_id": "..."}. If vcn_version major versions do not match, the receiving node closes the connection immediately.SYNC_PACKETS, {"type": "sync_packets", "packets": [...]}. Array of full VCN packet objects. Sent after both sides have exchanged SYNC_HELLO.SYNC_DONE, {"type": "sync_done"}. Signals completion. Both sides close the connection after sending and receiving SYNC_DONE.Two nodes in physical proximity exchange packets directly, device to device. No internet, no infrastructure, no configuration. Uses Android Nearby Connections API (Bluetooth and WiFi Direct transport) via the nearby_connections Flutter package. mDNS handles discovery on any shared local network (community WiFi, home router, café hotspot).
Packet size constraint: Core text packets must not exceed 1 KB. Images are never included in proximity sync packets (see §8).
Discovery: Nodes advertise their area_tag as the service ID. A node only initiates connection to peers advertising a matching area tag, preventing cross-community leakage at the transport layer.
Session lifecycle: Sessions are opportunistic and short-lived. A session opens when two compatible nodes are discovered, exchanges the outbound packet queue, then closes. No persistent connection is maintained.
Any device running a VCN-enabled app may opt in as a dedicated node, a persistent, always-on regional anchor. Dedicated nodes are the community's own infrastructure. No Veydrin server is involved at any step.
Discovery: Dedicated nodes advertise themselves using a Kademlia-based DHT conforming to BEP 5 (Mainline DHT). The DHT key is the SHA-1 hash of the node's 4-character geohash string. The stored value is a compact contact record: {node_id, ip, port, last_seen}. Mobile devices query the DHT for their geohash key (and all 8 adjacent geohash cells) to find regional dedicated nodes and sync directly. DHT entries have a 30-minute TTL and are re-announced every 15 minutes by active dedicated nodes. The DHT stores only contact addresses, not content. No persistent record of who was where.
Bootstrap: New nodes join the DHT by querying the Tier 3 endpoint at GET /dht-bootstrap, which returns a list of 8 known DHT nodes as [{ip, port}]. After initial bootstrap, nodes discover peers through standard Kademlia routing and no longer depend on Tier 3 for DHT operations.
Activation: Available in Settings when charging is detected. User toggle, off by default. Charging + WiFi required. Battery use when active is negligible (WorkManager, Doze-compliant, fires only during sync windows).
Cold start: A dedicated node in a region solves cold start for all new users, new device connects to WiFi, finds the dedicated node in the DHT, syncs, and immediately has the region's packet history. No physical proximity required.
Old device repurposing: Any Android device (including old phones no longer used as primary) can serve as a dedicated node permanently. Plug in, open app, enable Node Mode. The device becomes a regional anchor for its community. No advanced setup. UPnP handles port forwarding automatically on consumer routers.
Storage cap: User-configurable. Suggested default: 500 MB. Devices store packets for their geohash region and all 8 adjacent cells. Auto-trim by age then by geohash distance when cap approaches.
When a device has internet connectivity and no dedicated node is yet available in its region, it pushes packets to and pulls from a lightweight HTTPS endpoint backed by a Cloudflare Worker and Cloudflare KV storage. This is the mechanism that connects the first users in any region before the dedicated node mesh fills in. It is the only component in the VCN stack with an external infrastructure dependency.
Architecture: One Cloudflare Worker handles both reads and writes. Packets are stored in Cloudflare KV keyed by {area_tag}:{packet_id} with a TTL matching the packet's ttl field. KV entries expire automatically, no manual cleanup. Cloudflare's free tier (100k requests/day, 1 GB KV) is sufficient for bootstrap use across all regions.
Push: POST /packets with the full VCN packet JSON as the request body. The Worker verifies the packet's Ed25519 signature against the source_node public key before storing. Packets with invalid or missing signatures are rejected with HTTP 400. No accounts. No API keys. No sessions. The signature is the authentication.
Pull: GET /packets?area_tag={tag}&since={unix_ms}, returns all stored packets for the given area tag with timestamps after since. Response is a JSON array. Incremental, clients track the timestamp of their last successful pull.
Global coverage: One Worker deployment serves all regions, every packet carries an area_tag and pull requests filter by it. Devices in Cebu receive only Cebu packets. No cross-regional leakage.
Rate limiting: The Worker enforces per-source-node rate limits: maximum 60 packet pushes per hour per source_node. Excess requests receive HTTP 429. This is a spam floor, not an access control mechanism.
Role: Once a region has dedicated nodes, Tier 3 becomes pure redundancy. It does not replace Tier 2, it precedes it in regional adoption and backs it up permanently. The Worker source code is published in the VCN protocols repository under AGPL-3.0 and may be self-hosted by any community that prefers to eliminate this dependency.
Two nodes exchange a batch of packets by encoding them as a QR code (or sequence of QR codes) and scanning with the device camera. No internet, no Bluetooth pairing, no network of any kind. Requires only that both devices be in the same physical location.
Encoding: Base64-URL-encoded, gzip-compressed JSON array of packet objects. A single QR at max density (v40, error correction L) holds ~2.9 KB. Threshold for multi-frame: 2 KB (implementation-defined reference default, adjust after device testing on mid-range Android/GrapheneOS).
Multi-frame: { "frame": 1, "total": 3, "batch_id": "uuid", "data": "..." }, frames assembled in any order, duplicates ignored.
Primary use cases: Transaction records, listing discovery at physical markets, identity exchange, sync bootstrap for new devices with no connectivity of any kind.
VCN-enabled apps may optionally route communication through Tor hidden services (.onion addresses). This tier provides metadata privacy, the network location of the user is not revealed to destination nodes or network observers. Tor transport is opt-in and user-initiated. Apps must not silently route traffic through Tor without explicit user consent.
When to implement: Applications serving populations with elevated surveillance risk, asylum seekers, domestic violence survivors, political dissidents, LGBTQ+ communities in hostile jurisdictions.
Badge requirement: Required for the enhanced VCN trust badge (see Community Spec §7).
Apps may implement pluggable transports (obfs4, Snowflake, or equivalent) that disguise VCN traffic as innocuous traffic, evading deep packet inspection and censorship infrastructure. This tier is for deployments where Tor traffic itself is blocked or surveilled. Must be explicitly enabled by the user, not default.
When required: Applications deployed for communities in censored or authoritarian environments, or any app intending to earn the full VCN trust badge (Community Spec §7).
Dedicated Node Mode is a first-class feature of the VCN protocol, not a hidden setting, not a developer tool. Any user with a spare Android device can strengthen their community's network by enabling it. The device becomes a persistent regional anchor that serves all VCN-enabled apps in the area.
When Node Mode is active, the app collapses to a minimal status screen showing: packets stored, last sync timestamp, peers seen in the last 24 hours, and storage used. No advanced configuration required. UPnP handles port forwarding automatically on consumer routers, no manual setup.
Auto-trim runs two passes when storage approaches the configured cap (default 500 MB):
The node's own locally-generated packets are never trimmed regardless of age or cap.
| State | RAM | CPU | Network |
|---|---|---|---|
| Idle (advertising only) | ~15-25 MB | Negligible | Near zero |
| Active sync window | ~30-50 MB | Low burst | Only during sync |
| Long-running dedicated node | ~30-50 MB | WorkManager bursts only | Periodic, not persistent |
Images are never included in sync packets propagated through any tier. This is a firm protocol-level constraint, not a recommendation. Packet size limits and low-bandwidth target environments make inline images incompatible with reliable sync.
When a listing or record includes an image, the emitting node stores the image locally and includes only the SHA-256 hex digest (image_hash field) in the packet payload. Image transfer is a separate, out-of-band, bilateral, on-demand operation using direct HTTP over whichever transport tier connects the two nodes:
GET /image/{image_hash}. The request is authenticated by Ed25519-signed headers: X-VCN-Node: {node_id}, X-VCN-Ts: {unix_ms_timestamp}, X-VCN-Sig: {base64url(sign(UTF-8("{image_hash}:{unix_ms_timestamp}")))}. The receiver MUST reject requests where the timestamp differs from the receiver's current time by more than 300 seconds.Content-Type: image/*.image_hash.Transport: Image requests route via the best available tier, Tier 2 (dedicated node relay) preferred for cross-WiFi requests, Tier 1 (direct device-to-device) for proximity requests. Tier 3 (Cloudflare) is not used for image transfer. If no transport path exists to the source node at the time of request, the request is deferred until a sync session with the source node (or a node that has cached the image) becomes available.
The source node may decline any image request with HTTP 403. Declining does not affect packet delivery or listing visibility. Images are optional, listings without images are fully valid.
The vcn_node shared Flutter/Dart package is the single implementation of this protocol. All Veydrin apps import it. No app reimplements any part of the protocol directly.
All protocol logic lives in vcn_node: identity generation, packet format, signing, type map, transport, sync. All app-specific UX stays in the consuming app: community tab layout, listing rendering, partner card display, deep link handling UI. This boundary must be respected, coupling app UX to protocol logic makes extraction impossible.
Development sequence: vcn_node is built and proven in Eluvorim (first implementation) before extraction as a standalone published package. The extraction boundary defined here must be implemented before Eluvorim scaffolding begins.
VcnConfig.init({
required String sourceApp, // registered source_app identifier (compile-time constant)
required String areaTag, // community area tag, set once, persisted in Hive CE
})
sourceApp is a compile-time constant defined per app (e.g. "eluvorim"). areaTag is set during onboarding and stored in the app's local Hive CE box. It is also writable later via VcnSync.setAreaTag(). Both values are automatically attached to every emitted packet envelope, VcnEmitter.emit() does not require them as parameters.
VcnEmitter.emit({
required String packetType,
required Map<String, dynamic> payload,
Map<String, dynamic>? location, // geohash schema, see §5
String? deepLink,
int ttl = 72,
})
Builds the envelope, attaches source_node and source_app from local identity, signs, adds to the outbound queue for all active transports, returns the packet_id.
VcnReceiver.register( String packetType, Future<void> Function(VcnPacket packet) handler, )
Registers a callback for a given packet_type. Multiple handlers may be registered for the same type. Called after area tag filtering and deduplication pass.
VcnSync.start() // Begin all available transport tiers
VcnSync.stop() // Graceful shutdown
VcnSync.setAreaTag(tag) // Update active area tag filter
VcnSync.enableNodeMode(cap) // Activate dedicated node (charging + WiFi required)
VcnSync.disableNodeMode() // Deactivate dedicated node
// Returns current transport tier availability
VcnSyncStatus VcnSync.status() → {
tier1_active: bool, // BLE/WiFi Direct advertising
tier2_active: bool, // DHT connected to at least one dedicated node
tier3_active: bool, // Cloudflare endpoint reachable
tier4_active: bool, // always true, QR available when app is open
tier5_active: bool, // Tor circuit established
tier6_active: bool, // pluggable transport active
node_mode: bool, // dedicated node mode running
last_sync: int, // unix ms of last successful sync on any tier
}
VCN provides standard E2E encryption for direct messages between nodes:
vcn-x25519-encryption-v1 and empty salt. This is deterministic, the same master seed always produces the same encryption key.nonce(12) + ciphertext + mac(16) and encoded as Base64-URL without padding.contact_exchange packets (see §2.2).enc_key from the message payload is automatically saved. This enables encryption without prior explicit contact exchange.enc_key differs from the stored key for that contact, the receiver SHOULD flag the message to the user as a potential key change. Decryption proceeds using the sender-provided key.source_node, the receiver SHOULD flag the message to the user as a potential downgrade. The message is still delivered but visually distinguished. This prevents silent stripping of encryption by a relay or attacker.text field. Receivers MUST accept both formats.Reproducible from two fixed Ed25519 seeds. A conforming implementation MUST derive the same X25519 keys, agree the same shared secret, and decrypt the payload to the plaintext. The nonce is fixed here so the vector is reproducible; a real sender MUST use a random nonce.
Sender seed (hex, 32 bytes): 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
Sender X25519 public key (Base64-URL): Rg7Gsuhq_nru9sXwh4eH_uWafk2nn4_O8yREGJrYoFc
Recipient seed (hex, 32 bytes): 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f
Recipient X25519 public key (Base64-URL): ma9ZsxQoN-tlYRyKL5ykIg1KPl3M_skAGixg7avWmWg
Shared secret (X25519 ECDH, hex, 32 bytes): 304f4bd498cd17eb2ed9fa7b11c0caeb06f8d8a8333489571f45c8886bcf5040
Plaintext: Meet at the north gate at dusk.
Nonce (hex, 12 bytes, fixed for this vector only): 000102030405060708090a0b
Encrypted payload (Base64-URL, nonce + ciphertext + mac): AAECAwQFBgcICQoLqvsUlB5juwtxehuWoPlAa4nvV900lIRib7TF8vIUCcEZwQHR4Go-RiQWi5Eqv3Q
To verify: derive each X25519 key from its seed with HKDF-SHA256 (info vcn-x25519-encryption-v1, empty salt); the X25519 agreement of one side's private key with the other side's public key yields the shared secret; AES-256-GCM with that secret and the leading 12-byte nonce decrypts the remainder to the plaintext. This vector was produced by the reference implementation and independently verified by a second X25519 and AES-GCM library.
transit block (hop counter, elapsed time) is unsigned and can be altered by any forwarder. Advisory only. Since v1.4 such tampering degrades routing on that branch rather than invalidating the emitter signature, so it can no longer silently destroy a packet. Hop-by-hop authentication of the transit block is not yet specified.seen_packets set is pruned of entries older than 720 hours. Packets replayed after pruning could be accepted as new. Mitigation: receivers SHOULD reject packets whose timestamp is older than the packet type's maximum TTL. For packet types with TTLs exceeding 720 hours (e.g. surplus at 2160 hours), the seen_packets retention for those entries SHOULD be extended to match.Added in VCN Protocol v1.3.
VCN has no central authority and no global moderation. Trust and safety are enforced locally by each device and collectively by each community. The following mechanisms form the trust layer.
Receivers MAY maintain a list of blocked source_node identifiers. Packets from blocked nodes MUST be dropped on receive before any other processing. Blocked nodes' packets MUST NOT be forwarded, the blocking device does not relay them to any tier. Blocks are local to the device and are not broadcast to the mesh. Blocks SHOULD auto-expire after 90 days of inactivity to prevent stale lists.
When a user blocks a node, the application SHOULD emit a flag packet to the local area channel:
{ "target": string } // source_node ID of the flagged node
Receivers tally flag packets per target from unique source nodes. A flag is only counted if the flagging node has contributed 3 or more non-flag packets to the local store (anti-Sybil gate). When the count reaches a community threshold (default: 5 unique flaggers), the target is permanently blocked with no expiry. This is local consensus; each device makes its own decision based on the flags it has received. There is no global ban. A flagged node retains access to other areas and to direct messaging.
Flag packets use the same area_tag as community posts and propagate via all tiers. They have a TTL of 720 hours (30 days).
Tier 3 relays MUST enforce per-source_node rate limits in addition to per-IP limits. Recommended limits:
Tier 1 (BLE) implementations SHOULD limit inbound connections to 10 per minute to prevent handshake flooding.
Tier 3 relays MAY require proof-of-participation for community post types. A node must have a valid VRS counter record (from any Veydrin application) before its community posts are accepted. Direct messages and contact exchange packets are exempt, they always route. This prevents drive-by spam from freshly generated node identities.
Packet payloads MUST NOT exceed 8,192 bytes (8 KB) when serialized as JSON. Receivers MUST reject packets exceeding this limit. Tier 3 relays enforce a 16 KB total request body limit which encompasses the full envelope.
During Tier 1 BLE sync handshake, receivers MUST reject bloom filters exceeding 16 KB (approximately 21,334 Base64-URL characters). This prevents memory exhaustion from malicious peers transmitting oversized filter data.
Once a receiver has successfully verified an org_signature for a given org_scope against a founder public key, that key is pinned locally. Subsequent packets claiming the same org_scope but signed by a different key MUST be rejected. This prevents org impersonation even if the org slug is known. Pinning is local and does not require a central registry.
This section defines what it means to be a conforming VCN implementation. Four conformance targets exist. An implementation MAY satisfy one or more targets.
An application that creates and signs VCN packets. MUST satisfy:
signature and transit excluded).packet_id. Never reused.An application that accepts and processes VCN packets. MUST satisfy:
source_node before delivering any packet to application handlers. Discard silently on failure.seen_packets set. Discard packets with a previously seen packet_id.area_tag. Also deliver packets on subscribed special channels: _dm (filtered by recipient node_id), _org_{slug} (filtered by org membership), _sync_{id} (filtered by sync group). See §2.2 for channel patterns.packet_type MUST be forwarded (if the node forwards) but MUST NOT cause errors. Silently ignore for local delivery.A node that relays packets to other nodes. Every forwarder MUST also satisfy VCN Receiver rules. Additionally:
transit.hops_remaining by 1 on forward. Do not forward a packet whose hops_remaining is 0. The signed ttl field MUST NOT be modified.transit block is the only part of a packet a forwarder MAY change. Every other field, and the original signature, MUST remain byte-identical.A persistent, always-on regional anchor. MUST satisfy all Forwarder rules plus:
An application that supports organization broadcasts. MUST satisfy:
org_scope, org_signature, and the signature MUST be computed per §2.1 (sign org_scope:title).org_signature against the stored founder key. Discard on failure. Ignore if not a member.vcn_org_key_version, slug, public_key, private_key_enc, nonce, salt, kdf, kdf_iterations.All VCN applications SHOULD support identity portability:
This section is non-normative and records the status of known implementations, following the convention of RFC 7942. It may be removed before any final publication.
Reference implementation: Melisae. An open-source (AGPL-3.0) Flutter application running on the web and Android is the reference implementation of this specification. It implements node identity (§2), packet signing and the canonical form (§3), the v1.4 transit block with multi-hop forwarding, v1.4 packet-age expiry with clock_asserted, encrypted direct messaging over X25519 and AES-256-GCM (§10), community flagging with org founder-key pinning, and the multi-tier sync transports (§6).
Test vectors. The canonical packet signing vector (§3) and the encrypted direct message vector (§10) are reproducible from fixed seeds and have been independently verified by a second, unrelated Ed25519, X25519, and AES-GCM library. This confirms the canonical form, signature, key agreement, and encryption are interoperable at the algorithm level.
Interoperability status, stated plainly. One reference implementation exists today. A recognised standard requires at least two independent, interoperating implementations built from separate code bases. This document and its test vectors are written so a second implementation can be built from the specification alone. That second implementation, and a documented interoperability test between the two, is the remaining work before this specification can claim interoperability.
VCN Protocol Spec uses semantic versioning (MAJOR.MINOR). The vcn_version envelope field is the designated compatibility mechanism.
| Change type | Version bump | Compatibility |
|---|---|---|
| Add optional envelope field; add optional payload schema field; add new packet type; clarify existing behavior | MINOR |
Backward compatible. Old nodes ignore unknown fields. |
| Change required envelope field; change canonical serialization; change signing algorithm; restructure type map; change transport requirement | MAJOR |
Breaking. Nodes receiving a packet with an unsupported major version drop it silently and log locally. No exotic negotiation. |
| Correct typographic or formatting error without changing behavior | Patch note, no version bump | No implementation change required. |
vcn_version field is sufficient. Minor versions are backward compatible by definition, no negotiation needed.
The VCN Protocol Spec and the VCN Community Spec version independently. Protocol changes do not require community governance changes and vice versa.
| Term | Definition |
|---|---|
| Area tag | A community-chosen string identifier for a geographic region (e.g. ph_cebu, us_austin). Used for packet routing and filtering. Not a geohash, area tags are human-readable names. |
| Bloom filter | A probabilistic data structure that tests whether a packet ID has been seen. May produce false positives (causing a missed sync) but never false negatives. |
| Canonical serialization | The deterministic JSON encoding used for signing: keys sorted alphabetically, compact form, UTF-8, with the signature field and the transit block excluded. |
| Dedicated node | A device running in always-on mode that stores and forwards packets for its region. Acts as a persistent regional anchor in the mesh. |
| Deep link | A URI (veydrin://...) embedded in a packet that directs the receiving app to a specific screen or action. |
| Delivery | Processing a packet locally, passing it to registered handlers in the receiving app. Distinct from forwarding. |
| DHT | Distributed Hash Table. A decentralized key-value store used for dedicated node discovery. VCN uses Mainline DHT (BEP 5, Kademlia-based). |
| Forwarding | Relaying a packet to other nodes without local processing. Only the transit block is modified; the packet is otherwise byte-identical. Nodes MUST forward all valid packets regardless of local interest. |
| Geohash | A string encoding of geographic coordinates into a grid cell. VCN uses 4-character geohashes (~40 km precision) as the privacy floor for location data. |
| Node | Any device running a VCN-enabled application. Every node has a unique Ed25519 identity and can emit, receive, and forward packets. |
| Node ID | The Base64-URL-encoded Ed25519 public key of a node. 43 characters, no padding. This is the node's permanent, self-certifying identity. |
| Packet | The atomic unit of data in VCN. A signed JSON envelope containing metadata (type, area tag, TTL, timestamp) and a type-specific payload. |
| source_app | A registered string identifier set at compile time that identifies which Veydrin application emitted a packet (e.g. melisae, eluvorim). |
| Transport tier | One of six layered communication channels: T1 Physical Proximity (BLE/WiFi Direct), T2 Dedicated Nodes (DHT), T3 HTTPS (Cloudflare Workers), T4 QR Physical Exchange, T5 Tor, T6 Pluggable Transports. |
| TTL | Time to live. An integer field on each packet representing remaining hops. Decremented by 1 at each forwarding node. Packet is not forwarded when TTL reaches 0. |
| vcn_node | The shared Flutter/Dart package that implements the entire VCN protocol. All Veydrin apps import it rather than reimplementing protocol logic. |
| Reference | Title | Relevance |
|---|---|---|
| RFC 8032 | Edwards-Curve Digital Signature Algorithm (EdDSA) | Ed25519 for node identity and packet signatures. |
| NIST SP 800-38D | Galois/Counter Mode (GCM) | AES-256-GCM for identity export encryption. |
| RFC 8259 | The JavaScript Object Notation (JSON) Data Interchange Format | Packet envelope and payload serialization. |
| RFC 9562 | Universally Unique Identifiers (UUIDs) | packet_id is UUID v4. |
| RFC 4648 §5 | Base64url Encoding | Node ID and signature encoding. |
| FIPS 180-4 | Secure Hash Standard (SHA-256) | Image content addressing. |
| RFC 8446 | Transport Layer Security (TLS) 1.3 | Required for Tier 3 HTTPS transport. |
| RFC 2119 | Key words for use in RFCs | Normative keyword definitions. |
| RFC 8174 | Ambiguity of Uppercase vs Lowercase in RFC 2119 | Uppercase keywords carry normative weight. |
| VODS v1.0 | Veydrin Open Data Standard | Data interoperability baseline for VCN member applications. |
| BEP 5 | DHT Protocol (Mainline DHT) | Kademlia-based distributed hash table for Tier 2 dedicated node discovery. |
| RFC 8018 | PKCS #5: Password-Based Cryptography (PBKDF2) | Key derivation for identity export passphrase. |
| Version | Date | Changes |
|---|---|---|
1.4 |
2026-08 | Immutable/mutable split. New unsigned transit block carrying hops_remaining, elapsed_ms and optional prev_node; every field outside it is immutable and signed. ttl is frozen as the original hop budget and MUST NOT be modified in transit. Canonical form now excludes transit as well as signature. Fixes a defect that prevented any packet from surviving more than one hop: decrementing the signed ttl invalidated the emitter signature, and §11 R1 requires silently discarding packets that fail verification. Packet age and expiry. New immutable clock_asserted field; expiry derives from accumulated elapsed_ms when the emitter cannot vouch for its clock, and from the greater of wall age and elapsed time when it can, so nodes with no reliable time source expire packets correctly. Additive and backward compatible: pre-1.4 packets carry no transit block, so their canonical form and existing signatures are unchanged. |
1.3.1 |
2026-04 | Encryption downgrade detection: receivers SHOULD flag plaintext messages from contacts with known encryption keys. Anti-Sybil gate for community flagging: flagging node must have 3+ non-flag packets before flags count toward permablock threshold. |
1.3 |
2026-04 | Added §10.1 Trust & Safety: local block lists with auto-expiry, community flagging with permablock threshold, per-node rate limiting (community/DM/other categories), proof-of-participation gate (VRS counter required for community posts), 8 KB packet payload limit, BLE connection rate limiting (10/min), bloom filter size validation (16 KB max), organization signature pinning. New flag packet type (30-day TTL). |
1.2 |
2026-04 | End-to-end encrypted messaging: X25519 ECDH (HKDF-derived from Ed25519) + AES-256-GCM for direct messages. TOFU key backfill from message payloads. Key rotation detection. Contact exchange now includes enc_key field. Encrypted message payload variant (enc, enc_key, v fields). Standalone org key export format (O5), PBKDF2-SHA512 + AES-256-GCM, enables sharing org signing authority independently of node identity. Dedicated nodes now pull from 8 adjacent geohash cells (D3 regional storage). Incremental sync for node mode polling. |
1.1 |
2026-04 | Added §2.1 Organization Founder Identity (separate Ed25519 keypair for org ownership, independent from node identity). Added §2.2 Channel Patterns (geographic area tags, _dm for direct messages, _org_{slug} for org broadcasts, _sync_{id} for cross-device sync). BLE discovery MUST use universal vcn.mesh service ID. Org broadcast payload fields defined (org_scope, org_name, org_signature). |
1.0 |
2026-03 | Initial release, node identity (Ed25519), packet format with canonical signing, eleven packet types, geohash location privacy, six transport tiers, dedicated node mode, image transfer protocol, vcn_node package interface. |