# RFC-3 — CivicIPFS Civic Network Protocol **Status:** Draft **Version:** 0.2 **Depends on:** RFC-2 (CivicIPFS Orchestrator Core) v0.3 **Changes from 0.1:** Dependency version updated. No protocol changes — crypto facts were always correct for orchestrator-to-orchestrator communication (all Ed25519, orchestrator own keys). **Author:** CivicIPFS Project **Repository:** TBD **Note:** WireGuard transport and Duniter attestation are deferred to a future RFC as optional extensions to this protocol. --- ## 1. Purpose and Scope This document specifies how CivicIPFS orchestrators communicate with each other — the Civic Network Protocol. RFC-3 covers: - The inter-orchestrator API endpoints - Peer discovery and connection verification - The Challenger component — challenge issuance and proof response - Byte-range selection strategy - What the challenge mechanism proves and does not prove - The ReputationManager component — scoring, updates, propagation - The SQLite tables that support this protocol - The `network:` configuration section that belongs in RFC-2's configuration file - Deferred items and open questions specific to this layer This document does not specify the Subscription API, Kubo integration, receipts, service offers, or operational requirements. Those are RFC-2. --- ## 2. Design Principles for the Network Layer These extend the RFC-2 design principles with constraints specific to a small, trust-bounded peer network. ### 2.1 No central registry Orchestrators find each other through operator-configured peer lists. There is no discovery service, no bootstrap node, no DNS seed. This is the same design decision as the addon's manual orchestrator configuration — permanent, not deferred. A registry reintroduces a central point of failure and a governance dependency the architecture is designed to avoid. ### 2.2 Signatures on everything Every message that crosses an orchestrator boundary carries an Ed25519 signature from the sending orchestrator's key (established in RFC-2 section 10). A message without a valid signature is dropped silently. There is no exception. ### 2.3 One hop for reputation Reputation information propagates exactly one hop. An orchestrator sends reputation updates only for challenges it issued itself. It does not re-propagate updates received from peers. This is a hard rule, not a configuration option. It bounds the attack surface for reputation manipulation and eliminates amplification. ### 2.4 Possession, not promise The challenge-proof mechanism proves that an orchestrator holds specific content at a specific moment. It proves nothing about the future and nothing about the past beyond the moment of proof. The design accepts this limitation and documents it clearly rather than overclaiming. --- ## 3. Architecture Position ``` Orchestrator A Orchestrator B ┌─────────────────────┐ ┌─────────────────────┐ │ │ │ │ │ Challenger ─────┼──POST /civic───▶ Challenger │ │ ReputationMgr ─────┼──POST /civic───▶ ReputationMgr │ │ │ │ │ │ Civic Network API │◀───────────────┼─ Civic Network API │ │ port 8422 │ │ port 8422 │ │ IPv4 + IPv6 │ │ IPv4 + IPv6 │ └─────────────────────┘ └─────────────────────┘ ``` Both ports bind dual-stack (IPv4 and IPv6) as specified in RFC-2 section 6. The same dual-stack rationale applies here: small civic operators behind NAT benefit from IPv6 global routability without port forwarding. All traffic between orchestrators is over HTTPS. The Civic Network API is the only public-facing surface for inter-orchestrator communication. --- ## 4. Peer Discovery and Connection ### 4.1 Peer configuration Peers are configured statically in the orchestrator's YAML configuration file under the `network.peers` key: ```yaml network: peers: - url: https://orchestrator.example.org - url: https://civic.another-org.net - url: https://[2001:db8::1]:8422 # IPv6 explicit ``` An operator adds a peer by editing this file and reloading the orchestrator. There is no in-protocol peer addition. This is the static IP model: deliberate, operator-controlled, permanent. ### 4.2 Startup verification At startup, and on a configurable periodic schedule (default every 6 hours), the orchestrator attempts to connect to each configured peer: 1. Fetch `GET /civic/v1/info` from the peer URL 2. Verify the response signature against the peer's declared pubkey 3. Check that `accepted_realms` includes at least one realm in common with this orchestrator — specifically `CIVICUS_IPFS` 4. Record the peer in the `peers` table with its verified pubkey and `last_seen_at` timestamp A peer that fails any check is logged with the specific failure reason and excluded from the challenge schedule until the next verification attempt. It is not removed from the configuration — the operator decides whether to remove it. ### 4.3 Pubkey pinning Once a peer's public key has been verified and stored in the `peers` table, subsequent connections compare the presented key against the stored one. A key change is not automatically accepted — it is logged as a warning and the operator must manually clear the stored key before the new one is accepted. This is deliberate: public key changes for established peers are operationally rare and warrant human attention. An unexpected key change is more likely to indicate a problem than a routine rotation. --- ## 5. The Civic Network API Endpoints These endpoints are served on port 8422, bound dual-stack. All POST endpoints require a valid Ed25519 signature in the request header — same convention as the Subscription API: ``` X-CivicIPFS-Orchestrator: {orchestrator_id} X-CivicIPFS-Sig: {Ed25519 signature of request body} ``` GET endpoints require no authentication — they return public information. ``` GET /civic/v1/info Returns orchestrator identity and capabilities. No authentication required. Response: { "orchestrator_id": "...", "version": "0.1", "pubkey": "...", "accepted_realms": ["CIVICUS_IPFS"], "reputation_score": float | null, "challenge_participation": "active" | "passive" | "none" } GET /civic/v1/pins Returns the list of CIDs this orchestrator has committed to pin. No authentication required — commitments are public by design. Paginated: ?page={n}&per_page={n, max 1000} Response: { "orchestrator_id": "...", "page": int, "total": int, "cids": [ { "cid": "baf...", "pinned_at": unix_ts, "expires_at": unix_ts | null } ] } POST /civic/v1/challenge Body: signed ChallengeRequest object (see section 6.1) Response: signed ProofResponse or signed FailureAck (see section 6.2) Authentication: required POST /civic/v1/reputation Body: signed ReputationUpdate object (see section 7.2) Authentication: required Response: { "recorded": true } or { "rejected": true, "reason": "..." } ``` --- ## 6. The Challenger Component The Challenger implements both sides of the challenge-proof exchange. It is the accountability layer of the civic network. ### 6.1 The challenge mechanism A CID is a cryptographic hash of content. An orchestrator that claims to pin a CID must be able to produce the content on demand. The Challenger exploits this property systematically and on a schedule. **Challenge issuance — what the challenger sends:** ```json { "schema": 1, "challenge_id": "urn:uuid:550e8400-e29b-41d4-a716-446655440000", "challenger_id": "12D3KooW...", "target_id": "12D3KooW...", "cid": "bafybeig...", "byte_offset": 3891204, "byte_length": 16384, "issued_at": 1700000000, "respond_by": 1700003600, "sig": "..." } ``` **Proof response — what the challenged orchestrator returns:** ```json { "schema": 1, "challenge_id": "urn:uuid:550e8400-e29b-41d4-a716-446655440000", "responder_id": "12D3KooW...", "cid": "bafybeig...", "byte_offset": 3891204, "byte_length": 16384, "bytes_b64": "...", "responded_at": 1700001200, "sig": "..." } ``` The `bytes_b64` field is the base64-encoded raw bytes from `/api/v0/cat?arg={cid}&offset={offset}&length={length}` on the local Kubo node. **Failure acknowledgement** — returned when the CID is not locally available or the byte range cannot be served: ```json { "schema": 1, "challenge_id": "urn:uuid:...", "responder_id": "...", "failure": "not_pinned" | "fetch_error" | "timeout", "responded_at": 1700001200, "sig": "..." } ``` A failure acknowledgement is still signed. An unsigned failure response is treated identically to no response — recorded as a timeout. ### 6.2 Verification The challenger verifies a proof response by: 1. Checking the `challenge_id` matches the issued challenge 2. Verifying the Ed25519 signature against the responder's stored public key 3. Fetching the same byte range independently: - First choice: from its own Kubo node if it pins the CID - Second choice: from `/api/v0/cat` against any reachable IPFS gateway with the same range parameters 4. Comparing the bytes byte-for-byte A proof passes if and only if the bytes match and the signature is valid. A proof fails if the bytes do not match, the signature is invalid, or the response does not arrive before `respond_by`. ### 6.3 Byte-range selection strategy The challenger selects `byte_offset` and `byte_length` using the following strategy, in priority order: **1. Target the final quarter of the file.** Content is more likely to be cached near the start. The final quarter is less likely to be in Kubo's in-memory block cache, making a positive response a stronger proof of full possession rather than partial availability. For a file of known size `S`, the offset is chosen uniformly at random from `[S * 0.75, S - byte_length]`. **2. File size is unknown — use a fixed deep offset.** If the file size cannot be determined before issuing the challenge (it may require fetching the DAG root), the offset defaults to `max(0, dag_size_estimate - 65536)`. If no size estimate is available, a random offset from `[1MB, ∞)` is used, capped to avoid overshooting. A challenge that overshoots the file returns a shorter byte range — this is acceptable and handled by the verifier. **3. `byte_length` is fixed at `16384` (16 KB).** This is sufficient to make fabrication impractical while keeping challenge traffic small. It is not configurable per-challenge — the value is a protocol constant in this version. Future versions may negotiate this value during peer handshake. This strategy is robust without paranoia. It does not require cryptographic randomness for the offset — standard PRNG is sufficient because the goal is unpredictability to the challenged party, not security against a cryptographic adversary. The CID hash itself already provides the binding between content and commitment. ### 6.4 What the challenge mechanism proves and does not prove **Proves:** The responding orchestrator holds the specific byte range requested, at the time the proof was generated. Combined with the CID hash guarantee, this proves possession of that portion of the content. **Does not prove:** - That the content will remain available after the proof - That the content was available before the challenge - That the content is stored with any redundancy - That the content is available from the IPFS network generally (the orchestrator may hold it only locally, not yet announced) These limitations are known, accepted, and documented here rather than obscured. The value of the mechanism is in its cumulative effect over time: an orchestrator that consistently passes challenges across many CIDs over many months has, by definition, held those CIDs through many independent spot-checks. The pattern of consistent passes is the evidence of reliability — not any single proof. An orchestrator that consistently fails challenges for committed CIDs is demonstrably not honouring its commitments. The reputation system makes this visible to the network. ### 6.5 Challenge schedule Each orchestrator challenges its configured peers on a schedule: - **Interval:** once per peer per 24 hours (default; configurable) - **Selection:** one CID chosen uniformly at random from the peer's published pin list at `/civic/v1/pins` - **Timing:** staggered randomly within the interval window to avoid synchronised load spikes across the network An operator may increase challenge frequency as evidence of reliability commitment — more frequent passes over time build a stronger reputation signal. An operator may not reduce frequency below once per 7 days while participating in the challenge network. Below that threshold the reputation data becomes too sparse to be meaningful. --- ## 7. The ReputationManager Component The reputation system is a collection of signed statements about observed behaviour, propagated exactly one hop across the peer network. It is not a blockchain, not a vote, not a consensus mechanism. ### 7.1 Reputation score Each orchestrator maintains a local reputation score for each known peer. The score is a float between 0.0 and 1.0: ``` score = challenges_passed / (challenges_passed + challenges_failed) ``` Only outcomes from the last 90 days are counted. An orchestrator with no challenge history in the last 90 days has a score of `null` — unknown, not zero. Unknown is not the same as bad. Timeouts count as failures. A `not_pinned` failure acknowledgement for a CID that appears in the peer's published pin list counts as a failure. A `not_pinned` acknowledgement for a CID not in the pin list is not counted — it was an erroneous challenge and the challenger's own error. ### 7.2 Reputation updates After each challenge outcome, the challenger signs a ReputationUpdate and sends it to all configured peers via `POST /civic/v1/reputation`: ```json { "schema": 1, "issuer_id": "12D3KooW...", "subject_id": "12D3KooW...", "challenge_id": "urn:uuid:...", "outcome": "pass" | "fail" | "timeout", "cid": "bafybeig...", "observed_at": 1700001200, "sig": "..." } ``` ### 7.3 Propagation rules These rules are not configurable. They are protocol-level constraints that apply to all participating orchestrators. **Rule 1 — Issue only what you observed.** An orchestrator sends reputation updates only for challenges it issued. It never sends updates about challenges it received or heard about from peers. **Rule 2 — Do not forward.** An orchestrator that receives a reputation update records it locally. It does not forward it to any other orchestrator. Reputation propagates exactly one hop. **Rule 3 — Verify before recording.** A received reputation update is accepted only if: - The `issuer_id` is a known peer with a verified public key - The Ed25519 signature is valid - The `challenge_id` is not already recorded from this issuer (deduplication — prevents replay) **Rule 4 — Rate limit.** An orchestrator accepts at most 10 reputation updates per source peer per 24-hour period. Updates beyond this limit are dropped with a `rejected: rate_limited` response. The rate limit applies per `issuer_id`, not per `subject_id`. **Why these rules?** One-hop propagation means a coordinated reputation attack requires compromising direct peers — it cannot be amplified through the network. Rate limiting means even a compromised peer cannot flood the reputation table. Signature verification means updates cannot be forged. Deduplication means replayed updates have no effect. The result is a reputation system that is resistant to the most likely attack vectors for a small civic network without requiring a consensus mechanism or a trusted third party. ### 7.4 Reputation display An orchestrator's own reputation score — as seen by the network — is computed from the updates its peers have sent about it: ``` own_score = weighted_average( [update.outcome for update in received_updates where update.subject_id == self.orchestrator_id and update.observed_at > (now - 90 days)], weight = 1 / (now - update.observed_at + 1) ) ``` More recent updates carry slightly more weight. An orchestrator with no received updates has an own_score of `null`. This is returned in `/api/v1/reputation` (RFC-2) and `/civic/v1/info`. An orchestrator with zero configured peers has no external reputation. This is expected for new deployments. It is surfaced honestly to subscribers as `null`, not as a zero score. --- ## 8. State Storage — Network Tables These tables are created in the same SQLite database file as the RFC-2 core tables. Schema migrations are versioned and run at startup by the orchestrator binary. ```sql -- Configured and verified peers CREATE TABLE IF NOT EXISTS peers ( peer_id TEXT PRIMARY KEY, peer_url TEXT NOT NULL, pubkey TEXT NOT NULL, accepted_realms TEXT NOT NULL, first_seen_at INTEGER NOT NULL, last_seen_at INTEGER, last_status TEXT NOT NULL DEFAULT 'unknown', active INTEGER NOT NULL DEFAULT 1 ); -- Challenges this orchestrator issued CREATE TABLE IF NOT EXISTS challenges_issued ( challenge_id TEXT PRIMARY KEY, target_id TEXT NOT NULL, cid TEXT NOT NULL, byte_offset INTEGER NOT NULL, byte_length INTEGER NOT NULL, issued_at INTEGER NOT NULL, respond_by INTEGER NOT NULL, outcome TEXT, resolved_at INTEGER ); CREATE INDEX IF NOT EXISTS challenges_issued_pending ON challenges_issued(respond_by) WHERE outcome IS NULL; -- Challenges this orchestrator received and responded to CREATE TABLE IF NOT EXISTS challenges_received ( challenge_id TEXT PRIMARY KEY, challenger_id TEXT NOT NULL, cid TEXT NOT NULL, byte_offset INTEGER NOT NULL, byte_length INTEGER NOT NULL, received_at INTEGER NOT NULL, responded_at INTEGER, outcome TEXT ); -- Reputation updates received from peers about other orchestrators CREATE TABLE IF NOT EXISTS reputation_received ( challenge_id TEXT NOT NULL, issuer_id TEXT NOT NULL, subject_id TEXT NOT NULL, outcome TEXT NOT NULL, cid TEXT NOT NULL, observed_at INTEGER NOT NULL, received_at INTEGER NOT NULL, PRIMARY KEY (challenge_id, issuer_id) ); CREATE INDEX IF NOT EXISTS reputation_subject_recent ON reputation_received(subject_id, observed_at); ``` --- ## 9. Configuration — Network Section This is the `network:` key that belongs in the RFC-2 configuration file. It is specified here because the parameters belong to this protocol. ```yaml network: # Civic Network API port — inter-orchestrator only # Bound dual-stack (IPv4 + IPv6) port: 8422 # Manually configured peer orchestrators # This list is permanent and operator-managed. # There is no automatic peer discovery. peers: - url: https://orchestrator.example.org - url: https://civic.another-org.net # How often to re-verify peer connectivity (hours) peer_verify_interval_hours: 6 challenge: # How often to challenge each peer (hours) # Minimum: 168 (once per 7 days) # Default: 24 (once per day) interval_hours: 24 # How long to wait for a proof response before recording timeout timeout_seconds: 3600 # Fixed byte range length for all challenges (bytes) # Protocol constant in this version — do not change byte_length: 16384 # Fraction of file from which to draw the offset # 0.75 = final quarter of file offset_fraction: 0.75 reputation: # Days of challenge history to include in score history_days: 90 # Maximum reputation updates accepted per peer per day max_updates_per_peer_per_day: 10 ``` --- ## 10. Go Interface for the Network Layer The network components expose internal Go interfaces that are testable without network access. ```go // Challenger is implemented by the civic network challenger. type Challenger interface { IssueChallenge(ctx context.Context, peer Peer, cid string) (*ChallengeResult, error) RespondToChallenge(ctx context.Context, req ChallengeRequest) (*ProofResponse, error) } // ReputationManager tracks and propagates reputation. type ReputationManager interface { RecordOutcome(challengeID, subjectID, outcome string, observedAt int64) error RecordReceived(update ReputationUpdate) error ScoreFor(orchestratorID string) *float64 // nil = unknown OwnScore() *float64 // nil = no peers BroadcastUpdate(ctx context.Context, update ReputationUpdate) error } // PeerRegistry manages known peers. type PeerRegistry interface { Peers() []Peer Get(peerID string) (Peer, bool) Verify(ctx context.Context, peerURL string) (Peer, error) } ``` Production implementations are in the `civic` package. Test implementations are in `civic/mock`. This is the same isolation pattern as the `IPFSClient` interface in RFC-2 section 7. --- ## 11. Deferred Items **D1 — WireGuard transport.** Optional encrypted transport for established peer pairs, replacing HTTPS for those connections. Deferred to a future RFC alongside Duniter attestation. **D2 — Duniter attestation.** Anchoring CIDs to the Duniter blockchain for external, manipulation-resistant timestamping. Deferred to the same future RFC. **D3 — Challenge negotiation.** In this version, `byte_length` is a fixed protocol constant (16384). A future version may negotiate this value per peer during the `/civic/v1/info` handshake. Deferred. **D4 — Cross-realm challenges.** Currently, an orchestrator only challenges peers that share at least one realm in common. Whether challenges across realm boundaries are meaningful or desirable is an open question. Deferred. **D5 — Reputation bootstrapping.** A new orchestrator has `null` reputation and no peers. How a new operator establishes initial peer relationships and builds reputation from zero is an operational question without a specified answer in this version. Deferred to an operational guide. --- ## 12. What a Future RFC Will Specify A future RFC (currently unnumbered) will cover optional extensions to the Civic Network Protocol: - WireGuard transport configuration for peer connections - Duniter blockchain interface and attestation record format - How an attested receipt differs from an unattested receipt - Integration of attestation timestamps into the challenge verification flow - Alternative attestation mechanisms considered --- *End of RFC-3 draft 0.1*