The interchange formats in use today were designed for fidelity and portability, not provenance. GPX (2002) carries coordinates and timestamps; TCX adds structured training data; FIT adds efficient binary encoding and a rich sensor model. All three assume a cooperative author. Each is trivially synthesised: writing a FIT file describing a 2:30 marathon requires no marathon.
This was adequate while activity data was a personal record. It is inadequate now that activity data carries consequence. The companion paper Proof of Physical Activity [1] establishes the structural reason a device cannot close this gap alone: hardware attestation authenticates computation, not reality, and the sensor interface is a trust boundary the enclave does not see across. Evidence that a human moved must therefore come, in part, from other humans.
What is missing is not a better sensor format. It is a container for evidence — one that travels with the activity, survives redaction, and can be checked by a party who trusts neither the athlete nor the platform.
| Goal | Consequence for the design |
|---|---|
| G1 Offline-verifiable | All evidence needed for a verdict is inside the file. No network call, no API key, no trust in the issuer. |
| G2 Selectively disclosable | Redacting the track must not invalidate the evidence. Proof of performance must be separable from disclosure of location. |
| G3 Complementary | Wraps FIT/GPX/TCX rather than replacing them. Adoption must not require abandoning existing tooling. |
| G4 Deterministic | Byte-identical re-serialisation, so the content address is stable across implementations. |
| G5 Extensible without forks | Unknown fields are preserved and ignored; new evidence types are additive. |
| G6 Privacy-preserving by default | Ephemeral identifiers, quantised encounters; no athlete becomes a trackable beacon by participating. |
must, must not, should, may are to be interpreted as in BCP 14 [2].
| activity root | The content address of a proof; the digest binding all other components. |
| ban | One unit of log10 likelihood odds. Evidence strength is denominated in bans [1]. |
| encounter | A mutually signed record of two athletes' physical co-presence at a quantised time and place. |
| MMR | Merkle Mountain Range: an append-only accumulator supporting efficient prefix roots (§5). |
| prefix root | The MMR root over track samples up to a given index — what an encounter commits to. |
| profile | A disclosure level: FULL, REDACTED or MINIMAL (§8). |
| subject | The athlete to whom the proof pertains, identified by public key. |
| verifier | Any party evaluating a proof. Not necessarily FlowStake. |
An .fsp file is a single deterministically encoded CBOR map [3] with integer keys. Integer keys keep the encoding compact and stable across localisations; a registry of assigned keys appears in §12.
FSP = {
0: version, ; uint — protocol version (this document: 1)
1: profile, ; uint — 0 FULL | 1 REDACTED | 2 MINIMAL
2: subject, ; map — athlete identity and device attestation (§4)
3: activity, ; map — sport, interval, aggregate metrics (§4.3)
4: track, ; map — MMR root, sample count, optional samples (§5)
5: encounters, ; array — signed encounter records (§6)
6: evidence, ; map — evidence terms and total in bans (§7)
7: endorsements, ; array — verifier signatures, on-chain anchors (§9)
8: source, ; map — wrapped FIT/GPX/TCX payload or digest (§10)
9: extensions ; map — namespaced, ignored-if-unknown (§12)
}
Keys 0–6 are mandatory in every profile. Key 7 is optional and carries third-party assertions. Keys 8–9 are optional.
subject = {
0: pubkey, ; bstr — Ed25519 public key, 32 bytes
1: key_attestation, ; map — platform key attestation (§4.2), optional
2: alias ; tstr — display handle, optional, never identity-bearing
}
The subject is identified by key, not by account. A proof therefore remains valid independent of the issuing platform's user database — a property that matters for G1: a verifier checks a signature, not a login.
key_attestation = {
0: platform, ; tstr — "apple.appattest" | "android.playintegrity" | "none"
1: assertion, ; bstr — platform-specific attestation blob
2: nonce, ; bstr — challenge the assertion was produced over
3: asserted_at ; uint — epoch seconds
}
Attestation establishes that the signing key resides in hardware and that the producing software was unmodified. Per [1] §4 this bounds an adversary's software capability but does not constrain the physical world; verifiers must not treat a valid attestation as evidence that an activity occurred. Its contribution is bounded by the solo ceiling of §7.3.
activity = {
0: sport, ; tstr — "running" | "cycling" | "walking" | "swimming" | ...
1: started_at, ; uint — epoch seconds
2: ended_at, ; uint
3: distance_m, ; uint
4: moving_s, ; uint
5: elevation_m, ; int, optional
6: metrics ; map, optional — sport-specific aggregates (cadence, power, HR)
}
Aggregates are retained in every profile, including MINIMAL. This is deliberate: the claim "21.1 km in 1:29:14" is the assertion being proven, and it must survive redaction of the coordinates that would otherwise disclose where.
Encounter evidence requires committing to the track so far at the moment two athletes meet ([1] Theorem 1). A conventional balanced Merkle tree cannot produce prefix roots without recomputation, and the prefix set is not known until the activity ends. A Merkle Mountain Range [4] — an append-only accumulator maintained as a list of perfect binary subtrees — supports appending a leaf in amortised O(1), and yields a root over the first n leaves at any point, which is exactly the prefix commitment required. It is therefore the structure this specification mandates.
track = {
0: mmr_root, ; bstr — 32 bytes, root over all samples
1: count, ; uint — number of samples committed
2: sample_hash, ; tstr — "sha256"
3: samples, ; array — omitted in MINIMAL; redacted in REDACTED (§8)
4: redactions ; array — [{from, to, reason}], present iff profile = REDACTED
}
sample = [ t, lat_e7, lon_e7, alt_cm, acc_cm, flags ] ; fixed-order array
leaf_i = SHA-256( 0x00 || canonical_cbor(sample_i) )
parent = SHA-256( 0x01 || left || right )
prefix_root(n) = bag_peaks( peaks of the MMR after n appends )
Domain separation bytes 0x00/0x01 prevent second-preimage attacks that conflate leaves with internal nodes [5]. Coordinates are integers in units of 10−7 degrees; floating-point encodings must not be used, since their canonicalisation is implementation-dependent and would break G4.
The evidentiary core. Full protocol semantics — radio exchange, binding modes, timing windows — are specified in the companion engineering document [6]; this section defines only the serialised artefact and its validation.
encounter = {
0: peer_pubkey, ; bstr — counterparty Ed25519 key
1: t_q, ; uint — epoch seconds, quantised to 30 s
2: geo, ; tstr — geohash, precision 7 (~76 × 152 m)
3: self_prefix, ; bstr — subject's MMR prefix root at t_q
4: peer_prefix, ; bstr — counterparty's prefix root at t_q
5: nonce, ; bstr — 16 bytes; 8 contributed by each party
6: medium, ; uint — 0 SERVER | 1 BLE | 2 UWB | 3 OPTICAL | 4 VIDEO
7: binding, ; uint — 0 DIRECT | 1 DEFERRED
8: self_sig, ; bstr — Ed25519 over canonical_cbor(keys 0..7)
9: peer_sig ; bstr
}
Note that (b) is checkable from the file alone even though the counterparty's track is absent: the subject's own prefix root is recomputable from the subject's own samples. The counterparty's peer_prefix is not checkable offline — it requires the counterparty's file — which is why cross-party graph consistency is an endorsement (§9), asserted by a party that has seen both.
evidence = {
0: params_id, ; tstr — identifier of the parameter set, e.g. "fs-v1"
1: params_digest, ; bstr — SHA-256 of the canonical parameter document
2: terms, ; map — { term_name: bans } (signed decimal, 4 dp)
3: total_bans, ; tstr — decimal string; sum of terms after caps
4: tier, ; tstr — "BRONZE"|"SILVER"|"GOLD"|"PLATINUM", optional
5: caps_applied ; array — names of caps that bound the total, e.g. ["solo_ceiling"]
}
Evidence is denominated in bans (log10 likelihood odds), which add for conditionally independent sources [1] §3. Posterior probability under a prior of even odds follows as P = 1 − 1/(1 + 10total). Decimal strings are used rather than CBOR floats so that total_bans is reproducible byte-for-byte across platforms.
A score is meaningless without the parameters that produced it. params_digest binds the proof to a specific, publishable parameter document. A verifier that does not recognise params_id must either fetch and check the document against its digest, or report the evidence as unevaluated — it must not silently substitute its own parameters, which would produce a different verdict under the same file.
The solo ceiling of [1] Corollary 1 is not an implementation detail; it is a claim about what the file can prove. Where device-local terms are bounded by it, the encoder must record "solo_ceiling" in caps_applied, so a verifier can distinguish a proof that reached its evidentiary limit from one that merely scored low.
The property that distinguishes this format from FIT: the evidence survives redaction of the track. Encounter signatures cover prefix roots, not coordinates; aggregates are retained independently. An athlete can therefore prove performance without disclosing route.
| Profile | Track samples | Encounter geo | Offline-verifiable claims |
|---|---|---|---|
| FULL (0) | all | disclosed | everything, including prefix recomputation (§6b) |
| REDACTED (1) | privacy zones and endpoints removed; redactions lists the excised ranges | disclosed | all except prefix checks inside redacted ranges |
| MINIMAL (2) | none — root and count only | unchanged — signed, immutable | signatures, aggregates, evidence total, tier |
Redaction must not modify encounter records in any way — including coarsening geo. Every encounter field is covered by both parties' signatures and contributes to encounters_root; mutating one post hoc invalidates the signatures, changes activity_root, and destroys precisely the evidence redaction is meant to preserve. Coarser encounter geography is a protocol parameter — sign a shorter geohash at exchange time — not a redaction step. (This corrects an error in an earlier draft, caught by the reference implementation's root-invariance test.)
A verifier encountering a REDACTED or MINIMAL profile must skip prefix recomputation for unavailable ranges and must report those checks as not performed rather than as passed.
The last clause matters more than it appears. A verifier that reports unperformed checks as passed converts a privacy feature into a forgery vector; the distinction between verified and not checked must reach the consumer of the verdict.
activity_root = SHA-256( 0x02 ||
canonical_cbor([ version, subject_digest, activity_digest,
mmr_root, encounters_root, evidence_digest ]) )
encounters_root = MMR root over SHA-256(canonical_cbor(encounter_i)), i ascending
The activity root is the content address: a stable 32-byte identifier for the proof, independent of profile — since redaction alters neither mmr_root nor the encounter set.
endorsement = {
0: kind, ; uint — 0 VERIFIER | 1 GRAPH_CONSISTENCY | 2 CHIP_TIMED | 3 ONCHAIN
1: issuer, ; bstr — issuer public key or contract address
2: statement, ; map — kind-specific assertion
3: issued_at, ; uint
4: sig ; bstr — over canonical_cbor([activity_root, kind, issuer, statement, issued_at])
}
Endorsements carry claims that cannot be checked from the file alone: GRAPH_CONSISTENCY (a party has seen both sides of the encounters and found no contradiction), CHIP_TIMED (an official timing system corroborates the result), ONCHAIN (the root is anchored). They are optional and additive: a verifier that trusts no issuer still obtains a verdict from keys 0–6, merely a weaker one. Trust is therefore opt-in and explicit, which is the property that lets the format outlive any particular issuer.
For settlement, activity_root is signed as an EIP-712 typed message and consumed by a contract that never sees the underlying track:
Settlement(bytes32 activityRoot, address athlete, uint256 totalBansE4,
uint64 notAfter, uint256 challengeId)
totalBansE4 is the evidence total scaled by 104, allowing a contract to require, say, a Platinum threshold (≥ 40000) without floating-point arithmetic. A settlement contract thus enforces evidentiary quality on-chain while holding no personal data — the separation of verification from value described in [1] §12.
An .fsp should wrap rather than replace the athlete's existing file:
source = {
0: format, ; tstr — "fit" | "gpx" | "tcx"
1: digest, ; bstr — SHA-256 of the original bytes
2: embedded ; bstr — the original bytes, optional
}
This yields a migration path that costs adopters nothing: devices continue to emit FIT, platforms continue to parse it, and the proof travels alongside. Conversion from .fsp to GPX is lossy but always possible; conversion from GPX to a meaningful .fsp is not possible at all, because the evidence does not exist in the source. That asymmetry is the point of the format.
The verdict must be reported as a total in bans together with the list of checks not performed. A bare tier label must not be presented without that list, since a Platinum tier computed with four checks skipped is not a Platinum proof.
Version 1 assigns container keys 0–9; 10–31 are reserved for future core use, and 32+ are available to registrations. Extensions are namespaced:
extensions = { "com.example.hrv": { ... }, "org.itra.index": { ... } }
Decoders must preserve unknown keys when re-serialising, so that a proof passing through an intermediate tool retains its content address. Adding an evidence term is a minor change (unknown terms contribute zero and are listed as not evaluated); changing a digest construction, the canonical encoding, or the semantics of an existing key is a major change requiring a version bump.
| Threat | Mitigation | Residual |
|---|---|---|
| Track edited after the fact | Encounter prefix bindings (§6b) | Ranges after the last encounter remain editable |
| Fabricated encounters | Two signatures; independence weighting [1] §7 | Genuine collusion between real athletes |
| Sybil counterparties | Hardware-attested keys raise identity cost | Attested devices can still be bought |
| Replay of a whole proof | Nonces; notAfter in settlement | Verifiers must track spent roots |
| Redaction abused to hide contradictions | Unperformed checks reported explicitly (§8) | Consumers may ignore the report |
| Parameter substitution | params_digest binding (§7.2) | Requires published parameter documents |
| Key compromise | Non-extractable hardware keys; revocation via endorsement | Proofs issued before revocation stay valid |
| Cryptographic obsolescence | Algorithm agility via sample_hash; version bump for suite changes | Long-lived proofs may need re-anchoring |
Two properties are worth stating plainly. First, a proof is evidence, not a verdict: it carries a likelihood ratio, and the decision threshold belongs to the verifier, who alone knows what is at stake. Second, absence of evidence is not evidence of fabrication — a solo run in a sparse network scores low because nobody was there, not because it did not happen. Consumers that conflate the two will exclude honest athletes in low-density regions, which is an equity failure as much as a technical one.
An API makes consumers dependent on the issuer's availability, pricing and continued existence. A format does not. GPX has outlived its authoring company by two decades and is read by software that has never contacted Topografix; that durability is a property of the artefact, not the organisation.
The strategic consequence is that verification and value can separate cleanly. FlowStake issues proofs and operates the network that makes strong proofs possible; settlement contracts, insurers, race organisers and ranking systems consume them without asking permission and without trusting the issuer — because §11 lets them check for themselves. Adoption of a verifiable format is not a partnership; it is a decision by the consumer alone. That is the property this specification exists to create.