HPPR Core Specification
006 – B64A
HPPR-006 · Base64-Ascend (B64A)
B64A is an order-preserving Base64 variant.
Alphabet:
0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~
Canonical form omits = padding and zero-fills
trailing bits.
Alphabet Mapping
0..9->'0'..'9'10..35->'A'..'Z'36->'_'37..62->'a'..'z'63->'~'
~ keeps the alphabet ASCII-sorted and path-safe.
Encoding Rules
- Pack bits like RFC 4648 Base64 (MSB-first 6-bit groups).
- Do not emit
=. - Zero-fill the final partial 6-bit group.
- Output length is
ceil(8*N/6)forNinput bytes.
Decoding and Validation
- Reject bytes outside the B64A alphabet.
- Decode first, then validate tail bits:
len % 4 == 2: low 4 bits must be zerolen % 4 == 3: low 2 bits must be zero
- Implementations should validate by decoding, not by regex.
Ordering Guarantees
- Equal-length strings compare exactly like decoded bytes.
- If one canonical string is a strict prefix of another, the shorter string sorts first.
Conformance
- MUST emit and accept only the B64A alphabet above.
- MUST forbid
=padding. - MUST zero-fill trailing bits and reject non-zero filler bits.
- SHOULD validate by decoding plus explicit tail checks.
Test Vectors
| Hex | B64A | Notes |
|---|---|---|
| (empty) | empty input | |
00 |
00 |
1 byte |
0000 |
000 |
2 bytes |
000000 |
0000 |
3 bytes |
FF |
~l |
tail 4 bits zero |
FF00 |
~l0 |
tail 2 bits zero |
000102 |
0012 |
exact 3 bytes |
Reject examples: 01, 001,
~m, ~l1, =, +,
/.
009 – HSB3
HPPR-009 · HSB3 Signatures
HSB3 is a BIP-340 style Schnorr signature over secp256k1.
HSB3 uses BLAKE3-256 for all hashing and key derivation tags.
Reference code lives in rust/libs/hsb3.
Parameters
- Curve: secp256k1 (
y^2 = x^3 + 7 mod p) - Order:
n - Generator:
G - Hash primitive:
BLAKE3.derive_key - Tags:
hppr-🖧/auxhppr-🖧/noncehppr-🖧/challenge
Helper notation:
bytes(x): 32-byte big-endian encoding of scalar or field elementbeInt(b): integer from 32-byte big-endian bytestagged(tag, msg): first 32 bytes ofderive_key(tag, msg)
Key Generation
- Draw random 32-byte
d0; reject0andd0 >= n. - Set
d = d0, computeP = d*G. - If
P.yis odd, setd = n - dand recomputeP. - Private key is
d; public key isbytes(P.x).
The even-y convention is mandatory.
Deterministic Key Derivation
Derive a keypair from arbitrary secret bytes.
- Initialize BLAKE3 derive mode with tag
hppr-🖧/adhoc-key. - Feed secret bytes.
- Enter XOF mode.
- Rejection sample 32-byte candidates until
0 < d0 < n. - Apply the same even-
ynormalization as key generation.
Conformance:
- MUST use tag
hppr-🖧/adhoc-keyexactly - MUST use XOF rejection sampling
- MUST apply even-
yconvention - MUST reject empty secret
Ring1 Adhoc Derivation (Argon2id)
Ring1 secret tokens derive through Argon2id first.
Token format:
V.<b64a>.H3
Argon2id inputs:
- password: UTF-8 original secret
- salt: first 16 bytes of
BLAKE3.derive_key("hppr-🖧/phc-salt", "<ring1>/<repo-vkey>") - params:
$argon2id$v=19$m=<m>,t=<t>,p=<p>$ - output length: 32
If HELLO omits PHC, defaults are
m=12288, t=3, p=1.
Then derive HSB3 key from:
<derived-token>/<ring1>/<repo-vkey>
Signing
Inputs:
msg32: BLAKE3-256 digest of message bytes- private key
d auxRand32: fresh random 32 bytes
Rules:
auxRand32MUST be unique per signature- explicit all-zero aux input is invalid
Algorithm:
t = tagged(aux, auxRand32)mask = t xor bytes(d)k0 = tagged(nonce, mask || Px || msg32) mod n; rejectk0 = 0R = k0*G; ifR.yodd thenk = n-k0, elsek = k0e = tagged(challenge, bytes(R.x) || Px || msg32) mod ns = (k + e*d) mod n- signature bytes are
bytes(R.x) || bytes(s)
Text form uses B64A.
Verification
Inputs: signature r||s, public key Px,
and msg32.
- Parse
r,s; rejectr >= pors >= n. - Recover
PfromPxand take even-yroot. e = tagged(challenge, bytes(r) || Px || msg32) mod nR' = s*G - e*P; reject infinity or oddy.- Accept iff
R'.x == r.
Conformance
Implementations:
- MUST use the three UTF-8 tags exactly
- MUST use BLAKE3-256 message digests
- MUST preserve even-
yconvention in keygen/sign/verify - MUST emit 64-byte signatures and 32-byte public keys
- MUST reject invalid scalars and coordinates
- MUST use constant-time scalar and point operations
- MUST NOT reuse
auxRand32
HPPR Packet Format
© R.A.Sol
Every packet starts with a markline:
🖧: <hash>
<hash> is BLAKE3-256 in B64A. It identifies
the canonical payload after the first LF.
HPPR packet types:
- Blob: raw bytes
- Plex: metadata + embedded Blob
- Seal: signature + embedded Plex
Shared Rules
- Canonical payload is everything after the markline LF.
- Hash text format is
T.<b64a>.H3whereTisB,P, orS.1 - Header syntax is
<Name>: <value>with exactly one space after:. - Header values are non-empty.
- Header names and values are UTF-8 NFC.
- Control bytes are forbidden in headers:
0x00..0x1Fand0x7F. - Header line length limit is 1024 bytes, excluding trailing LF.
- Line endings are LF only. CR and CRLF are invalid.
- Whitespace is data. Never trim.
Blob
🖧: <hash>
Data-Length: <len>
<data exactly <len> bytes>
Rules:
<hash>MUST start withB..- Canonical payload is
Data-Length, blank line, then<len>data bytes. Data-Lengthis base-10 with no leading zeros, except0.- Readers MUST consume exactly
<len>bytes. - Blob data is opaque.
- Blob
Data-Lengthmax is 32 MiB.
Plex
🖧: <hash>
Group: <group>
App: <app>
Location: <location>
TAI: <tai>
[Extra headers]*
🖧: <blob_hash>
Data-Length: <len>
<data>
Rules:
<hash>MUST start withP..- Required headers appear exactly once, in this order:
Group,App,Location,TAI. - Extra headers follow
TAI. - Canonical payload is all Plex headers plus the full embedded Blob packet, including Blob markline and data bytes.
TAI Format
TAI format:
<seconds>:<subseconds>
Rules:
- 10 digits,
:, then 9 digits - no spaces
- subseconds are nanoseconds
TAI is currently UTC +37 seconds (at time of writing, 2026).
Header Validation
General:
- Header names MUST NOT contain
:. - Header values MAY contain
:.
Group and App:
- non-empty
- case-sensitive
- max 56 bytes
- MUST NOT contain
/,{,},|,# - MUST NOT equal
.or..
Location:
- non-empty; at least one segment is required
- MUST NOT start with
/ - MUST NOT end with
/ - split by
/into segments - each segment is non-empty
- each segment max 128 bytes
- segment MUST NOT contain
{,},| - segment MUST NOT equal
.or.. - total
Locationvalue max 1014 bytes
Extra Headers
Extra-header ordering is canonical:
- sort by header name using bytewise ascending order
- same-name headers MUST be consecutive
- same-name header order is user-defined and hash-significant
Reserved header names (MUST NOT appear as extra headers):
Data-LengthGroupAppLocationTAISeal-BySeal-Sig🖧⋯🖧
Limits:
- at most 512 extra headers
- each extra header line max 1024 bytes (excluding LF)
Seal
🖧: <hash>
Seal-By: <verification-key>
Seal-Sig: <signature>
🖧: <plex_hash>
Group: <group>
App: <app>
Location: <location>
TAI: <tai>
[Extra headers (sorted)]*
🖧: <blob_hash>
Data-Length: <len>
<data>
Rules:
<hash>MUST start withS..- Canonical payload is
Seal-By,Seal-Sig, then full embedded Plex. Seal-Byformat isV.<b64a>.H3(48 chars).- Signing keys use
&.<b64a>.H3. Seal-Sigis an 86-char B64A text signature.- Signature algorithm is HSB3 over the Plex hash digest.
Limits
- Blob data: 32 MiB (
Data-Lengthmax) - Extra headers per Plex: 512
- Header line length: 1024 bytes (excluding LF)
Locationvalue: 1014 bytesLocationsegment: 128 bytesGroupandAppvalue length: 56 bytes
Parsing
- Read markline and verify the type prefix in the hash.
- Blob:
- read
Data-Length - consume exact data bytes
- verify Blob hash
- read
- Plex:
- read required headers in required order
- read extra headers with canonical ordering checks
- parse embedded Blob
- verify Plex hash
- Seal:
- read
Seal-By,Seal-Sig - parse embedded Plex
- verify Seal hash
- verify signature
- read
Reject on any of the following:
- invalid UTF-8 or non-NFC header text
- forbidden control bytes
- CR or CRLF line endings
- size or count limit violations
- required-header order violations
- extra-header ordering violations
- hash mismatch
- signature verification failure
Trailer-hash Format
Trailer-hash format supports streaming when the final hash is only known at the end.
Open marker:
⋯🖧: <type>[ <coordinate>]
Close marker:
⋯🖧: <hash>
Rules:
<type>is one ofB,P,S.- Exactly one opening marker is allowed per packet.
- Double-open, triple-open, and nested-open sequences are invalid.
- Trailer-hash is a transport transformation only. Packet hashes are unchanged.
Seal trailer example:
⋯🖧: S //<group>/<app>/<location>/|/seal/<verification-key>
<escaped data>
⋯🖧: B.<blob-hash>.H3
Group: <group>
App: <app>
Location: <location>
TAI: <tai>
[Extra headers]*
⋯🖧: P.<plex-hash>.H3
Seal-By: <verification-key>
Seal-Sig: <signature>
⋯🖧: S.<seal-hash>.H3
Additional rules:
- Escape data by replacing each
⋯🖧:with⋯⋯🖧:before sending. - Unescape after data extraction by reversing that replacement.
- Data ends at the first
\n⋯🖧:sequence. - Validate all headers and hashes using the same canonical rules.
- To convert to standard packet form:
- remove leading
⋯markers - insert Blob
Data-Length - restore standard nested packet layout
- verify hashes and signature
- remove leading
Both sides buffer full packet bytes to compute and verify hashes.
Thin Format
Thin format stores only part of a packet.
- Thin Seal stores:
- Seal markline
Seal-BySeal-Sig- embedded Plex
🖧:hash line
- Thin Plex stores:
- Plex markline
- Plex headers
- embedded Blob
🖧:hash line
- Blob is already minimal and is effectively thin by definition.
Thin packets omit embedded content bytes and rely on nested hashes for reconstruction.
Hash computation always requires full packet bytes. Implementations MUST resolve thin references before hash verification.
Thin format is for storage and transfer when inner packets are already present.
011 – Basic Example
HPPR Basic Packet Examples
This file shows one Blob, one Plex, and one Seal packet in readable form. Hashes are shortened for line-width clarity.
Blob Example
🖧: B.EXAMPLE_BLOB_HASH.H3
Data-Length: 34
# HPPR Quickstart
This is Blob data.
Plex Example
🖧: P.EXAMPLE_PLEX_HASH.H3
Group: a-group
App: some-app
Location: our-collection/item
TAI: 1640995200:000000000
+Link: source B.EXAMPLE_BLOB_HASH.H3
X-Custom: header value
🖧: B.EXAMPLE_BLOB_HASH.H3
Data-Length: 25
This is embedded blob data.
Seal Example
🖧: S.EXAMPLE_SEAL_HASH.H3
Seal-By: V.EXAMPLE_VKEY.H3
Seal-Sig: EXAMPLE_SIGNATURE_B64A
🖧: P.EXAMPLE_PLEX_HASH.H3
Group: a-group
App: some-app
Location: our-collection/item
TAI: 1640995200:000000000
🖧: B.EXAMPLE_BLOB_HASH.H3
Data-Length: 22
This is signed content.
012 – Create Examples
Reference Script for 011 Examples
This script generates the examples used in
011-PACKETS-BASIC-EXAMPLE.md.
#!/bin/bash -e
blob_msg="# HPPR Quickstart
HPPR packets are content-addressed using BLAKE3.
This Blob contains raw markdown data."
plex_msg="# Plex
This is a plex packet with metadata and links."
seal_msg="# Seal
This is a signed packet that wraps a plex packet."
# Deterministic test-only signatures.
export _UNSAFE_VULNERABLE_HPP_HSB3_DETERMINISTIC_AUX_SEED="examples"
export HPPR_SECRET_KEY="&.ydejWAbshBxyrcKILG3bXkD7fU5c72LtHvLJRfzGXal.H3"
tai="1640995200:000000000"
out="./spec/011-PACKETS-BASIC-EXAMPLE.md"
# Blob
echo -n "$blob_msg" | mkpac --blob > "$out"
blob_hash=$(head -n 1 "$out" | awk '{print $2}')
# Plex
echo -n "$plex_msg" | mkpac \
-g a-group \
-a some-app \
-l our-collection/item \
-t "$tai" \
-H "+Link: source $blob_hash" \
-H "X-Custom: header value" \
-H "Multiple-Values: B" \
-H "Multiple-Values: A" \
>> "$out"
# Seal
echo -n "$seal_msg" | mkpac \
-k "$HPPR_SECRET_KEY" \
-g a-group \
-a some-app \
-l our-collection/item \
-t "$tai" \
>> "$out"Unified Resource Coordinate
© R.A.Sol
HPPR supports two address forms:
- by hash:
////<hash> - by coordinate:
//<group>/<app>/<location>
Hash addresses are immutable. Coordinate addresses are time-dependent and may resolve to different packets as new packets are stored.
A versioned coordinate pins one exact packet.
Terms
- Location: the
Locationheader value in Plex or Seal packets. - Coordinate: a string prefixed with
//://<group>/<app>/<location>.
Version Selectors
Multiple packets can share one coordinate. Add
/|/... to select a version.
- Top-coordinate: no
|, resolves to the latest packet. Example://g/a/loc - Versioned-coordinate: includes type path, TAI,
and hash. Example:
//g/a/loc/|/plex/<tai>/<hash>
Example
Packet:
🖧: P.EXAMPLE~HASH~EXAMPLE~HASH~EXAMPLE~HASH~EX.H3
Group: a-group
App: some-app
Location: our-collection/item
TAI: 1640995200:123000000
...
Addresses:
- Direct hash:
////P.EXAMPLE~HASH~EXAMPLE~HASH~EXAMPLE~HASH~EX.H3 - Top-coordinate:
//a-group/some-app/our-collection/item - Versioned-coordinate:
//a-group/some-app/our-collection/item/|/plex/1640995200:123000000/P.EXAMPLE~HASH~EXAMPLE~HASH~EXAMPLE~HASH~EX.H3
Get resolution
In the tables below, <loc> is the Location
header value and does not end with /.
The tip is the latest packet at a path.
Tie-break order is highest TAI, then highest hash.
| Path | Gets |
|---|---|
.../<loc> |
tip |
.../<loc>/ |
tip |
.../<loc>/| |
tip |
.../<loc>/|/plex |
Plex tip |
.../<loc>/|/plex/<tai> |
Plex tip at tai |
.../<loc>/|/plex/<tai>/<hash> |
exact Plex |
.../<loc>/|/seal |
Seal tip |
.../<loc>/|/seal/<vkey> |
Seal tip for signer |
.../<loc>/|/seal/<vkey>/<tai> |
signer Seal tip at tai |
.../<loc>/|/seal/<vkey>/<tai>/<hash> |
exact Seal |
List resolution
| Path | Lists |
|---|---|
.../<loc>/ |
child locations, plus |/ when present |
.../<loc>/|/ |
plex/ and or seal/ |
.../<loc>/|/plex/ |
TAIs |
.../<loc>/|/plex/<tai>/ |
hashes |
.../<loc>/|/seal/ |
signer verification keys |
.../<loc>/|/seal/<vkey>/ |
TAIs |
.../<loc>/|/seal/<vkey>/<tai>/ |
hashes |
022 – Filesystem
Filesystem Repository
© R.A.Sol
This spec defines filesystem storage for HPPR packets.
It follows packet rules in 010 and address rules in
020.
Repository Layout
repo/
├── hash/<Type>/<hh>/<tail>.H3
├── ref/B/<hh>/<tail>/...
├── ref/P/<hh>/<tail>/...
├── index/<group>/<app>/...
├── detach/<hash>
└── .tmp/
hash/: packet storage by typeB,P,Sref/: back-reference treesindex/: coordinate indexdetach/: detached hashes.tmp/: atomic write staging
Filesystem requirements:
- MUST be case-sensitive
- MUST support UTF-8 names except
/and NUL - implementation MUST refuse startup if unsupported
Hash Storage (hash/)
Path form:
hash/<T>/<hh>/<tail>.H3
Where:
<T>isB,P, orS<hh>is first two B64A digest chars<tail>is remaining digest chars
Storage format:
- Blob (
B): raw content bytes only - Plex (
P): thin format - Seal (
S): thin format
Blob file size defines Data-Length during
reconstruction.
Index Storage
(index/)
Plex path:
index/<group>/<app>/<loc>/|/plex/<tai>/<hash>
Seal path:
index/<group>/<app>/<loc>/|/seal/<vkey>/<tai>/<hash>
Rules:
- marker files are zero-byte
- filename/path encodes identity and version
<loc>is Location header value without trailing slash<tai>usesseconds:subsecondsform
Back-Reference Storage
(ref/)
Blob to Plex references:
ref/B/<hh>/<tail>/<plex-hash>
Plex to Seal references:
ref/P/<hh>/<tail>/<seal-hash>/<vkey>
These support reverse traversal from inner to outer packet.
Store Flow
- Write Blob raw bytes to
hash/B/.... - For Plex:
- write thin Plex to
hash/P/... - add
index/.../|/plex/<tai>/<hash> - add
ref/B/.../<plex-hash>
- write thin Plex to
- For Seal:
- write thin Seal to
hash/S/... - ensure embedded Plex and Blob exist
- add
index/.../|/seal/<vkey>/<tai>/<seal-hash> - add embedded Plex index entry
- add
ref/B/.../<plex-hash>andref/P/.../<seal-hash>/<vkey>
- write thin Seal to
Duplicates SHOULD succeed as idempotent writes. Use
.tmp/ plus atomic rename for final placement.
Detach Cleanup
When detaching Plex or Seal:
- remove index entries
- remove matching
ref/B/.../<plex-hash> - if that
ref/B/...directory becomes empty, record blob hash indetach/ - never detach the empty-blob constant hash
Reconstruction
- Blob: synthesize markline and
Data-Lengthfrom file size + raw bytes. - Plex/Seal: resolve embedded hashes from
hash/and rebuild full packet.
Hash verification requires full reconstructed packet bytes.
Tip Tracking
Tips are materialized as links under
index/.../|/.
Required tip links:
- top tip:
|/tip - plex tip:
|/plex/tip - seal type tip:
|/seal/tip - per-signer seal tip:
|/seal/<vkey>/tip
Tip update rule:
- compare
(tai, hash)lexicographically - newer value replaces existing link target
Tip Recovery
If a tip link is missing while version directories exist, implementation MUST:
- scan affected
|/plex/and|/seal/once - recompute latest entries
- recreate missing tips
If scan finds no packets, empty |/ structure SHOULD
be cleaned up.
After detach, recompute tip links for the affected coordinate.
Portability
- Atomic rename is required.
- If filesystem lacks symlinks, emulate tip links with files.
- Emulated tip links MUST be hidden from
🖧LISTexactly as symlinks are.
HPPR Basic Commands
© R.A.Sol
Security Model
HPPR validates packets at the packet layer, not the channel layer.
- markline hash proves packet bytes are unchanged
- Seal signature proves which key signed the packet
Non-stream commands use one request packet and one response
packet per exchange. Streaming behavior is command-specific (for
example 🖧WATCH, 🖧AUDIT in 050).
Null Packet
The sentinel hash 0.H3 is never computed or
verified.
Use 0.H3 for:
- HELLO response
- error responses
Null packets are never stored in repository storage.
🖧: 0.H3
[Header: value]*
Data-Length: <len>
<data>
Header rules follow 010 (format, encoding, control bytes, line length). Differences from Plex:
- Any header name is allowed (no required headers, no ordering rules).
- Header count max is 512.
Data-Lengthis the final header before the blank line.- Data max is 34 MiB (32 MiB payload + 2 MiB envelope overhead).
Session Lifecycle
Each connection starts with HELLO.
The repo creates a session-id and binds it to the connection.
Later requests must carry that session-id in
Location.
HELLO Request
Client sends a Null packet with App: 🖧HELLO:
🖧: 0.H3
App: 🖧HELLO
Data-Length: 0
HELLO Response
Repo responds with session parameters and capabilities:
🖧: 0.H3
Session-ID: <session-id>
Repo-Name: <repo-name>
Seal-By: <repo-vkey>
[PHC: $argon2id$v=19$m=<m>,t=<t>,p=<p>$]
[Command: 🖧NAME VERSION]*
[Transport: <via>]*
Data-Length: 0
Header meanings:
| Header | Meaning |
|---|---|
Session-ID |
Repo-generated TAI for this connection session |
Repo-Name |
Repository identifier. Default is localhost |
Seal-By |
Repo verification key from ring0 keys, oldest by TAI/hash |
PHC |
Argon2id parameters for Ring1 token derivation |
Command |
Supported command and integer version |
Transport |
Available transport via string (031) |
Sealed HELLO
After session setup, an authenticated 🖧HELLO Seal
request returns fresh status and capabilities. The response is a
Seal signed by repo-vkey. Session-ID and
PHC are omitted.
No ACL check. Any authenticated identity may send
🖧HELLO.
Repo Identity
Repo identity is stored at:
//repo/admin/identity/|
It is a self-signed Seal. Repo-Name in that identity
is returned in HELLO.
Authenticated Requests
After HELLO, commands are sent as Seal packets. App values
starting with 🖧 are protocol commands.
🖧: S.<hash>.H3
Seal-By: <client-vkey>
Seal-Sig: <signature>
🖧: P.<hash>.H3
Group: <group>
App: 🖧<COMMAND>
Location: <location-with-session>
TAI: <tai>
🖧: B.<hash>.H3
Data-Length: <len>
<args>
Location format depends on auth scheme:
| Scheme | Group | Location |
|---|---|---|
| Ring1 (050) | repo |
<repo>/<ring1>/<session-id> |
| Ring2 (060) | <target-group> |
<repo>/<session-id> |
| Anyone | repo |
<repo>/anyone/<session-id> |
A different signing key requires a new connection and HELLO.
Command Responses
Successful responses are Seal packets signed by the repo verification key.
🖧: S.<hash>.H3
Seal-By: <repo-vkey>
Seal-Sig: <signature>
🖧: P.<hash>.H3
Group: repo
App: 🖧<COMMAND>
Location: <repo-name>/<session-id>
TAI: <response-tai>
🖧: B.<hash>.H3
Data-Length: <len>
<response-data>
Commands
| Command | Payload | Response | ACL |
|---|---|---|---|
🖧HELLO |
(none) | session parameters (Null or Seal) | — |
🖧GET |
<urc> (020) |
full packet bytes | read |
🖧HEADERS |
<urc> |
markline + headers to first blank line | read |
🖧LIST |
<urc-coordinate> |
LF-separated children, sorted | list |
🖧STORE |
complete packet bytes | LF-separated stored hashes | write |
🖧STORE
🖧STORE payload is a complete packet starting with
markline.
It also accepts thin packets:
- thin Plex may reference existing Blob with
🖧: B.<hash>\n - thin Seal may reference existing Plex with
🖧: P.<hash>\n
For authenticated App: 🖧STORE, request
Data-Length may be up to 34 MiB. This allows 32 MiB
packet payload plus up to 2 MiB envelope overhead.
Other authenticated Seal apps remain capped at 32 MiB.
Response data lists stored hashes from outermost to innermost:
S.seal~hash.H3
P.plex~hash.H3
B.blob~hash.H3
- storing a Seal returns 3 lines
- storing a Plex returns 2 lines (Plex, Blob)
- storing a Blob returns 1 line
Errors
Errors are Null packets. Data starts with one status line:
ERROR <TYPE> <detail>keeps the connection openFATAL <TYPE> <detail>closes the connection
Standard error types:
NOT_FOUNDFORBIDDENTOO_LARGEINVALIDINVALID_IDENTITYUNAUTHORIZEDHELLO_REQUIREDINTERNAL
Example:
🖧: 0.H3
Data-Length: 21
ERROR NOT_FOUND ring1
031 – Via Syntax
Via Syntax
© R.A.Sol
A via string identifies a transport endpoint.
Grammar
Full form:
scheme+host[:port]
Bare form (auto-negotiate transport):
host[:port]
Hostless form (used in HELLO Transport headers where
host is already known):
scheme[:port]
Unix domain socket:
unix+<absolute-path>
Schemes
| Scheme | Default port | Transport |
|---|---|---|
tcp |
4777 |
TCP plaintext |
quib |
4776 |
QUIB encrypted (033) |
ws |
4778 |
WebSocket |
udp |
4777 |
UDP datagram (stateless) |
auto |
4777 |
Auto-negotiate (equivalent to bare form) |
Host
Host is a hostname, IPv4 address, or bracketed IPv6 address.
IPv6 bracket form: [::1] or
[::1]:port.
Port
Port is optional. When omitted, the scheme default applies. Bare
form defaults to 4777.
Hostless Resolution
Hostless forms appear in HELLO Transport headers.
The client fills in the host from its existing connection.
Examples:
tcp— TCP on default port, same hostudp:4777— UDP on port 4777, same hostquib:4776— QUIB on port 4776, same host
Display Form
The canonical display form is:
scheme+host:portwhen a scheme is sethost:portfor auto-negotiateunix+<path>for Unix sockets
Examples
| Input | Scheme | Host | Port |
|---|---|---|---|
tcp+10.0.0.1 |
tcp | 10.0.0.1 | 4777 |
tcp+10.0.0.1:9000 |
tcp | 10.0.0.1 | 9000 |
quib+1.1.1.1:4776 |
quib | 1.1.1.1 | 4776 |
udp+10.0.0.1 |
udp | 10.0.0.1 | 4777 |
ws+example.com |
ws | example.com | 4778 |
auto+example.com |
auto | example.com | 4777 |
example.com |
auto | example.com | 4777 |
example.com:9000 |
auto | example.com | 9000 |
tcp:4777 |
tcp | (from context) | 4777 |
udp |
udp | (from context) | 4777 |
unix+/tmp/hppr.sock |
— | — | — |
032 – UDP
UDP Datagram Transport
© R.A.Sol
Stateless single-datagram transport for fast lookups.
Via: udp+host[:port] (031). Default port:
4777.
Advertised in HELLO Transport headers as
udp[:port].
Supported Commands
🖧GET🖧HEADERS
Protocol
No HELLO, no session. Each exchange is one request datagram and one response datagram.
Request is a stateless sealed request (035) in a single UDP datagram.
Response is one datagram: raw packet bytes on success, Null packet on error. Unrecoverable failures produce no response.
033 – QUIB
QUIB
© R.A.Sol
HPPR QUIB transport uses a custom crypto layer instead of TLS 1.3.
Primitives:
- Key agreement: secp256k1 ECDH
- Key derivation: BLAKE3
derive_key - QUIC payload encryption: ChaCha20-Poly1305
- QUIC header protection: ChaCha20 (RFC 9001 §5.4.4 pattern)
- MAC: BLAKE3 keyed hash
No TLS, no X.509, no rust ring. Both peers must run
this crypto.
Handshake
Two-message exchange inside QUIC Initial CRYPTO frames. Both
messages travel in the Initial encryption space. After ECDH
completes, each side writes a single 0x00 confirmation
byte into Handshake CRYPTO to signal readiness, then upgrades to
Data-space keys.
Message 1 (client to server, Initial CRYPTO)
client_ephemeral_pubkey (33 bytes, SEC1 compressed secp256k1)
client_transport_parameters (variable)
Message 2 (server to client, Initial CRYPTO)
server_ephemeral_pubkey (33 bytes, SEC1 compressed secp256k1)
encrypted_hello_length (2 bytes, big-endian u16)
encrypted_hello (variable, ChaCha20-Poly1305)
server_transport_parameters (variable)
The entire Message 2 is sent in a single CRYPTO frame. The client derives the shared secret from the server’s ephemeral pubkey, then decrypts the hello and parses transport parameters in one step.
Transport parameters use quinn-proto’s wire encoding
(TransportParameters::write /
TransportParameters::read).
Encrypted HELLO Payload
The server encrypts a HELLO payload into Message 2 using
hello_key from the key derivation XOF stream (offset
256, see Key Derivation).
Encryption uses ChaCha20-Poly1305 with a zero nonce (unique key per connection).
Plaintext is a standard Null HELLO packet (per
030-BASIC-COMMANDS.md) without Session-ID.
Contains:
Repo-NameSeal-ByPHC(optional)FormatTransport(repeated, optional)Command(repeated)
When available, Transport headers advertise other
active transports.
The encrypted hello includes a 16-byte Poly1305 tag appended to the ciphertext.
Post-handshake stream bootstrap
QUIB keeps standard QUIC endpoint roles:
- the client initiates the connection (
connect) - the server accepts the connection (
accept)
All application streams are client-initiated. The HELLO payload is delivered during the handshake, so no server-initiated stream is needed.
- client reads repo metadata from
handshake_data()(the decrypted HELLO) - client derives Session-ID from keying material
- client opens a bidirectional stream and sends commands
- server accepts streams and dispatches commands
The server uses accept_bi() for all streams. The
client uses open_bi() for the command stream and any
additional streams.
Session-ID Derivation
Session-ID is session_id from the key derivation XOF
stream (offset 288, see Key Derivation).
Output is 32 bytes. Text format is Q#<b64a>
(the Q# prefix distinguishes QUIB-derived session-ids
from TAI-based ones).
Both sides compute the same value. This replaces the server-generated TAI session-id used on TCP connections.
The 🖧HELLO command remains available as an optional
application-level request for capabilities refresh, repo-name
lookup, or sealed status queries.
Key Agreement
shared_point = ECDH(client_ephemeral_secret, server_ephemeral_pubkey)
shared_bytes = shared_point.x (32 bytes, big-endian)
Both sides generate fresh ephemeral secp256k1 keypairs per connection.
Key Derivation
All keys for a connection are derived from the ECDH shared secret using a single BLAKE3 XOF stream:
stream = BLAKE3.derive_key_xof("hppr-🖧/quib/keys", shared_bytes)
Stream layout (368 bytes):
| Offset | Length | Name |
|---|---|---|
| 0 | 32 | hs_c2s_packet |
| 32 | 32 | hs_s2c_packet |
| 64 | 32 | hs_c2s_header |
| 96 | 32 | hs_s2c_header |
| 128 | 32 | c2s_packet |
| 160 | 32 | s2c_packet |
| 192 | 32 | c2s_header |
| 224 | 32 | s2c_header |
| 256 | 32 | hello_key |
| 288 | 32 | session_id |
| 320 | 12 | hs_c2s_iv |
| 332 | 12 | hs_s2c_iv |
| 344 | 12 | c2s_iv |
| 356 | 12 | s2c_iv |
Handshake keys (hs_*) protect the Handshake packet
space. Data keys protect the 1-RTT (Data) packet space.
Each packet key has a sibling IV derived from the same XOF stream. The IV is used in nonce construction (see Payload Encryption below).
quinn-proto requires two key upgrades during handshake: Initial
to Handshake, then Handshake to Data. Both sides derive keys from
this layout and return them via write_handshake. Each
side writes a 0x00 confirmation byte into Handshake
CRYPTO when returning 1-RTT keys, ensuring the peer receives a
Handshake-space packet and completes the transition to Data.
QUIC Key Update
Each direction derives 44 bytes via BLAKE3 XOF from the current packet key:
stream = BLAKE3.derive_key_xof("hppr-🖧/quib/update", current_packet_key)
next_packet_key = stream[0..32]
next_iv = stream[32..44]
Both key and IV are replaced. Header keys are not updated.
QUIC Payload Encryption
ChaCha20-Poly1305 (RFC 8439).
Nonce construction (12 bytes), following the TLS 1.3 pattern (RFC 9001 §5.3):
nonce = IV XOR (0x00000000 || BE64(packet_number))
The IV is the 12-byte sibling value derived alongside the packet key (from the XOF stream for initial/handshake/data keys, or from the key update XOF for rotated keys). The packet number is encoded as an 8-byte big-endian integer and XORed into the last 8 bytes of the IV.
Tag length: 16 bytes, appended to ciphertext.
Limits:
- Confidentiality: 2^62 QUIC packets
- Integrity: 2^36 decryption failures
QUIC Header Protection
ChaCha20 mask generation per RFC 9001 §5.4.4.
Input: 16-byte sample from encrypted payload at offset
pn_offset + 4.
counter = LE_u32(sample[0..4])
nonce = sample[4..16]
mask = ChaCha20(hp_key, counter, nonce, zeroes)[0..5]
Apply mask:
- Long headers:
header[0] ^= mask[0] & 0x0f - Short headers:
header[0] ^= mask[0] & 0x1f header[pn_offset .. pn_offset+pn_len] ^= mask[1..1+pn_len]
Sample size: 16 bytes.
Initial QUIC Keys
Before handshake completes, both peers derive identical keys from the destination connection ID using a single BLAKE3 XOF stream:
stream = BLAKE3.derive_key_xof("hppr-🖧/quib/initial", dst_cid)
Stream layout (152 bytes):
| Offset | Length | Name |
|---|---|---|
| 0 | 32 | c2s_packet |
| 32 | 32 | s2c_packet |
| 64 | 32 | c2s_header |
| 96 | 32 | s2c_header |
| 128 | 12 | c2s_iv |
| 140 | 12 | s2c_iv |
Initial keys use the same ChaCha20-Poly1305 and ChaCha20 algorithms as 1-RTT keys. Initial protection is not secret (the CID is on the wire).
Retry Integrity
Retry tags use BLAKE3 keyed MAC with a fixed public key, truncated to 16 bytes.
tag = BLAKE3.keyed_hash(RETRY_KEY, len(orig_dst_cid) || orig_dst_cid || retry_pseudo_packet)[..16]
RETRY_KEY is a fixed 32-byte constant compiled into
both peers:
8a 3f c1 7b 52 e6 d9 04 ab 1e 73 f0 28 95 dc 46
b3 67 0a 5d e4 89 f1 3c 7e b2 05 6f d8 a1 43 97
This mirrors RFC 9001’s public retry integrity key. The tag is tamper-detection, not a secret.
HMAC
BLAKE3 keyed hash with 32-byte output.
mac = BLAKE3.keyed_hash(key, data)
Verification compares all 32 bytes.
Token Encryption
Address validation tokens use per-token derived ChaCha20-Poly1305.
Key derivation:
aead_key = BLAKE3.derive_key("hppr-🖧/quib/token-aead", master_key || random_bytes)
Seal and open use a zero nonce. Each token has a unique derived key, making nonce reuse impossible.
Export Keying Material
output = BLAKE3.derive_key(label, shared_bytes || context)
label is interpreted as a UTF-8 string for the
BLAKE3 context parameter. Output length is variable via BLAKE3
XOF.
Peer Identity
peer_identity returns the peer’s ephemeral SEC1
compressed secp256k1 public key (33 bytes).
handshake_data returns the decrypted HELLO payload
bytes (client side) or the client ephemeral public key (server
side).
For clients, the HELLO payload is a standard Null packet containing the server’s repo-vkey and capabilities.
No 0-RTT
Early data is not supported. early_crypto returns
None.
Context Strings
All BLAKE3 derive_key / XOF context strings used by
this spec:
| Context | Use |
|---|---|
hppr-🖧/quib/keys |
XOF for all handshake/data keys, IVs, hello key, session-id |
hppr-🖧/quib/initial |
XOF for initial keys and IVs |
hppr-🖧/quib/update |
XOF for key update (packet key + IV per direction) |
hppr-🖧/quib/token-aead |
Token AEAD key derivation |
035 – Stateless
Stateless Requests
© R.A.Sol
Stateless requests skip the HELLO handshake. A client sends a
Seal before HELLO using
Location: <via>/anyone/stateless.
<via> is the via string (031) for the transport the client
connected to (e.g. tcp+1.1.1.1:4777). The server does
not validate this segment for stateless requests.
Rules
Groupisrepo- Location session segment is the literal string
stateless - Ring1 name is
anyone - Client signs with an ephemeral keypair
- Server resolves identity as
anyoneand appliesanyoneACL rules - Only
🖧GET,🖧LIST,🖧HEADERSare allowed; others returnFORBIDDEN - No session binding validation (repo-name and session-id are unchecked)
- TAI tolerance still applies
- Connection stays open after response
- Client may send
🖧HELLOafterward to upgrade to session mode
Stateless requests are replayable by design. They provide packet integrity via the Seal signature, not identity. Use only for public read-only data.
UDP
UDP uses stateless requests exclusively. See 032.
HPPR Access Control
© R.A.Sol
ACL rules are defined per repository identity.
Each rule targets a coordinate prefix and controls three operations:
readwritelist
Rule evaluation uses longest-prefix match with per-operation inheritance.
Operations
read: fetch packet data- commands:
🖧GET,🖧HEADERS,🖧MEMBERS(060)
- commands:
write: store or mutate repository state- commands:
🖧STORE,🖧ADD,🖧DETACH
- commands:
list: enumerate or subscribe to coordinate trees- commands:
🖧LIST,🖧WATCH,🖧TIPS
- commands:
Rule Format
Rule syntax:
<ops> <coordinate-prefix>
<ops> is exactly three characters:
[r|d|.][w|d|.][l|d|.]
Meaning:
r,w,l: explicit allow for that operationd: explicit deny for that operation.: inherit from the next-longest matching rule
Examples:
ACL-Rule: rwl //u/chess/
ACL-Rule: r.l //u/mail/
ACL-Rule: rdl //u/market/
ACL-Rule: .w. //u/market/nl/eindhoven/
Common patterns:
rwl: full accessr.l: read and list, inherit writeddd: deny allrwd: read and write, deny list
Rule Ordering
Rules MUST be stored in canonical sorted order.
Sort bytewise, with custom priority:
|sorts before//sorts before all other bytes
Equivalent compare mapping:
|=>0x01/=>0x02
Resolution
Given a request coordinate:
- Find the longest matching rule prefix.
- For each operation, apply explicit allow or deny when present.
- For
.values, continue to the next-longest matching rule. - If no explicit decision is found, deny.
For read and write checks, evaluate against the packet versioned coordinate.
Prefix examples:
| Prefix | Matches |
|---|---|
//u/a/README.md/| |
exact README.md location |
//u/a/README.md/ |
README.md and children |
//u/a/README.md |
also README.md-draft |
Identity Storage
Rules are stored as ACL-Rule Plex headers.
Location depends on identity scheme:
- Ring1 (050):
//repo/admin/ring1/<name>/setup/|
- Ring2 (060):
//<group>/admin/setup/|/seal/<repo-vkey>
HPPR Ring1 Authentication
© R.A.Sol
Ring1 is the standard repository-auth API. All Ring1 requests are Seals signed by a Ring1 member key.
The key can be derived from a password or set up explicitly.
Request Envelope
Ring1 request form:
🖧: S.<hash>.H3
Seal-By: <member-vkey>
Seal-Sig: <signature>
🖧: P.<hash>.H3
Group: repo
App: 🖧<COMMAND>
Location: <repo-name>/<ring1-name>/<session-id>
TAI: <tai>
🖧: B.<hash>.H3
Data-Length: <len>
<args>
Rules:
GroupisrepoLocationcarries repo name, ring1 name, and HELLO sessionSeal-Bymust be a configured member key or derived member key
Keys
ring0 Key
The repo verification key is the oldest packet at:
//repo/admin/ring1/ring0/keys/|/seal
Oldest means lowest (TAI, hash).
Ring1 and Ring2 setup packets must be signed by this repo key.
Ring1 Keys Config
Path:
//repo/admin/ring1/<name>/keys/|/seal/<vkey>
Packet contains Secret-Key: &.<b64a>.H3.
🖧ADD can use these keys.
Setup Config
Path:
//repo/admin/ring1/<name>/setup/|
Signed by ring0.
Headers:
Ring1-NameRing1-Secret-Token(optional)Ring1-Expire(optional)Member(repeatable)ACL-Rule(repeatable)
Ring1-Name constraints:
- max 128 bytes
- must match path segment
- must not contain
/ { } | - must not equal
.or..
Secret Token Derivation
Ring1-Secret-Token format:
<derived-token> <original-secret>
Split on first ASCII space.
- left side:
V.<b64a>.H3 - right side: original secret, may contain spaces
Client derives <derived-token> using Argon2id
with HELLO PHC. Repo does not recompute Argon2id. Repo
uses <derived-token> directly.
Argon2id:
- password: UTF-8 original secret
- salt: first 16 bytes of
BLAKE3.derive_key("hppr-🖧/phc-salt", "<ring1>/<repo-vkey>") - output length: 32 bytes
Then HSB3 key derivation secret is:
<derived-token>/<ring1-name>/<repo-vkey>
Special Ring1 Names
ring0: full repository access, ACL bypassanyone: unauthenticated fallback ACL identityguest: signed non-member identity used by Ring2 flow
Pre-ACL Defaults
These apply before ACL-Rule evaluation and are
final.
ddd //repo/admin/ring1/ring0/dwd //repo/admin/request/ring1/rd. //repo/admin/ring1/rd. //repo/admin/identity
Ring1 Commands
Ring1 includes all commands from 030 and adds:
🖧HELLO🖧ADD🖧DETACH🖧TIPS🖧WATCH🖧AUDIT
🖧ADD
Input is LF-separated headers, blank line, optional data.
Type selection:
Seal-Bypresent: create Seal- else if any Plex header present: create Plex
- else: create Blob
Defaults:
Group: uApp: indexLocation: rootTAI: now
References:
🖧: B.<hash>references existing Blob🖧: P.<hash>references existing Plex
🖧DETACH
Payload is one hash. Removes packet from coordinate index only. Stored packet remains in hash storage. ring0 only.
🖧TIPS
Returns LF-separated versioned coordinates for tip packets.
🖧WATCH
Returns a stream of + and - lines for
matching coordinate prefix changes. Events are filtered by
list permission.
🖧AUDIT
Streams audit log lines. ring0 only.
Errors
Common Ring1 failures:
HELLO_REQUIREDUNAUTHORIZED invalid signatureUNAUTHORIZED not a memberNOT_FOUND ring1NOT_FOUND inner packetINVALID configINVALID sessionUNAUTHORIZED ring1
Security
Ring1 expects encrypted transport for confidentiality. Replay
resistance uses HELLO-bound session IDs in request
Location.
HPPR Ring2 Authentication
© R.A.Sol
Ring2 is group-based authentication. Signer authorization is based on membership in the target group.
Request Envelope
Ring2 request form:
🖧: S.<hash>.H3
Seal-By: <member-vkey>
Seal-Sig: <signature>
🖧: P.<hash>.H3
Group: <target-group>
App: 🖧<COMMAND>
Location: <repo-name>/<session-id>
TAI: <tai>
🖧: B.<hash>.H3
Data-Length: <len>
<args>
Rules:
Groupis the target group, notrepoLocationis<repo>/<session-id>Seal-Bymust resolve to group membership- non-members evaluate under Ring1
guestACL fallback
Group Setup
Setup path:
//<group>/admin/setup/|/seal/<repo-vkey>
Setup packet is signed by repo-vkey.
Headers:
Ring2-Name(must equal<group>)Ring2-Expire(optional)ACL-Rule(optional, repeatable)
Each ACL-Rule coordinate must start with
//<group>/.
Pre-ACL Default
Before ACL rules, apply:
r.. //<group>/admin/members/|
This grants read access to membership config.
Membership Config
Membership packets live at:
//<group>/admin/members/|/seal/<vkey>
Multiple packets may exist under different signers and versions.
Supported headers:
Member: <vkey> [<tags>...]Member-Delegate: [<group>]|[<vkey>[/<tai>/<hash>]] [<mods>...]
Member
Adds one member key plus optional tags.
Member-Delegate
Delegates membership from another config source. Pipe separator
| is required.
Defaults:
- omitted group means current group
- omitted vkey means signer of current packet
Pinned form:
<vkey>/<tai>/<hash>pins to exact member packet version
Modifier:
dynamicfollows unpinned delegates
Tag modifiers:
*: inherit all tags+tag: inherit tag if presenttag: grant tag!tag: deny tag
Traversal is depth-first with max depth 8.
🖧MEMBERS
Returns expanded member list with tags.
Payload is a // URC.
Shorthand:
//<group>expands to//<group>/admin/members/|/seal/<repo-vkey>
Response:
- LF-separated lines:
<vkey> [<tags>...] - sorted by vkey
Errors
Common Ring2 failures:
HELLO_REQUIREDUNAUTHORIZED invalid signatureUNAUTHORIZED not a memberNOT_FOUND ring2 setupINVALID configINVALID sessionUNAUTHORIZED ring2
HPPR Conventions
© R.A.Sol
This spec defines bootstrap and onboarding conventions.
Bootstrap Requirements
A new repository needs four initial items.
- ring0 keys
- path:
//repo/admin/ring1/ring0/keys/|/seal/<vkey> - self-signed Seal with
Secret-Key - oldest key becomes repo-vkey
- path:
- ring0 setup
- path:
//repo/admin/ring1/ring0/setup/|/seal/<repo-vkey>
- path:
- anyone setup
- path:
//repo/admin/ring1/anyone/setup/|/seal/<repo-vkey>
- path:
- guest setup
- path:
//repo/admin/ring1/guest/setup/|/seal/<repo-vkey>
- path:
ring0 Initial Token
Initial ring0 member derives from token init:
secret = "init/ring0/<repo-vkey>"
signing_key = derive_key_from_secret(secret)
Replace bootstrap setup immediately with a secure token.
Default anyone Setup
Ring1-Name: anyone
ACL-Rule: .w. //repo/admin/request/ring1/
ACL-Rule: r.l //repo/admin/route/
ACL-Rule: r.l //u/
Default guest Setup
Ring1-Name: guest
Joining a Repository
Standard Ring1 join flow:
- user writes request to:
//repo/admin/request/ring1/<name>/setup/| - user watches reply path:
//repo/admin/request/ring1/<name>/reply/| - admin approves or denies in reply packet
- if approved, admin creates ring1 setup packet
- user derives key and reconnects
Provisional Access
An anyone request gets temporary read/list access to
matching reply path:
//repo/admin/request/ring1/<name>/reply/|
<name> must match first segment from request
Location.
Joining a Group
Ring2 join flow:
- request path:
//<group>/admin/request/member/|/seal/<requester-key> - reply path:
//<group>/admin/request/member/<requester-key>/reply/|
Recommended guest ACLs:
ACL-Rule: .w. //<group>/admin/request/member/|/seal/
ACL-Rule: r.l //<group>/admin/request/member/
Request headers:
Request-Tags(optional)
Reply headers:
Request-Status: approved|denied|pending+Link: request <hash>
Group Deployment Pointer Convention
Canonical app content should live under:
//u/apps/<publisher>/<app>/...
Each group publishes its deployment pointer at:
//<group>/admin/deploy/<app>/|
Required headers:
Deploy-App: <app>Deploy-Root: //<...>Deploy-Signer: V.<...>.H3
Runtime intent:
- clients resolve
//<group>/<app>/...through the deploy pointer - clients then read content from
Deploy-Root
Group forks are supported by pointing Deploy-Root to
a group-owned subtree instead of the canonical
//u/apps/... tree.
+Link Header
Generic form:
+Link: <tag> <hash>
Common tags:
request: this packet replies to that requestsupersedes: this packet replaces older version
Typed links are headers named [Type]+Link. Tools
should scan header names containing +Link.
Hash links form a DAG. Cycles are impossible because packet hash includes all header bytes.
Chunked Content Convention
Large content can be split into chunk blobs with a manifest packet.
Manifest detection:
- has one or more
Chunk+Linkheaders - has
Data-Length: 0
Syntax:
Chunk+Link: <start>..<end> <T>.<hash>.H3
- range is half-open: start inclusive, end exclusive
T=B: raw blob chunkT=P: nested chunk manifest
Required header:
Content-Total-Length: <n>
Optional headers:
Content-TypeContent-Hash-Full: B.<hash>.H3
Validation:
- chunk ranges contiguous
- no overlap
- total span exactly
0..<Content-Total-Length>
Nested manifests use 0-relative ranges in each sub-manifest. Depth limit is 8.
Default chunk size is 32 MiB. Smaller chunks improve random access and increase overhead.
Ring0-Proxy Convention
Ring1 may request a ring0-mediated action.
Request path:
//repo/admin/ring1/<ring1-name>/🖧<COMMAND>/|
Reply path:
//repo/admin/ring1/<ring1-name>/🖧<COMMAND>/reply/|
Supported commands:
🖧LIST🖧HEADERS🖧ADD
🖧GET is excluded. Use 🖧HEADERS to
obtain hash, then fetch blob by hash.
HPPR Replication
© R.A.Sol
Repository replication uses command 🖧EXCHANGE. It
synchronizes packets while preserving ACL decisions.
🖧EXCHANGE
🖧EXCHANGE has three phases:
- announce
- transfer
- finalize
Phase 1: Announce
Requester sends up to 1024 LF-separated lines:
NEED <versioned-coord>
HAVE <versioned-coord>
Responder returns FIN or PENDING
followed by status lines:
SEND <coord>
DENY <coord>
RECV <coord>
HAVE <coord>
Status meanings:
SEND: responder will send this packetDENY: denied by ACL or policyRECV: responder wants this packet from requesterHAVE: responder already has this packet
Phase 2: Transfer
Both peers stream raw packets concurrently in both directions.
Implementations MUST perform bidirectional I/O concurrently. Unidirectional blocking can deadlock the exchange.
Packet boundaries are parsed using Data-Length.
Phase 3: Finalize
Responder reports final per-coordinate results:
FIN
OK <coord>
FAIL <coord> <type> <msg>
ACL Rules in Phase 1
For NEED <coord>:
- check
read - if readable and exists:
SEND - else:
DENY
For HAVE <coord>:
- check
read - if denied:
DENY - if allowed and exists:
HAVE - if missing, check
write - if writable:
RECV - else:
DENY
Blob-Addressed 🖧EXCHANGE
🖧EXCHANGE also accepts blob hash addressing:
NEED ////B.<hash>.H3
HAVE ////B.<hash>.H3
Rules:
- only
B.hashes are valid P.andS.in blob-addressed mode are FATAL invalid requests- no ACL checks in this mode
- any authenticated session may request blob hash sync
Decision rules:
NEED: blob exists =>SEND, elseDENYHAVE: blob exists =>HAVE, elseRECV
Transfer still uses full raw blob packets.
🖧STREAM_IN
🖧STREAM_IN ingests live trailer-format Seal
segments.
Request:
App: 🖧STREAM_IN- data:
<coordinate-prefix>
Response:
- sealed
OK
After OK, connection enters relay mode and accepts
trailer segments.
Per segment:
- relay bytes to matching
🖧STREAM_OUTsubscribers - detect segment close marker
- verify hash and signature
- store packet
- emit
🖧WATCHevent
ACL: write checked per segment at store time.
Disconnect behavior:
- publisher disconnect ends stream
- no explicit end marker required
- partial in-flight segment is discarded
- subscriber sockets are closed
No FATAL is injected into relay stream. Subscribers detect truncation via trailer parsing.
Size rule:
- each segment must respect 32 MiB blob limit
Continuity recommendation:
- include
Previous-Segment: <hash>after first segment
🖧STREAM_OUT
🖧STREAM_OUT subscribes to live trailer bytes by
prefix.
Request:
App: 🖧STREAM_OUT- data:
<coordinate-prefix>
Behavior:
- enters dedicated relay mode immediately
- no
OKresponse packet - when no active publisher exists for the prefix, subscriber waits for one
- wait uses a fixed server timeout
- on wait timeout, repo writes
FATAL NOT_FOUND wait timeoutand closes - forwards bytes from matching active publishers
Late joiners receive bytes buffered from current in-flight segment and then continue live.
ACL: read checked at subscription time.
On publisher disconnect, repo closes subscriber sockets. Mid-segment disconnect yields partial trailer data followed by EOF.
Completed segments remain available via 🖧GET and
🖧EXCHANGE.
Relay Chaining
A downstream repo can relay a live stream by:
- subscribing upstream with
🖧STREAM_OUT - publishing downstream with
🖧STREAM_IN
.H3names the active format extension: BLAKE3-256 hashing, HSB3 signatures, UTF-8 NFC normalization (Unicode 17.0.0), and this packet structure.↩︎