iris

Iris Protocol Architecture

This document defines the architecture of the Iris Protocol, a proprietary, standalone Decentralized Terrestrial Satellite Oracle Network (DtsON). Iris enables requesters (decentralized applications or dapps) to reliably ingest, verify, and utilize Geographical Information System (GIS) data, such as satellite imagery.

Contents

  1. System Overview: The Chain of Provenance
  2. The Geodesic Reconstruction Model
  3. State Machine Architecture
  4. Network Layer
  5. Data Provenance & Ingestion
  6. Data Normalization Engine
  7. Consensus Engine (Iris-BFT)
  8. Smart Contract Integration
  9. How the Layers Connect — A Full Request Walkthrough
  10. Versioning & Upgrade Strategy
  11. System Complexity and Compute Requirements
  12. Observability & Monitoring
  13. End-to-End Fault Tolerance
  14. Protocol Parameters

Related documents

DocumentRelationship
GlossaryCanonical project-specific dictionary — actors, state layers, round phases, runtime components, protocol messages, cryptographic primitives, network protocols, TEE vendors, smart contracts, and cross-cutting concepts. First place to look when a term feels load-bearing in any doc or crate, and first place to update when new vocabulary lands
WhitepaperHigh-level vision and motivation. This architecture doc is the technical realization of the whitepaper's goals
Data Normalization SpecificationComplete mathematical formulation of the tensor metrics summarized in §6
Implementation Plan — Demo / — MVPPhased engineering breakdown that maps directly to the layers defined in §3. Demo covers Phases 0 → 4.5; MVP covers Phase 5+
Protocol ParametersVersioning model + current parameter set + allowed-override ranges + per-version history (formerly architecture.md §14)
Workspace Crate MapPer-crate guide to the Cargo workspace — what each iris-* crate is for, current implementation status, dependency graph, and how a round flows across crate boundaries
Threat ModelCompanion threat catalog to §1.3 Trust Model — per-layer attack analysis, mitigations, and slashing rules
Error Handling Strategies (archived)Resolved rationale record — the normative spec is now §8.8 Round Failure Handling (failure taxonomy, FailureReason enum, contributor credit schedule, fraud-proof slashing path) and the §7.4 wire-format input discipline. Archived doc retains the full four-strategy comparison
Versioning Strategies (archived)Resolved rationale record — the normative spec is now §10 Versioning & Upgrade Strategy, the §8.2 minProtocolVersion interface, and §8.3 epoch rotation. Archived doc retains the four versioning axes, the strategy comparison, and the OQ-1…OQ-6 rationale
GovernanceOff-chain governance framework that approves committee changes, version bumps, and emergency patches. Specifies the 67% threshold, epoch-anchored upgrade flow, dispute resolution, and the Iris Foundation / Iris Protocol LLC organizational split
TokenomicsConcrete starting values for every governance-tunable parameter — slash percentages, request fee formulas, persistence pricing, the reputation algorithm, treasury management, voting weights, and phase-by-phase parameter ramps. The architecture fixes mechanisms; tokenomics fixes defaults
Testing Strategies (archived)Resolved strategy record for how every probability bound (§7.4) and slashing path (§8.8) is empirically validated: five-tier test pyramid (Unit / Property / Integration / Simulation / Fork), iris-sim adversarial harness with 14-entry misbehavior catalog, and a private real-imagery adversarial corpus. The resolution summary lives in architecture_review.md §7; implementation detail (iris-sim API, fuzz/mutation procedures) is preserved in the archived doc

System Overview: The Chain of Provenance

The core design philosophy of the Iris Protocol is the Chain of Provenance. Unlike early oracle models that relied on trusting node operators to report data truthfully, Iris is architected to guarantee that the rich geospatial data it ingests remains mathematically provable and completely untampered with throughout the entirety of its journey—from the commercial satellite provider to the blockchain smart contract.

Every jump in the data's lifecycle is secured by a specific cryptographic protocol, creating an unbroken chain of trust. By strictly enforcing this provenance pipeline, Iris operates as a highly secure, Byzantine Fault Tolerant (BFT) consensus network that removes the need to trust the physical nodes themselves.

The Provenance Pipeline

flowchart LR
    Provider["Data Provider
(Maxar, Planet)"] Node["Regular Node
(iris-fetch enclave)"] Leader["Leader Node
(Aggregator)"] IPFS[("Decentralized
Storage")] Contract{"Smart Contract
(Blockchain)"} Requester(("Requester
(Dapp)")) Provider -- "TLS-in-TEE
Hardware Attestation" --> Node Node -- "Noise Protocol
(ed25519 Encryption)" --> Leader Leader -- "CID Hashing
(Content Addressing)" --> IPFS Leader -- "BLS Threshold Signature
(Committee Seal)" --> Contract IPFS -. "Fetch Panel Data" .-> Requester Contract -. "Verify CID & Signature" .-> Requester classDef external fill:#ffe0b2,stroke:#ff9800,stroke-width:2px; classDef iris fill:#e3f2fd,stroke:#2196f3,stroke-width:2px; classDef storage fill:#e8f5e9,stroke:#4caf50,stroke-width:2px; classDef onchain fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px; class Provider external; class Node,Leader iris; class IPFS storage; class Contract,Requester onchain;

System Actors

Entities in the Iris network participate in one or more of the following roles. This section defines what each actor does; §1.3 Trust Model defines what we trust them to do and what happens when that trust is violated.

Regular Node

An active committee member that forms the trust backbone of the Iris network. Regular Nodes independently observe satellite imagery, generate cryptographic provenance proofs, and provide BLS signature shares during consensus.

Leader Node

A Regular Node elected by a deterministic, per-round formula to serve as the round's aggregator and proposal authority. Leadership is a rotating duty — a Leader Node carries all responsibilities of a Regular Node plus the aggregation role.

Attestor (TEE Vendor PKI)

The hardware-rooted authority whose public-key infrastructure signs the attestation documents produced by iris-fetch enclaves. Each accepted vendor (AWS for Nitro Enclaves, Intel for SGX/TDX, AMD for SEV-SNP) is a distinct Attestor; the protocol is multi-vendor by design and never depends on any single one. The Attestor is not an Iris network participant — it is the off-network root of trust for the attestation_doc field of Manifest.

Relayer

An opt-in, reputation-gated transport role that bridges the host blockchain and the Iris network. The Relayer is an overlay responsibility that node operators with sufficiently high reputation scores may assume — it is distinct from, but built on top of, the Regular Node role.

Data Provider

External commercial satellite imagery providers (Maxar, Planet Labs, Sentinel, and similar) that serve the raw geospatial data consumed by Regular Nodes. Data Providers are not Iris network participants — they are external services whose outputs are made trustworthy by the provenance pipeline.

Requester

Decentralized applications (dapps) or smart contracts on a host blockchain that initiate the protocol by submitting DataRequest transactions and consume the resulting verified panels.

Trust Model

§1.2 defines the actors and their protocol roles; this section defines what we trust them to do. Iris is a Byzantine Fault Tolerant network — no single actor is assumed honest, but the protocol does rely on a small set of explicit trust assumptions to function. The table below enumerates, for each actor, what we rely on them to do, what we explicitly do not trust them with, and what happens when that trust is violated. The companion document threat_model.html catalogs the attacks against these assumptions and the corresponding mitigations.

ActorTrusted to doNot trusted to doConsequence of misbehaviorDeep-dive
RequesterPay the request fee; provide a valid AoI / timestamp pair on-chainInfluence consensus, leader election, or panel content beyond the request parametersRequest stalls or is rejected on-chain; no slashing (Requester holds no stake)§1.2
Data ProviderServe imagery over TLS using a stable hostname, with a valid TLS certificate chainBe honest about pixel content — only the TLS-attested fact of delivery is trusted; pixel-level lies are filtered by the consensus medoidNone directly (Provider is external); systematically anomalous Providers are excluded from future round queries by operators§5
Attestor (TEE Vendor PKI)Honor the published attestation-PKI semantics (sign only genuine platform measurements; honor revocation lists)Read plaintext (TLS terminates inside the enclave); forge attestations for code that did not run; alter or fabricate response payloadsRemoval from the on-chain accepted-attestor whitelist; cross-vendor-quorum rule (Phase 3) bounds blast radius of a single-vendor compromise§5.5
Regular NodeHonestly observe imagery from inside an iris-fetch enclave with a whitelisted measurement; produce a valid attestation document; submit a Manifest; cast a BLSShare on accepted proposalsForge consensus alone; equivocate; submit data anomalous to the medoid; tamper with bytes between the enclave and the local cacheStake slashed for invalid attestation, anomalous data, or attributable DKG misbehavior§3.4, the threat model §Slashing
Leader NodeAggregate manifests; run the normalization pipeline; propose the medoid (Average Scenario); aggregate t valid BLS sharesForge consensus alone (each Regular Node independently re-verifies the proposal); withhold a successful proposal indefinitelyRound fails (no proposal advances → re-elect or timeout); attributable misbehavior is slashable§7.1§7.4
RelayerTransport DataRequest events on-chain → off-chain and finalized panels off-chain → on-chainForge consensus; alter the CID or signature payload; fabricate a DataRequestLost relay-fee revenue; reputation decay; underlying stake slashable for attributable censorship (proposed — see the threat model §Slashing)§8.7
Committee (collective)Maintain BFT honest majority f < n/3; produce valid t-of-n threshold signatures; commit the reputation Merkle rootBe individually honest — only the collective threshold is assumed; any individual member may be ByzantineNetwork-wide failure mode if violated: f ≥ n/3 enables safety violations (forged panels, equivocation); detected after the fact via on-chain signature verification§3.4
Iris FoundationOperate the Phase 1 Relayer (§8.7); steward the on-chain [attestation] whitelist (accepted vendor roots + accepted enclave measurements per §5.5); steward governance until decentralization completesOverride consensus; hold BLS key shares; modify finalized on-chain panelsReputational damage and governance pressure; phase roadmaps progressively reduce dependency to zero§5.5, §8.7
Trust state shown reflects the current Phase 1 / MVP. The Attestor, Relayer, and Foundation rows all evolve under phased decentralization roadmaps that monotonically weaken their trust requirements over time. See §5.5 (Attestation) and §8.7 (Relayer) for the complete phase progressions.

Aggregate Trust Assumptions

A handful of cross-cutting invariants underpin the per-actor trust grants above. These are network-wide assumptions, not properties of any single actor:

Threat Analysis: The trust assumptions above are not self-enforcing — each one is a target. Sybil and eclipse attacks try to undermine the GossipSub mesh and Kademlia DHT; a compromised TEE platform could mint a forged attestation (absorbed by the BFT threshold and Phase 3 cross-vendor-quorum rule); a Byzantine Leader can equivocate or propose a sub-optimal medoid; the DKG ceremony can be sabotaged; a censoring Relayer can stall liveness even though it cannot break safety. See threat_model.html for the full per-layer attack catalog, mitigation analysis, and slashing rules.

Non-Goals & Scope Boundaries

To prevent misinterpretation of the protocol's capabilities, the following are explicitly out of scope for the Iris architecture as specified in this document:

The Geodesic Reconstruction Model

Before diving into subsystems it helps to have a mental model for what the state machine is actually building.

The Analogy: A Sphere of Flat Panels

Imagine the Earth as a geodesic sphere — not a smooth continuous surface, but a polyhedron assembled from many discrete flat panels (like a Buckminster Fuller dome projected around the entire globe). Each panel covers a bounded patch of the planet's surface — an Area of Interest (AoI) — defined by a bounding box and a point in time.

When a Requester requests satellite data for a specific location, the Iris network is essentially being asked to reconstruct one panel of this sphere. The reconstruction pipeline works as follows:

  1. Multiple Regular Nodes independently photograph the same panel by fetching satellite imagery from different Data Providers (Maxar, Planet, Sentinel, etc.). Each photograph is a slightly different perspective of the same physical surface — different viewing angles, different spectral sensitivities, different times of day.
  2. The Data Normalization Engine aligns all photographs into a shared coordinate space via orthorectification, then computes pairwise Similarity Scores (𝒮) to measure how close each photograph is to every other.
  3. The Average Scenario is selected — the single photograph that is mathematically most similar to the consensus pool. This image becomes the panel's canonical reconstruction: the best available estimate of what that patch of Earth actually looks like.
  4. The panel is finalized when the committee threshold-signs the reconstruction's IPFS CID and delivers it on-chain.

Over time, as Requesters request data for different locations and timestamps, the network accumulates a growing mosaic of verified panels — an ever-expanding geodesic reconstruction of the Earth's surface, each facet independently verified by decentralized consensus.

Why This Analogy Matters

The geodesic model clarifies several architectural decisions:

Protocol Constraints & Considerations

The geodesic model assumes panels are photographable in the first place. That assumption is bounded by orbital geometry, sensor optics, and the math the network is willing to execute. The single most load-bearing parameter is the Effective Nadir Angle — Iris's calculated cap on how far off-nadir an observation may be before a manifest is rejected. It is a protocol parameter, not an industry constant, and it trades reconstruction fidelity against satellite eligibility.

State Machine Architecture

The Iris state machine operates at four nested layers, each tracking different aspects of the system. Understanding these layers is essential to understanding what the node software is actually doing at any given moment.

flowchart TD
    subgraph L4 [" "]
        C["Layer 4: Committee State (Network Lifecycle)

Tracks: Active Nodes, DKG, Aggregate PubKey"]:::content subgraph L3 [" "] P["Layer 3: Panel State (Global Persistent State)

Tracks: Finalized IPFS CIDs, On-chain Reports"]:::content subgraph L2 [" "] N["Layer 2: Node State (Local Persistent State)

Tracks: Local Cache, Keys, Active Rounds Map"]:::content subgraph L1 [" "] R["Layer 1: Round State (Per-Request)

Tracks: FSM Phases (Observing → Voting → Commit)"]:::content end end end end classDef content fill:#fff,stroke:#666,stroke-width:1px,color:#000; classDef l4 fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px; classDef l3 fill:#fff3e0,stroke:#ff9800,stroke-width:2px; classDef l2 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px; classDef l1 fill:#e3f2fd,stroke:#2196f3,stroke-width:2px; class L4 l4; class L3 l3; class L2 l2; class L1 l1;

Layer 1 — Round State (per-request lifecycle)

The innermost and most active layer. Every incoming DataRequest event from a host blockchain spawns a new Round. A round is the atomic unit of work in Iris: it begins with a request and ends with either a finalized panel (success) or a structured failure surfaced on-chain via reportFailure (see §8.8).

Topic version suffixes. The vN suffix on every GossipSub topic in this section (iris/requests/v1, iris/observations/v1, iris/consensus/v1) tracks the message-schema axis defined in versioning_strategies.md §1. It advances independently of the on-chain currentEpoch() and the libp2p transport ID, via the epoch-windowed upgrade strategy in §10. The suffix shown here is the current schema version; a future breaking change to a message in any of these topics activates vN+1 at an epoch boundary, with no overlap window.

The round progresses through a strict finite state machine. The success path is the central spine; every other terminal arrow lands in Failed, where the failure category is reported on-chain per §8.8.

stateDiagram-v2
    [*] --> Idle
    Idle --> Observing : Request received
    Observing --> Aggregating : Phase 2 deadline elapsed
≥t Manifests collected Observing --> Failed : Phase 1 commitments < t
(InsufficientCommitments)
OR Phase 2 manifests < t
(InsufficientManifests) Aggregating --> PreCommit : Proposal broadcast
(Leader pins to IPFS) Aggregating --> Failed : No Proposal within
proposal_deadline_seconds
(LeaderTimeout) PreCommit --> Voting : Nodes verify &
broadcast BLS shares Voting --> Commit : Threshold signatures
aggregated (t > 2n/3) Voting --> Failed : Voting timeout
(InsufficientSignatures) Commit --> [*] : Report delivered on-chain Failed --> [*] : reportFailure called
(see §8.8)

State Definitions

StateWho is activeWhat is being trackedExit condition
IdleAll nodesSubscription to iris/requests/v1. The node is listening for the next DataRequest event from the blockchain Relayer (which bridges the on-chain request to the off-chain network) or GossipSub.A valid DataRequest is received and the node determines the Leader Node for this round via the election algorithm.
ObservingAll Regular NodesTwo-phase deadline (see §7.2). Phase 1 (60s): each node spawns its iris-fetch TEE enclave, the enclave completes the TLS handshake with its Data Provider, and the node publishes a TlsCommitment to iris/observations/v1. Nodes that miss this deadline are excluded immediately. Phase 2 (300s from round start): each committed node completes the full in-enclave payload download, the enclave returns the hardware-attested document, the node computes BLAKE2b(payload) (matching the in-enclave hash) and attestation_hash = BLAKE2b(attestation_doc_bytes), and publishes a final Manifest containing {request_id, image_hash, bounding_box, timestamp, attestation_hash, mission, processing_level, radiometric_units, node_signature} (the mission / processing_level / radiometric_units declaration triple is parsed in-enclave from the GeoTIFF metadata and bound into the attestation's user_data per §5.3 / §5.4 step 6; the registry in §6.1.1 / parameters.html §14.2.8 dispatches the normalization pipeline on the tuple). The node tracks: which providers it queried, TLS session state, the local file path of the cached GeoTIFF, the attestation document path, and its own manifest.The Phase 2 deadline elapses with ≥t valid Manifests collected; the Leader proceeds to Aggregating. If fewer than t nodes published a TlsCommitment at the Phase 1 deadline, the round transitions directly to Failed with InsufficientCommitments; if Phase 1 met threshold but fewer than t Manifests arrive by the Phase 2 deadline, the round transitions to Failed with InsufficientManifests (see §8.8).
AggregatingLeader Node onlyThe Leader Node collects all manifests from iris/observations/v1. For each manifest, the Leader Node requests the full GeoTIFF payload via a direct libp2p stream (the Bitswap-like transfer protocol). Once all payloads are retrieved, the Leader Node runs the full normalization pipeline: orthorectification → similarity metrics (μ₁, μ₂, μ₃) → exponential decay scoring 𝒮(μ) → pairwise similarity matrix → Average Scenario selection. The Leader Node tracks: the similarity matrix, the selected Average Scenario tensor, and its corresponding image hash.The Leader Node has computed the Average Scenario, pinned it to IPFS, obtaining a CID, and broadcast a Proposal. If no Proposal is broadcast within proposal_deadline_seconds (default 240s), any observing node can mark the round Failed with LeaderTimeout (see §8.8).
Pre-CommitLeader Node broadcasts, all Regular Nodes listenThe Leader Node publishes a Proposal message to iris/consensus/v1 containing: {request_id, selected_image_hash, selected_index, ipfs_cid, contributors, similarity_matrix, leader_signature}. The full pairwise similarity matrix is included so every node can deterministically verify the medoid pick (§7.4 Tier 1). Each Regular Node independently verifies the proposal via the two-tier scheme: Tier 1 mandatory checks (TLS proof, self-edge re-computation against own local image, medoid correctness from the published matrix, contributor-set integrity) and Tier 2 probabilistic spot-checks (fetch spot_check_count random other contributors' GeoTIFFs and verify their matrix entries against the node's own image). The node tracks: verification result (accept/reject), the cell coordinates of any spot-check discrepancy, and its own partial BLS signature (if accepted).Each node has either broadcast a BLS signature share to iris/consensus/v1 (accept) or broadcast a rejection (reject) carrying a fraud-proof seed if a spot-check failed.
VotingLeader Node collectsThe Leader Node collects BLS signature shares from iris/consensus/v1. It tracks: which nodes have responded, the count of accepts vs. rejects, the partial signatures received.The Leader Node has collected t valid signature shares (where t > 2n/3), OR the voting_timeout_seconds timer expires with < t shares — in which case the round transitions to Failed with InsufficientSignatures (see §8.8).
CommitLeader Node finalizesThe Leader Node aggregates t partial BLS signatures into a single 48-byte threshold signature. The Relayer module (acting as the trustless transport layer) submits the final report {request_id, ipfs_cid, aggregated_bls_signature} to the IrisVerifier smart contract on the host blockchain. During Phase 1 (§8.7) the Foundation Relayer performs this submission; from Phase 2 on, any reputation-qualified Relayer above the threshold may submit, with timeout-based fallback so a single censoring Relayer cannot stall the round. The node tracks: the finalized CID, the aggregated signature, the transaction hash of the on-chain delivery.The on-chain transaction is confirmed (ReportDelivered emitted) and the round is finalized.
FailedAny reputation-qualified address (per §8.8 authorization rules)Terminal failure state. The round records the local FailureReason (advisory) and the contributor credit map for whichever upstream phase fell short. The category drives the on-chain response: transient failures emit RoundFailed and partial-refund the Requester; attributable failures additionally trigger submitFraudProof and IrisStaking slashing; permanent failures gate resubmission with a cooldown. Full taxonomy and authorization rules in §8.8.reportFailure(requestId, reason, contributorCreditsRoot) is confirmed on-chain.

Round State Data Structure (Conceptual)

// ── Domain Type Aliases ──────────────────────────────────────────────
// Zero-cost newtypes / aliases that give PeerId, keys, and hashes
// distinct semantic meaning across the codebase.

type LeaderId        = PeerId;            // PeerId of the elected leader for a round
type ContributorId   = PeerId;            // PeerId of a node that contributed an observation
type ContentHash     = Blake2bHash;       // BLAKE2b hash of a raw GeoTIFF payload
type AttestationHash = Blake2bHash;       // BLAKE2b hash of a TEE attestation document (.att file)
type AggregateKey    = BlsPublicKey;      // Committee-wide BLS12-381 aggregate public key
type ShareSecretKey  = BlsSecretKeyShare; // A single node's BLS private key share (from DKG)
type SharePublicKey  = BlsPublicKeyShare; // A single node's BLS public key share (from DKG)
type ThresholdSig    = BlsSignature;      // The final aggregated t-of-n BLS signature
struct Round {
    // Identity
    request_id:       RequestId,
    round_number:     u64,
    leader:           LeaderId,
    am_i_leader:      bool,

    // Current position in the FSM
    state:            RoundState,  // enum { Idle, Observing, Aggregating, PreCommit, Voting, Commit, Failed }
    failure_reason:   Option<FailureReason>,  // populated iff state == Failed; advisory only — see §8.8

    // Observation phase — Phase 1 (TLS commitment, ≤60s)
    my_tls_commitment:    Option<TlsCommitment>,
    peer_tls_commitments: HashMap<ContributorId, TlsCommitment>,

    // Observation phase — Phase 2 (manifest finalization, ≤300s)
    my_manifest:          Option<Manifest>,
    peer_manifests:       HashMap<ContributorId, Manifest>,
    my_attestation_hash:  Option<AttestationHash>,            // hash of my own TEE attestation doc
    fetched_tensors:      HashMap<ContentHash, AlignedTensor>,  // Leader only

    // Aggregation phase (Leader only)
    similarity_matrix: Option<Array2<f64>>,
    average_scenario:  Option<AverageScenario>,  // { image_hash, ipfs_cid, tensor }

    // Commit phase
    proposal:          Option<Proposal>,
    signature_shares:  HashMap<ContributorId, BlsSignatureShare>,
    final_signature:   Option<ThresholdSig>,
    verification:      Option<VerificationResult>,  // Regular nodes: did I accept the proposal?

    // Timing
    observation_phase1_deadline: Instant,  // Phase 1: 60s — must publish TlsCommitment
    geotiff_body_deadline:       Instant,  // Phase 2 sub-budget cap on body fetch
    manifest_deadline:           Instant,  // Phase 2 absolute: 300s from round start — must publish Manifest
    phase_deadline:              Instant,  // Current active phase deadline (Aggregating, Voting, etc.)
}

Layer 2 — Node State (persistent, per-node)

This layer persists across rounds. It represents the node's long-lived identity and operational status.

State fieldWhat it tracksMutated when
Identityed25519 keypair, derived PeerId, BLS private key shareNode first boot (keypair generated) or DKG ceremony (BLS share issued)
Committee MembershipList of known committee members, their PeerIds, stake weights, BLS public key shares, and the aggregate public keyDKG ceremony completes after a committee change
Provider CredentialsAPI keys/tokens for Data Providers (Maxar, Planet, Sentinel)Configured by operator in iris.toml
Local CacheContent-addressed storage for GeoTIFFs (~/.iris/cache/objects/<blake2b>.tiff) and attestation documents (~/.iris/attestations/<hash>.att), with operator-friendly symlinks generated per request (~/.iris/cache/rounds/<request_id>/<provider>_<timestamp>.tiff). Garbage-collected per the policy in §3.3.1: hot-layer content past local_cache_retention_days (default 30) is LRU-evicted, with disk-pressure overrides — but never GCs content for which the node is an active-round contributorAfter every successful fetch; LRU eviction on cache GC
Active RoundsMap of RequestId → Round for all rounds the node is currently participating in. [Demo v0.0.1] / [MVP v0.1.0] constraint: limited to one active round at a time to conserve resources and prevent rounds from interfering with each other (competing for bandwidth during GeoTIFF transfer, CPU during normalization). Because rounds are fully independent — each has its own request ID, leader, observation window, and commit — this constraint is an implementation simplification, not a protocol requirement. [V1 v1.0.0] lifts the single-active-round invariant to allow concurrent rounds subject to configurable resource limitsNew request arrives / round finalizes
Peer TableKademlia routing table + GossipSub mesh peersContinuously, via libp2p discovery

Node State Data Structure (Conceptual)

struct NodeState {
    // Cryptographic identity
    keypair:              ed25519::Keypair,
    local_peer_id:        PeerId,                       // multihash(public_key)
    bls_private_share:    Option<ShareSecretKey>,        // Issued during DKG

    // Committee awareness
    committee_members:    Vec<CommitteeMember>,          // { peer_id, stake_weight, bls_pubkey_share }
    aggregate_pubkey:     Option<AggregateKey>,          // Committee-wide aggregate key
    current_epoch:        u64,                           // Monotonic; increments on committee change

    // Provider credentials (loaded from iris.toml)
    provider_credentials: HashMap<Provider, ApiCredential>,  // e.g., Maxar → Bearer token

    // Local cache (content-addressed storage)
    cache_root:           PathBuf,                       // ~/.iris/cache/
    object_store:         HashMap<ContentHash, PathBuf>, // blake2b → objects/<hash>.tiff
    attestation_store:    HashMap<AttestationHash, PathBuf>, // blake2b → attestations/<hash>.att

    // [Demo v0.0.1] / [MVP v0.1.0]: at most one active round.
    // [V1 v1.0.0]: bounded concurrent rounds (single-active-round invariant lifted).
    // The Round struct itself is in-memory only — local cache (objects /
    // attestations) and identity state above are file-backed and survive
    // restarts, but partial round progress (in-flight enclave fetches,
    // mid-download buffers, deadline timers) is not. A node that crashes
    // mid-round is treated by its peers as a non-contributor for that
    // round; on restart it returns to Idle without resuming. WAL-style
    // mid-round persistence is a candidate post-testnet hardening task.
    active_round:         Option<(RequestId, Round)>,

    // Peer table (managed by libp2p)
    kademlia_table:       KademliaRoutingTable,
    gossipsub_mesh:       HashMap<TopicHash, HashSet<PeerId>>,
}

struct CommitteeMember {
    peer_id:          PeerId,
    stake_weight:     u64,
    bls_pubkey_share: SharePublicKey,
}

Layer 3 — Panel State (the reconstruction output)

Each finalized round produces a Panel — one facet of the geodesic reconstruction. The Panel is the primary logical output of the Iris network.

struct Panel {
    // What patch of Earth does this panel represent?
    bounding_box:       BoundingBox,           // Geographic coordinates (lat/lon corners)
    timestamp:          DateTime<Utc>,         // When the imagery was captured

    // The reconstruction
    image_hash:         ContentHash,           // BLAKE2b hash of the finalized image

    // Provenance chain
    attestations:       Vec<AttestationRef>,   // References to the TEE attestation docs of contributing nodes
    contributing_nodes: Vec<ContributorId>,    // Which nodes provided imagery for this panel
    similarity_scores:  Vec<f64>,              // Each contributor's similarity score to the Average Scenario

    // Cryptographic seal
    bls_signature:      ThresholdSig,          // Threshold signature from >2/3 of the committee
    aggregate_pubkey:   AggregateKey,          // The committee's aggregate public key at time of signing

    // On-chain anchor
    request_id:         RequestId,             // Links back to the originating smart contract event
}

Final Payload & Decentralized Storage

While the Panel struct exists logically in the memory of the nodes during consensus, the final, long-term payload delivered to Requesters is packaged differently to ensure permanent accessibility.

Once the "Average Scenario" is finalized, the Leader Node:

  1. Serializes the entire Panel data structure into a standardized .json metadata file.
  2. Wraps both the .json metadata file and the finalized .tiff GeoTIFF image into a single IPFS directory.
  3. Pins this directory to IPFS.

The resulting IPFS CID now points to the complete package: the visual reconstruction (GeoTIFF) and the mathematically verified history of how it was made (JSON Metadata). The committee signs this root CID, and the Relayer submits it to the smart contract. Dapps can then fetch the IPFS folder to instantly retrieve both the imagery and the trustless provenance data.

Panels are immutable once committed. If the same AoI is requested again at a later time, a new panel is created — it does not overwrite the old one. This creates the temporal layering described in the geodesic model.

3.3.1 Storage Persistence & Pinning Strategy

The on-chain CID is only as useful as the underlying bytes are reachable. Iris addresses long-term availability via a persistence-fee model: the Requester pre-pays for a configurable storage period as part of the request fee, and IrisVerifier records the resulting expiry timestamp on-chain so consumers know when to refresh or accept best-effort thereafter. There is no protocol-subsidized "free tier" — every panel the network commits to keeping reachable carries a paid commitment, sized to its payload and its lifetime. This avoids the unbounded-storage pathology that has plagued earlier oracle networks where storage liability quietly accrues until cost-of-pinning swamps protocol revenue.

Persistence period and fee

ParameterDefaultWhere it lives
persistence_period_seconds31,536,000 (one year)Specified per-request by the Requester in the DataRequest; defaulted from iris.toml if omitted
persistence_price_per_gb_yeargovernance-tunable; ~$2 / GB-year reference at MVP launchIrisGovernance parameter, denominated in IRIS token at the prevailing oracle rate
persistence_fee (per request)payload_size_GB × persistence_period_years × persistence_price_per_gb_yearComputed at fee-quote time and bundled into the Requester's requestFee payment to IrisVerifier

The Requester explicitly pays for persistence as part of the same fee that pays the committee and the Relayer. Short-lived applications (e.g., a one-off ground-truth check that the Requester archives off-chain immediately) can specify a small persistence_period_seconds (governance-floored at ~30 days to cover hot-cache eviction lag); long-lived applications (e.g., environmental monitoring needing decade-scale archives) can specify multi-year periods and pay proportionally.

On-chain commitment and top-up

IrisVerifier records panelExpiresAt[requestId] = blockTimestamp(deliverReport) + persistence_period_seconds at report-delivery time and exposes a top-up entry point so anyone can extend a panel's lifetime past the original expiry:

/// Top up the persistence fund for a previously-delivered panel. Anyone
/// can call — the original Requester, a downstream dapp, or a third-party
/// archivist. msg.value is converted to additional persistence time at the
/// current persistence_price_per_gb_year rate; panelExpiresAt[requestId]
/// is extended accordingly. Reverts if the panel is already past expiry
/// for more than `extension_grace_period_seconds` (governance-tunable),
/// because by then warm-layer state may have already been released.
function extendPersistence(bytes32 requestId, uint256 additionalSeconds) external payable;

event PersistenceExtended(bytes32 indexed requestId, uint256 newExpiresAt, address fundedBy);

Consumers reading the on-chain CID can check panelExpiresAt[requestId] against the current block time to know whether the content is still under guaranteed reachability or is on best-effort.

Three-layer pinning topology

LayerWhat it storesDurationFunded byMVP operator
Hot — committee local cacheGeoTIFF + TEE attestation document in ~/.iris/cache/objects/<blake2b>.tiff and ~/.iris/attestations/<hash>.att on each contributing nodelocal_cache_retention_days (default 30) from delivery, then GC-eligibleImplicit in the round fee (committee members already hold the bytes)Each node's own disk
Warm — long-term pinning serviceThe full panel IPFS directory (GeoTIFF + JSON metadata + attestation documents + similarity matrix)Until panelExpiresAt[requestId], extendable via extendPersistenceThe Requester's persistence_feePhase 1: Iris Foundation pinning service (running ipfs-cluster with Pinata-class SLA). Phase 2: Filecoin storage deals funded by the Foundation out of persistence_fee. Phase 3: Permissionless proof-of-storage market with reputation-staked providers and slashing for failed proofs
Cold — content-addressable defense in depthThe underlying GeoTIFF bytes; anyone who still holds them can re-pin under the same CIDIndefinite (best-effort)None — opportunisticThe wider IPFS network; downstream dapps; archivists

The three layers are mutually reinforcing: hot cache covers the days following delivery (when consumer dapps actually fetch the panel), warm pinning provides the on-chain-committed persistence window, and cold content-addressability means even if the warm layer fails the bytes can resurface from any holder. A panel becomes truly unrecoverable only when no holder anywhere maintains a copy — a state the protocol does not promise to prevent past panelExpiresAt.

Phased decentralization of the warm layer

The persistence operator follows the same phased-decentralization shape as the §5.5 attestation roadmap and §8.7 Relayer roadmap:

PhaseOperatorStatusPersistence guarantee
Phase 1 — Foundation pinning serviceIris Foundation operates a pinning clusterMVPSingle-operator SLA. Lost pins are remediable from cold-layer holders but not slashable. Honestly framed as a liveness compromise, not a decentralized guarantee
Phase 2 — Filecoin storage dealsFoundation funds Filecoin deals on the Requester's behalf out of persistence_fee; deals are renewable at every epoch boundaryPlannedCryptographic proofs of replication and proofs of spacetime; deal failures are detectable on-chain and trigger Foundation re-deployment
Phase 3 — Permissionless proof-of-storage marketAny provider above a reputation threshold (mirroring §8.7) can bid for persistence_fee allocations; failed proofs slash the provider's stakeFutureTrustless; safety follows from the proof scheme

Local cache garbage collection

Each node MAY garbage-collect its local cache once both of the following hold for a given content hash:

  1. The round that produced it has reached Commit or Failed state.
  2. Either (a) the node has not been asked to serve the GeoTIFF via /iris/geotiff/1.0.0 for local_cache_retention_days (default 30), or (b) the disk-pressure threshold (cache_disk_pressure_pct, default 85%) is crossed and the LRU policy evicts.

A node MUST NOT GC content for which it is currently a contributor in an active round, regardless of disk pressure — the Leader Node may request the GeoTIFF at any point during Aggregating, and a node that fails to respond is treated as a non-contributor for fee/reputation purposes. Disk-pressure events that would force a GC of active-round content are surfaced as iris_cache_evictions_total metric increments and a WARN-level tracing event; operators should alert on this condition per §12.5.

Cache GC does not affect the warm-layer pin: that is the Foundation pinning service's (or Filecoin's) responsibility. A node that GCs its local copy can still re-fetch via IPFS if it later needs the same content.

Behavior at and past panelExpiresAt

Past expiry, the warm layer is no longer required to serve the content. Concretely:

  1. The Foundation pinning service (Phase 1) is permitted, not required, to release the pin. Many panels remain reachable indefinitely simply because the pinning operator has not actively reclaimed the storage. Requesters should not rely on this — past panelExpiresAt, reachability is best-effort.
  2. Cold-layer holders may continue to serve the CID. The bytes remain content-addressable; any operator who has them can re-pin without coordinating with anyone.
  3. The on-chain reports[requestId].ipfsCid record persists permanently. Only the availability commitment expires, not the on-chain anchor itself. A dapp consuming a stale panel sees the CID but may receive a 404 from IPFS gateways.
  4. Anyone can call extendPersistence before expiry to keep the content under guaranteed reachability, paying the per-byte-second rate at the time of extension. After expiry plus extension_grace_period_seconds, extension reverts on-chain because warm-layer state may have already been released.

This expiry behavior is the architectural escape valve that prevents the storage-cost pathology: a panel that no consumer cares to extend is eventually forgotten, and the protocol's storage liability shrinks naturally to whatever the funded panels collectively demand.

Layer 4 — Committee State (network-wide, slow-moving)

Trust Model: This section is the deep-dive for the Committee (collective) row in §1.3 Trust Model.

The committee is the set of staked, authorized nodes that participate in consensus. This state changes infrequently — only when operators join, leave, or are slashed.

State fieldWhat it tracksMutated when
Active SetThe ordered list of (PeerId, stake_weight) tuples for all nodes currently eligible to participate in roundsA node stakes/unstakes via the IrisStaking smart contract
Aggregate Public KeyThe BLS12-381 aggregate public key representing the committee. Stored both off-chain (in each node's config) and on-chain (in IrisVerifier.sol)DKG ceremony completes after a committee change
Threshold (t)The minimum number of signature shares required: t > ⌊2n/3⌋Committee size changes
EpochA monotonically increasing counter exposed by IrisVerifier.currentEpoch(). Bound into the BLS domain tag ("iris/v" || epoch_number || "/" per §10) so stale threshold signatures from old committees cannot be replayed in a new epoch. Three independent triggers advance it (per versioning_strategies.md §9.1): committee membership change, protocol-version bump (minProtocolVersion raised via governance), or scheduled DKG re-key on TTL expiryupdateCommittee() is called on-chain with a valid old-committee threshold signature over (newEpoch, newAggregateKey, committeeHash, newMinProtocolVersion)

Committee State Data Structure (Conceptual)

struct CommitteeState {
    // Active validator set
    active_set:       Vec<ValidatorEntry>,   // Ordered by cumulative stake
    total_stake:      u64,                   // Sum of all stake weights

    // Threshold cryptography
    aggregate_pubkey: AggregateKey,          // BLS12-381 aggregate public key
    threshold:        usize,                 // t > floor(2n/3) required signature shares
    dkg_state:        DkgState,              // Idle | InProgress | Completed

    // Epoch tracking
    epoch:            u64,                   // Monotonic; increments on committee change
    last_dkg_block:   u64,                   // Block height of last successful DKG
}

struct ValidatorEntry {
    peer_id:          PeerId,
    stake_weight:     u64,
    bls_pubkey_share: SharePublicKey,
    cumulative_stake: u64,  // Used for leader election range mapping
    status:           ValidatorStatus,       // PendingJoin | Active | SlashedExited
}

enum DkgState {
    Idle,
    InProgress { participants: Vec<PeerId>, round: u32 },
    Completed { shares_dealt: usize },
}

Committee Lifecycle

stateDiagram-v2
    [*] --> PendingJoin : Node stakes IRIS tokens
    PendingJoin --> DKGInProgress : Governance approves
    DKGInProgress --> ActiveCommittee : DKG succeeds
(new shares dealt) DKGInProgress --> PendingJoin : DKG fails
(retry, attempts < 3) DKGInProgress --> DkgAborted : DKG fails
(attempts ≥ 3 or ceremony timeout) DkgAborted --> [*] : Pending node dropped
(prior committee retained) ActiveCommittee --> SlashedExited : Slash or unstake SlashedExited --> [*] state PendingJoin { [*] --> AwaitingApproval } state ActiveCommittee { [*] --> ParticipatingInRounds }

DKG Failure Handling

The Distributed Key Generation ceremony is a synchronous multi-round protocol; any participant going offline, sending a malformed share, or failing a verification check causes the ceremony to abort. Because committee changes are infrequent and DKG is gated by on-chain governance, Iris favors safety over liveness here — a stalled DKG never silently degrades the threshold or the aggregate key.

What constitutes a DKG failure. A ceremony is considered failed if any of the following hold before the round timeout:

Retry policy. A failed ceremony is retried up to 3 times for a given committee transition. Each retry begins from a fresh nonce and re-broadcasts the participant list. The total ceremony budget (including retries) is bounded by a 15-minute wall-clock timeout measured from governance approval; this prevents an indefinitely-stalled ceremony from blocking subsequent committee changes.

Permanent abort. If all retries are exhausted, or the wall-clock budget elapses, the ceremony enters DkgAborted. In this state:

Liveness implication. Because the prior committee is retained on permanent abort, the network never loses its ability to sign panels — the worst case is that committee membership becomes "sticky" until governance approves a different candidate. This is the intended trade-off: a wedged DKG should not be able to halt request processing.

Network Layer

Iris is built on its own peer-to-peer (P2P) networking stack to remove dependencies on external oracle infrastructures. The network layer is responsible for peer discovery, authenticated communication, message propagation, and large-payload transfer between nodes. Understanding how these sub-layers compose is essential to understanding how consensus messages, manifests, and imagery flow through the system.

Protocol Stack

Iris is built entirely on rust-libp2p and composes several protocol behaviours into a single multiplexed connection between any two peers. The layering looks like this:

flowchart TD
    subgraph ApplicationLayer ["Application Layer - Virtual Streams"]
        direction LR
        sub1["GossipSub
(pub/sub)"] sub2["Kademlia
(DHT)"] sub3["RequestResponse
(/iris/geotiff)"] Identify["Identify Protocol
(exchange PeerId)"] end subgraph MultiplexingLayer ["Multiplexing Layer"] Yamux["Yamux Multiplexer
(combines logical streams)"] end subgraph SecurityLayer ["Security Layer"] Noise["Noise Protocol (XX)
(mutual authentication + encryption)"] end subgraph TransportLayer ["Transport Layer"] TCP["TCP/IP Socket"] end sub1 --> Yamux sub2 --> Yamux sub3 --> Yamux Identify --> Yamux Yamux --> Noise Noise --> TCP

Every connection between two Iris nodes traverses this entire stack. A single TCP connection is upgraded through Noise, multiplexed through Yamux, and then hosts multiple concurrent protocol streams — a Kademlia lookup, a GossipSub mesh link, and a GeoTIFF transfer can all share the same underlying socket.

Transport & Security

LayerTechnologyPurpose
TransportTCP/IP with DNS resolutionReliable, ordered byte-stream transport. DNS allows nodes to advertise human-readable addresses (/dns4/bootnode.iris.network/tcp/9000) alongside raw IPs
MultiplexingYamuxStream multiplexer that enables multiple logical streams over a single TCP connection. Each protocol (Kademlia, GossipSub, RequestResponse, Identify) opens its own Yamux sub-stream without requiring a new TCP handshake
EncryptionNoise Protocol Framework (XX handshake)Every connection is encrypted and mutually authenticated. During the handshake, both peers prove possession of their ed25519 private keys. The resulting Noise session provides forward-secure symmetric encryption for all data on the wire
Identityed25519 keypairs → PeerIdA node's PeerId is the multihash of its ed25519 public key. This creates a one-to-one binding between network identity and cryptographic identity — there is no way to impersonate a PeerId without holding the corresponding private key

4.2.1 Cryptographic Node Identity (PeerId)

In a decentralized network, it is critical to definitively prove who is sending a message without relying on a centralized registry. Iris solves this natively at the network layer using ed25519 keypairs to generate a mathematically verifiable PeerId.

How the PeerId is Created & Secured:

  1. Key Generation: When an Iris node is booted for the very first time, it generates a fresh ed25519 cryptographic keypair. It stores the private key securely on the local disk.
  2. Derivation: The node runs its public key through a multihash function. The resulting hash string (e.g., 12D3KooW...) is the node's PeerId. Because it is derived from the public key, the PeerId is permanently and exclusively mathematically bound to the private key.
  3. The Noise Handshake: Whenever two Iris nodes connect, they perform a Noise Protocol handshake. During this handshake, the connecting node must use its private key to sign a cryptographic challenge. The receiving node verifies the signature against the public key matching the PeerId.

Why This Matters for Iris:

This identity binding makes impersonation impossible. When a node receives a BLS signature share or a TLS proof from PeerId X, the underlying Noise connection has already cryptographically proven that the sender actively holds the private key for X. No additional authentication checks are needed at the application layer.

Similarly, TLS provenance proofs are bound to the signing node's PeerId, creating an unbroken chain: satellite API → TLS proof → node identity → BLS signature share → on-chain verification.

4.2.2 Node-to-Node Communication Lifecycle

To understand exactly how the Noise protocol and other network layers are used in practice, here is the chronological setup process every time two Iris nodes communicate (e.g., when the Leader Node connects to a Regular Node to fetch a GeoTIFF):

  1. The TCP Connection: Node A initiates a standard TCP connection to Node B's IP address and port. At this stage, the connection is raw, unencrypted, and unauthenticated.
  2. The Noise Handshake (Security): Immediately after the TCP connection is open, the nodes execute the Noise Protocol (XX pattern).
    • They perform a Diffie-Hellman key exchange to generate a temporary "shared secret" key.
    • They mutually authenticate by signing a challenge with their long-term ed25519 private keys.
    • Result: The connection is now mutually authenticated and protected by forward-secure encryption. If a hacker intercepts the traffic, it appears as random noise.
  3. The Yamux Multiplexer: With the secure tunnel established, the nodes initialize Yamux. Instead of opening a new TCP connection for every file transfer or gossip message, Yamux allows the nodes to open hundreds of lightweight, concurrent "virtual streams" inside that single encrypted Noise tunnel.
  4. The Identify Protocol: The very first virtual stream opened is the Identify protocol. The nodes formally introduce themselves by exchanging their PeerId, their software version (iris-node/v1.0.0), and the specific sub-protocols they support.
  5. Application Streams: Once identified, the nodes open further virtual streams for actual Iris protocol work. They will open a long-running stream for GossipSub (to exchange lightweight manifests) and temporary RequestResponse streams to directly transfer massive GeoTIFF payloads (~1GB up to 16GB).

Because Noise secures the entire TCP connection at the lowest level, all subsequent Yamux streams, GossipSub messages, and GeoTIFF transfers are automatically encrypted and inherently trusted to be coming from that specific PeerId.

sequenceDiagram
    participant A as Node A
(Initiator) participant B as Node B
(Receiver) Note over A, B: 1. Unencrypted Transport A->>B: TCP Handshake (SYN / ACK) Note over A, B: 2. Noise Protocol (Security) A->>B: Diffie-Hellman Key Exchange A->>B: Authenticate: Sign with ed25519 Private Key B->>A: Authenticate: Sign with ed25519 Private Key Note over A, B: Connection is now Forward-Encrypted & Mutually Authenticated Note over A, B: 3. Multiplexing A->>B: Initialize Yamux Multiplexer Note over A, B: 4. Identify Protocol (First Yamux Stream) A->>B: Send PeerId & Supported Protocols B->>A: Send PeerId & Supported Protocols Note over A, B: 5. Application Streams (Concurrent Virtual Streams) A->>B: Open Stream: GossipSub (Manifest Exchange) A->>B: Open Stream: RequestResponse (GeoTIFF Transfers)

Peer Discovery (Kademlia DHT)

Iris uses a Kademlia Distributed Hash Table for decentralized peer discovery. The Kademlia DHT does not store application data — it is used exclusively for finding other Iris nodes.

4.3.1 Bootstrap Process

When a new node starts for the first time:

  1. Load bootstrap addresses. The node reads a list of well-known bootstrap Multiaddr values from iris.toml. These are hosted by the Iris Foundation initially and are the only hardcoded entry points into the network.
  2. Dial bootstraps. The node establishes TCP connections to each bootstrap peer, performs the Noise handshake, and runs the Identify protocol to exchange agent strings, listen addresses, and protocol versions.
  3. Kademlia bootstrap. The node issues a Kademlia FIND_NODE query for its own PeerId. This self-lookup populates its routing table by discovering nodes that are close in XOR distance.
  4. Random walks. Periodically (every 30 seconds), the node queries a random PeerId to further populate its routing table and maintain diverse connections across the keyspace.
  5. Steady state. Once the routing table contains enough peers, the node can discover any other node in O(log n) hops without relying on the bootstrap nodes.
sequenceDiagram
    participant N as New Node
    participant B as Bootstrap Node
    participant Net as Iris Network (Other Peers)

    Note over N: 1. Load addresses from iris.toml

    Note over N, B: 2. Dial Bootstraps
    N->>B: TCP Connect + Noise Handshake
    B-->>N: Secure Connection Established
    N->>B: Identify Protocol (Exchange PeerIds)

    Note over N, B: 3. Kademlia Self-Lookup
    N->>B: Kademlia FIND_NODE (Target: Own PeerId)
    B-->>N: Returns closest peers it knows

    Note over N, Net: 4. Random Walks & Mesh Building
    N->>Net: Dial newly discovered peers
    N->>Net: Kademlia FIND_NODE (Target: Random PeerId)
    Net-->>N: Returns even more peers

    Note over N, Net: 5. Steady State
    Note over N: Routing table is now populated.
Can discover any node in O(log n) hops.

4.3.2 Routing Table

The Kademlia routing table organizes peers into k-buckets by XOR distance from the local node's PeerId. Each bucket holds up to k = 20 peers. The table provides O(log n) lookup guarantees for a network of n nodes. For Iris's expected committee sizes (10–100 nodes), this means any node can be located in 1–2 hops.

flowchart TD
    Root(("Root
(Bit 0)")) %% First split (Bucket 1) Root -- "Bit differs
(Opposite Half)" --> B1["Bucket 1
(Furthest Peers)
[Max 20 Peers]"] Root -- "Bit matches
(Same Half)" --> N1(("Bit 1")) %% Second split (Bucket 2) N1 -- "Bit differs
(Opposite Quarter)" --> B2["Bucket 2
[Max 20 Peers]"] N1 -- "Bit matches
(Same Quarter)" --> N2(("Bit 2")) %% Third split (Bucket 3) N2 -- "Bit differs
(Opposite Eighth)" --> B3["Bucket 3
[Max 20 Peers]"] N2 -- "Bit matches
(Same Eighth)" --> N3(("...")) %% Last split (Bucket 256) N3 -- "Last bit differs
(Next-door neighbor)" --> B256["Bucket 256
(Closest Peers)
[Max 20 Peers]"] N3 -- "Last bit matches" --> Local["🟢 Local Node
(Distance: 0)"] classDef bucket fill:#f9f6ff,stroke:#8a2be2,stroke-width:2px; classDef localNode fill:#d4edda,stroke:#28a745,stroke-width:3px; class B1,B2,B3,B256 bucket; class Local localNode;

Message Propagation (GossipSub v1.1)

GossipSub is the pub/sub layer that propagates lightweight messages across the network. Iris uses GossipSub v1.1, which includes peer scoring and flood publishing to harden the mesh against Sybil and eclipse attacks.

Topic Architecture

Iris defines three GossipSub topics, each carrying a specific message type at a specific phase of the round:

TopicMessage TypePayload SizePublished ByConsumed ByRound Phase
iris/requests/v1DataRequest~200 bytesRelayer (one node)All nodesIdle → Observing
iris/observations/v1TlsCommitment (Phase 1)~250 bytesAll Regular NodesLeader NodeObserving (Phase 1, ≤60s)
iris/observations/v1Manifest (Phase 2)~500 bytesAll Regular NodesLeader NodeObserving (Phase 2, ≤300s)
iris/consensus/v1Proposal / BLSShare / Rejection~300 B (BLSShare/Rejection); ~2 KB (Proposal, n=20) – ~40 KB (Proposal, n=100) — Proposal carries the full pairwise similarity matrix per §7.4Leader Node (Proposal) / Regular Nodes (BLSShare/Rejection)All nodesPreCommit → Voting → Commit
Critical design decision: Full GeoTIFF payloads (500 MB – 16 GB each) are never published to GossipSub. Gossiping a single 1 GB observation through a 20-node mesh would produce ~20 GB of redundant network traffic per round. Instead, only lightweight manifests (~500 bytes) are gossiped. The Leader Node retrieves full payloads via direct streams (Section 4.5) only when needed.

Mesh Topology

GossipSub v1.1 maintains a mesh of D = 6 peers per topic (configurable via iris.toml). The mesh parameters are:

ParameterValueRationale
D (target mesh degree)6Balances redundancy against bandwidth. Each message is forwarded to 6 peers
D_low (minimum mesh degree)4Below this, the node GRAFTs additional peers into the mesh
D_high (maximum mesh degree)12Above this, the node PRUNEs excess peers to limit fan-out
D_lazy (gossip factor)6Number of peers to whom the node sends IHAVE control messages for messages not directly forwarded
Heartbeat interval1 secondHow often the node evaluates mesh health and peer scores
Message TTL600 secondsMessages older than this are dropped and not forwarded. Sized to comfortably outlast the Phase 2 manifest deadline (300s, §7.2) so manifests published early in Observing remain forwardable until the Leader Node transitions to Aggregating, with 2× headroom for clock skew and slow propagation paths

Peer Scoring

GossipSub v1.1's peer scoring system is essential for Iris's Byzantine resistance at the network layer. Each peer is assigned a score based on:

Peers whose score drops below a configurable threshold are disconnected from the mesh and eventually blacklisted from the topic entirely.

Message Serialization

All GossipSub messages are serialized using CBOR (Concise Binary Object Representation) via the ciborium crate. CBOR was chosen over Protobuf for the MVP because:

Crate note: ciborium is used instead of the older serde-cbor crate, which has been unmaintained since 2021. ciborium is the actively maintained CBOR implementation for the serde ecosystem and is a drop-in replacement for serialization purposes.

Serialization migration timeline. CBOR is the serialization format for prototyping and devnet. At testnet launch all GossipSub message types migrate to Protobuf, with .proto IDL files checked into the repository as the canonical, schema-enforced breaking-change surface. From that point forward, any modification to a required field, removal of a field, or reordering of fields is a breaking change requiring a 67% governance vote and an epoch bump — see §10 Versioning & Upgrade Strategy.

Direct Streams — GeoTIFF & Attestation Document Transfer Protocols

GossipSub is designed for small, fan-out messages. Two artifacts in Iris are neither small nor fan-out: the GeoTIFF imagery payload (~500 MB – 16 GB) and the TEE attestation document (~4–14 KiB depending on vendor). Both flow point-to-point — the Leader Node pulls them from specific peers during Aggregating, and Regular Nodes pull the medoid's attestation during PreCommit verification (§7.4 Tier 1, item 1). Iris defines two parallel RequestResponse protocols for this purpose.

4.5.1 GeoTIFF Transfer (/iris/geotiff/1.0.0)

FieldValue
Protocol ID/iris/geotiff/1.0.0
Transportlibp2p RequestResponse behaviour over the existing Yamux-multiplexed connection
Request payloadGeotiffRequest { image_hash: ContentHash } — the BLAKE2b-512 hash of the desired GeoTIFF, as advertised in the sender's manifest
Response payloadGeotiffResponse { data: Vec<u8> } — the raw GeoTIFF bytes, streamed in chunks
Timeout3600 seconds
Max payload size16 GB

Transfer Flow

sequenceDiagram
    participant Leader
    participant NodeA
    participant NodeB
    participant NodeC

    Note over Leader: Aggregating phase begins

    Leader->>NodeA: GeotiffRequest { image_hash: 0xabc... }
    Leader->>NodeB: GeotiffRequest { image_hash: 0xdef... }
    Leader->>NodeC: GeotiffRequest { image_hash: 0x123... }

    NodeA-->>Leader: GeotiffResponse { data: [1.2 GB GeoTIFF] }
    NodeB-->>Leader: GeotiffResponse { data: [1.1 GB GeoTIFF] }
    NodeC-->>Leader: GeotiffResponse { data: [1.3 GB GeoTIFF] }

    Note over Leader: Verify BLAKE2b(data) == image_hash
    Note over Leader: All payloads received, run normalization pipeline
      

The Leader Node opens parallel direct streams to all contributing nodes simultaneously (via tokio tasks). Each stream is a dedicated Yamux sub-stream — they do not interfere with each other or with GossipSub traffic on the same connection.

Integrity Verification

Upon receiving a GeoTIFF payload, the Leader Node:

  1. Computes BLAKE2b(payload) and verifies it matches the image_hash from the sender's manifest.
  2. Checks that the manifest's attestation_hash references a valid, verifiable TEE attestation document (either cached locally or fetched from the sender via a separate request).
  3. Stores the raw GeoTIFF payload in the local cache (~/.iris/cache/objects/<blake2b>.tiff) and parses it into an in-memory ndarray-based tensor representation for the normalization pipeline.

If any verification step fails, the payload is discarded and the contributing node's peer score is penalized.

4.5.2 Attestation Document Transfer (/iris/attestation/1.0.0)

The TEE attestation document is too large to gossip (4–14 KiB; well above the GossipSub message budget) and too small to merit the chunked streaming codec used for GeoTIFFs. A second RequestResponse protocol parallels the GeoTIFF protocol, keyed by AttestationHash (the BLAKE2b-512 hash of the on-disk attestation envelope, also referenced as attestation_hash in the contributor's Manifest).

FieldValue
Protocol ID/iris/attestation/1.0.0
Transportlibp2p RequestResponse behaviour over the existing Yamux-multiplexed connection
Request payloadAttestationRequest { attestation_hash: AttestationHash } — the BLAKE2b-512 hash of the desired attestation document, as advertised in the sender's Manifest as attestation_hash
Response payloadAttestationResponse::Found { data: Vec<u8> } (raw .att envelope bytes) or AttestationResponse::NotFound
Timeout120 seconds
Max payload size256 KiB (typical: ~5–14 KiB; the cap is generous headroom for future vendor formats and any bundled collateral)

Caller pattern. The Leader Node pulls each contributor's attestation during Aggregating to perform full AttestationVerifier verification (§5.4) before including the contributor's GeoTIFF in the normalization run. Regular Nodes pull the attestation for selected_image_hash from the Leader Node (or any peer cache) during PreCommit to satisfy Tier 1 step 1 before broadcasting a BLS share. Both flows use the same protocol; serve-side responds from the local hot-layer cache (§3.3.1) at ~/.iris/attestations/<hash>.att.

A node that fails to serve a requested attestation (NotFound, timeout) is treated identically to a node that fails to serve a requested GeoTIFF: the contributor is dropped from the round and its GossipSub peer score is decremented. A node whose attestation bytes deserialize but fail any of the §5.4 verification checks is more serious — see threat_model.html §5 / Invalid attestation.

Message Lifecycle — A Complete Round

To illustrate how all network sub-layers interact during a single consensus round:

sequenceDiagram
    participant Chain as Host Blockchain
    participant Relayer as Relayer Node
    participant GS as GossipSub Mesh
    participant Leader as Leader Node
    participant Nodes as Regular Nodes (×n)

    Chain->>Relayer: DataRequest event emitted
    Relayer->>GS: Publish to iris/requests/v1

    GS->>Leader: DataRequest delivered
    GS->>Nodes: DataRequest delivered

    Note over Nodes: Observing Phase 1 (≤60s): spawn iris-fetch enclave, in-enclave TLS handshake

    Nodes->>GS: Publish TlsCommitments to iris/observations/v1
    GS->>Leader: TlsCommitments delivered (~250 bytes each)

    Note over Nodes: Observing Phase 2 (≤300s): finalize in-enclave download & vendor attestation

    Nodes->>GS: Publish Manifests to iris/observations/v1
    GS->>Leader: Manifests delivered (~500 bytes each)

    Note over Leader: Aggregating: need full GeoTIFFs

    Leader->>Nodes: Direct stream: GeotiffRequest per manifest
    Nodes-->>Leader: Direct stream: GeotiffResponse (500 MB - 16 GB each)

    Note over Leader: Run normalization pipeline, select Average Scenario, pin to IPFS

    Leader->>GS: Publish Proposal to iris/consensus/v1
    GS->>Nodes: Proposal delivered

    Note over Nodes: Verify proposal, re-run normalization

    Nodes->>GS: Publish BLS signature shares to iris/consensus/v1
    GS->>Leader: BLS shares delivered

    Note over Leader: Aggregate t > 2n/3 shares into threshold signature

    Leader->>Relayer: Final report (CID + BLS signature)
    Relayer->>Chain: deliverReport() on-chain
      

Notice the two distinct bandwidth regimes:

This two-tier design keeps the GossipSub mesh fast and lightweight while allowing the Leader Node to handle bulk data transfer via dedicated point-to-point channels.

Network State Data Structure (Conceptual)

The network layer maintains its own persistent state that lives alongside the Node State (Section 3.2):

struct NetworkState {
    // Identity & transport
    local_peer_id:    PeerId,
    keypair:          ed25519::Keypair,
    listen_addresses: Vec<Multiaddr>,       // e.g., /ip4/0.0.0.0/tcp/9000

    // Discovery
    bootstrap_addrs:  Vec<Multiaddr>,       // From iris.toml
    kademlia_table:   KademliaRoutingTable, // k-buckets of known peers
    connected_peers:  HashSet<PeerId>,      // Currently connected peers

    // GossipSub mesh state (per topic)
    mesh: HashMap<TopicHash, MeshState>,
    peer_scores: HashMap<PeerId, f64>,      // GossipSub v1.1 peer scores

    // Direct streams
    pending_transfers: HashMap<RequestId, Vec<PendingGeotiffRequest>>,
    transfer_stats:    HashMap<PeerId, TransferMetrics>,  // bandwidth, latency
}

struct MeshState {
    topic:       TopicHash,
    mesh_peers:  HashSet<PeerId>,   // Active mesh links (target: D=6)
    fanout_peers: HashSet<PeerId>,  // Peers we publish to but aren't meshed with
    last_published: Instant,
}

struct TransferMetrics {
    bytes_sent:     u64,
    bytes_received: u64,
    avg_latency_ms: f64,
    failed_requests: u32,
}

Configuration (iris.toml — Network Section)

All network parameters are operator-configurable via the [network] section of iris.toml:

[network]
listen_address = "/ip4/0.0.0.0/tcp/9000"
bootstrap_peers = [
    "/dns4/boot1.iris.network/tcp/9000/p2p/12D3KooW...",
    "/dns4/boot2.iris.network/tcp/9000/p2p/12D3KooW...",
    "/dns4/boot3.iris.network/tcp/9000/p2p/12D3KooW...",
]

[network.gossipsub]
mesh_degree = 6
mesh_degree_low = 4
mesh_degree_high = 12
lazy_degree = 6
heartbeat_interval_ms = 1000
message_ttl_seconds = 600

[network.kademlia]
k_bucket_size = 20
bootstrap_interval_seconds = 30

[network.transfer]
geotiff_timeout_seconds = 3600
max_payload_bytes = 17_179_869_184  # 16 GB
max_concurrent_transfers = 10

The TEE attestation integration carries its own configuration block. Authoritative until Phase 5's IrisGovernance introduces an on-chain attestation registry (per §5.5):

[attestation]
# Which TEE vendor this node is running its iris-fetch enclave on.
# One of: "nitro" | "sgx" | "tdx" | "sev_snp" | "mock" (mock is devnet-only).
local_attestor = "nitro"

# Per-vendor accepted-roots whitelist (used by AttestationVerifier on every
# inbound .att). Each entry binds a vendor tag to (a) the certificate-chain
# root used to validate the vendor's attestation PKI, and (b) the
# enclave-measurement values that identify accepted builds of iris-fetch.
# Entries with `enabled = false` are not accepted on this node.

[attestation.accepted.nitro]
enabled = true
root_cert_path = "~/.iris/attestation/aws-nitro-root.pem"  # bundled w/ iris-node
accepted_pcr0   = ["0x<48-byte-sha384-hex>"]               # iris-fetch image measurement
accepted_pcr1   = ["0x<48-byte-sha384-hex>"]               # kernel cmd-line measurement
accepted_pcr2   = ["0x<48-byte-sha384-hex>"]               # application image measurement

[attestation.accepted.sgx]
enabled = true
root_cert_path = "~/.iris/attestation/intel-dcap-root.pem"
accepted_mrenclave = ["0x<32-byte-hex>"]                   # iris-fetch SGX enclave measurement
accepted_mrsigner  = ["0x<32-byte-hex>"]                   # signer of the iris-fetch enclave

[attestation.accepted.sev_snp]
enabled = true
root_cert_path = "~/.iris/attestation/amd-ark.pem"
accepted_measurement = ["0x<48-byte-hex>"]                 # iris-fetch SEV-SNP launch measurement

# Cert-chain CN allowlist for §5.4 step 3 (Server Identity Check).
approved_provider_hosts = [
    "api.maxar.com",
    "api.planet.com",
    "scihub.copernicus.eu",
]

# §5.4 step 7 freshness window. Tighten for stricter replay defense.
attestation_freshness_window_seconds = 900

Data Provenance & Ingestion

This section is a focused deep-dive into the cryptographic mechanism that secures the first link in the Chain of Provenance: proving that raw satellite imagery is genuine and untampered. For the full round lifecycle that uses these proofs (observation, aggregation, verification, signing), see §3.1 — Round State. For the normalization pipeline that processes verified raw data, see §6 — Data Normalization Engine.

The Provenance Problem

Iris operates under the assumption that individual Regular Nodes are untrusted. If a node were to simply download a GeoTIFF and submit it, nothing would prevent that node from modifying the image (e.g., removing a building or altering crop health indicators). Furthermore, the data requires heavy preprocessing (orthorectification, resampling) before the network can compare it — and the network cannot trust the node to perform that preprocessing honestly.

Iris solves this with an unbroken Chain of Provenance built on two mechanisms:

  1. Hardware-attested fetches (this section): The node performs the TLS connection to the Data Provider from inside a Trusted Execution Environment (TEE). The TEE produces a hardware-signed attestation document binding the response payload's BLAKE2b hash to the loaded code, the requested URL, and a request-binding nonce. Anyone can verify the document against the TEE vendor's public-key infrastructure without contacting the node, the vendor, or the Provider.
  2. Deterministic Reproduction (§6, §7.4): Verifiers independently re-run the normalization pipeline on the attested raw data, ensuring any tampering during preprocessing is detected.

The result is a verifiable chain: Satellite API → TLS-in-TEE → Hardware Attestation → Deterministic Normalization → BLS Signature → On-chain Verification.

Why TEE attestation rather than TLSNotary. An earlier revision of this spec specified TLSNotary (two-party MPC over the TLS session) for the first link. The [2.3] integration spike (crates/iris-data/notes/tlsn-spike-findings.md, 2026‑05‑08) confirmed that the upstream tlsn crate is alpha-quality, that MPC preprocessing scales linearly with max_recv_data (263 ms for 16 KiB on alpha-14), and that direct attestation of multi-GB GeoTIFF payloads cannot meet the 60 s TlsCommitment budget at any committee size. The current approach removes the alpha-quality dependency, attests the actual GeoTIFF bytes (no HEAD-then-bulk-fetch split required), and replaces a single-vendor third-party Notary with a multi-vendor whitelist that operators can satisfy on AWS, GCP, Azure, or owned server hardware.

TEE Remote Attestation: Hardware-Rooted Proof of Origin

Iris uses TEE remote attestation — a vendor-signed assertion that a specific piece of code ran inside a hardware-isolated trust domain and produced specific outputs — to create a cryptographic guarantee of the raw data's origin without requiring cooperation from the Data Provider. The same primitive is used in production by Signal contact discovery, MobileCoin, Cloudflare Geo Key Manager, AWS KMS, and the npm/PyPI/GitHub artifact-attestation supply chain.

The Attested Fetch

When a Regular Node queries a Data Provider's API during the Observing phase (see §3.1), the network-facing fetch runs inside a TEE rather than on the host operating system. Concretely:

  1. Enclave bootstrap. The node's host process spawns a TEE instance (AWS Nitro Enclave, Intel SGX/TDX trust domain, AMD SEV-SNP guest) preloaded with the iris-fetch binary. The TEE's hardware boot chain measures the loaded code into vendor-specific platform configuration registers (Nitro PCR0/PCR1/PCR2, SGX MRENCLAVE, TDX MRTD, SEV-SNP MEASUREMENT). Operators choose their TEE vendor independently per §5.5; the protocol is vendor-agnostic.
  2. Standard TLS to the Provider. The enclave opens a normal TLS 1.3 connection to the authenticated endpoint (e.g., api.maxar.com) using the system root CA store bundled into the enclave image. There is no MPC, no garbled circuits, and no third-party signer in the connection — this is an ordinary HTTPS fetch from inside a hardware trust boundary.
  3. In-enclave streaming hash. The enclave streams the response body chunk-by-chunk through a BLAKE2b-512 hasher, never holding more than one chunk in memory. On stream completion it has both the payload bytes (handed back to the host process for caching) and the canonical image_hash.
  4. Attestation request. The enclave calls the platform-specific attestation API (NSM_GetAttestationDoc on Nitro, EGETKEY+EREPORT on SGX, TDG.MR.REPORT on TDX, MSG_REPORT_REQ on SEV-SNP) with a 64-byte user_data field containing BLAKE2b(request_id ‖ provider_hostname ‖ image_hash ‖ unix_timestamp ‖ enclave_signing_pubkey ‖ sensor_id ‖ mission ‖ processing_level ‖ radiometric_units). The sensor_id is a uint32 key into the IrisGovernance sensor registry, parsed in-enclave from the GeoTIFF metadata per design_q1_effective_nadir_angle.md §1.2.1.b so a fraud proof can attribute "wrong-sensor-claimed" forgery. The mission / processing_level / radiometric_units triple (uint16 / uint8 / uint8) is parsed alongside sensor_id from the same GeoTIFF metadata pass; it declares what the contributor claims to have fetched (e.g., Sentinel2Msi / L2A / Boa) so the §6.1.1 dispatch table can route to the correct preprocessing handler and so FraudType::MislabeledProduct (parallel to WrongSensorClaimed) is reachable. The platform returns a CBOR/COSE-signed (Nitro) or vendor-quote-signed (SGX/TDX/SEV-SNP) document binding (measurement, user_data, certificate_chain_to_vendor_root).
  5. Selective disclosure of request metadata. The enclave separately emits a small RequestRecord { url, method, header_keys, query_params } derived from the HTTP request, with sensitive fields (Authorization header value, API key query params) zeroed at the source so they never leave the enclave. The record is signed by an enclave-resident ed25519 key whose public half is itself bound into the attestation document's user_data.
sequenceDiagram
    participant Host as Host Process
(untrusted) participant TEE as Iris-Fetch Enclave
(measured) participant Vendor as TEE Vendor PKI
(AWS / Intel / AMD) participant API as Data Provider
(e.g., Maxar API) Note over Host, TEE: Phase 1 — Enclave bootstrap & measurement Host->>TEE: Launch iris-fetch with request_id, provider, query Note over TEE: Hardware measures loaded code
into PCR0/MRENCLAVE/MRTD/MEASUREMENT Note over TEE, API: Standard TLS 1.3, no MPC TEE->>API: TLS Handshake (system root CAs in enclave) API-->>TEE: TLS Session Established TEE->>API: HTTP GET imagery (Authorization in-enclave) API-->>TEE: Encrypted RAW GeoTIFF Payload Note over TEE: Streaming BLAKE2b → image_hash Note over TEE, Vendor: Phase 2 — Hardware attestation TEE->>Vendor: GetAttestationDoc(user_data = BLAKE2b(request_id‖host‖image_hash‖ts‖enclave_pubkey‖sensor_id‖mission‖level‖units)) Vendor-->>TEE: Signed attestation { measurement, user_data, cert_chain } TEE-->>Host: (payload bytes, attestation_doc, signed_request_record)

The whole flow is non-interactive with respect to the vendor's PKI: the attestation document is verified offline against the vendor's published root certificates. There is no per-round network call to AWS, Intel, or AMD.

Anatomy of an Attestation Document

The on-disk artifact a Regular Node produces after a successful attested fetch is an attestation document stored at ~/.iris/attestations/<attestation_hash>.att (the .att extension replaces the legacy .tlsn; see §3.2). It is a self-contained, portable artifact that any third party can verify without contacting the original Data Provider, the node, or the TEE vendor's online services. The document is a tagged enum across accepted vendor formats; common fields are normalized at the verifier boundary.

FieldDescription
AttestorTagged identifier Nitro | SgxDcap | TdxQuote | SevSnp | Mock selecting the vendor verifier. The Mock variant is dev-only and is never accepted on testnet/mainnet (the on-chain whitelist excludes it)
Vendor Certificate ChainThe attestation root → intermediate(s) → instance-key chain anchored at the TEE vendor's published PKI (AWS Nitro root, Intel DCAP root, AMD ARK/ASK/VCEK chain). A verifier independently checks chain validity against bundled vendor roots
MeasurementVendor-specific cryptographic measurement of the loaded enclave code: Nitro PCR0 (kernel) + PCR1 (kernel cmd-line) + PCR2 (application image); SGX MRENCLAVE; TDX MRTD; SEV-SNP MEASUREMENT. Verifiers check this against the accepted-measurements whitelist (§4.8) — only known builds of iris-fetch are admitted
User DataThe 64-byte enclave-bound payload BLAKE2b(request_id ‖ provider_hostname ‖ image_hash ‖ unix_timestamp ‖ enclave_signing_pubkey ‖ sensor_id ‖ mission ‖ processing_level ‖ radiometric_units). sensor_id (uint32) is parsed in-enclave from the GeoTIFF metadata and indexes the IrisGovernance sensor registry — see design_q1_effective_nadir_angle.md §1.2.1.b / §1.2.2.d. The mission (uint16, satellite/instrument family — Sentinel1Sar, Sentinel2Msi, Sentinel3Olci, Landsat9Oli, PlanetScope, WorldView3, Mock), processing_level (uint8L0, L1A, L1B, L1C, L1Grd, L1Slc, L2A, L2Ocn, Ard), and radiometric_units (uint8Dn, Toa, Boa, RadianceWm2, Sigma0Db, Sigma0Linear) declare what the contributor claims to have fetched and pick the preprocessing handler via §6.1.1 — the accepted-tuple matrix lives in parameters.html §14.2.8. Together this is the binding between the hardware attestation, the per-fetch facts, the declared sensor (which enables FraudType::WrongSensorClaimed), and the declared product (which enables FraudType::MislabeledProduct).
Server IdentityThe DNS hostname, ALPN, and X.509 certificate chain seen by the enclave when it connected to the Data Provider, signed by the enclave's ephemeral ed25519 key (whose public half is bound into user_data). Proves the connection terminated at the authentic API endpoint, not a spoofed server
Request Record{ url, method, header_keys (values omitted for Authorization / API keys), query_params }, signed by the enclave's ephemeral key. Proves what was requested (coordinates, timestamp, imagery product) without leaking credentials
Image HashBLAKE2b-512 of the raw response payload, computed in-enclave. This is what appears in the node's Manifest as image_hash and what binds the GossipSub-level commitment to the on-disk GeoTIFF and the attestation.
Vendor-Signed TimestampWall-clock time as reported by the platform attestation service. Nitro and SGX-DCAP both bind a fresh timestamp into the signed quote; SEV-SNP and TDX bind an attestation-counter / freshness nonce that the verifier resolves to a wall-clock through the vendor's collateral service

The on-disk file uses a versioned CBOR envelope: { version: u16, attestor: tag, document: bytes, signed_request_record: bytes }. Typical sizes are 5 KiB (Nitro), 8–14 KiB (SGX-DCAP including collateral), 6 KiB (TDX), 4 KiB (SEV-SNP) — all well under the legacy 2 MiB cap of the proof-transfer protocol (§4.5.2). The attestation_hash: AttestationHash referenced by Manifest is BLAKE2b-512 of the full envelope bytes.

Key Property: The attestation document only covers the raw, unprocessed API response. It does not — and cannot — attest to any post-processing the node performs on the data. This is why Iris requires Deterministic Reproduction (§6, §7.4) as the second link in the provenance chain. It also does not attest to truthfulness of the bytes the Provider served, only to the fact of delivery — pixel-level lies are filtered by the consensus medoid (§7.4) and addressed in threat model §2.3.

Attestation Verification

Any node in the network can verify an attestation document by performing the following steps. This verification is executed by the Leader Node during the Aggregating phase and by Regular Nodes during the PreCommit phase (see §3.1 for phase definitions). The implementation lives in crates/iris-data/src/verifier.rs (AttestationVerifier).

  1. Vendor Chain Validation. Dispatch on the document's attestor tag and verify the vendor certificate chain to the bundled root: aws-nitro-enclaves-cose for Nitro, dcap-rs / intel-tee-quote-verification-rs for SGX/TDX, the virtee SEV-SNP verifier for AMD. The accepted root authorities are configured under [attestation] in iris.toml (§4.8) and superseded by the on-chain whitelist once IrisGovernance ships. A document signed by a vendor not in the whitelist is rejected immediately.
  2. Measurement Whitelist Check. Verify the document's measurement (PCR0/MRENCLAVE/MRTD/MEASUREMENT) is in accepted_measurements for that vendor. This is the mechanism by which the network admits only known builds of iris-fetch — an operator running modified enclave code produces a measurement not on the list and is rejected before any other check runs.
  3. Server Identity Check. Verify the X.509 chain for the Data Provider TLS connection (carried in the document's signed request record) chains to a system root CA and that the server hostname is in approved_provider_hosts (e.g., api.maxar.com, api.planet.com, scihub.copernicus.eu). This prevents a compromised enclave image from being used to attest fetches against rogue servers.
  4. Payload Integrity Check. Verify that the document's image_hash field equals the image_hash advertised in the node's Manifest and equals BLAKE2b(payload) recomputed locally on the GeoTIFF received over /iris/geotiff/1.0.0. Any mismatch indicates either tampering after the enclave returned or contributor confusion; the contributor is dropped from the round.
  5. Request Plausibility Check. Inspect the (selectively disclosed) request record to confirm the query parameters cover the active DataRequest — i.e., the bounding box and timestamp in the HTTP request are consistent with the AoI being reconstructed.
  6. User-Data Binding Check. Recompute BLAKE2b(request_id ‖ provider_hostname ‖ image_hash ‖ vendor_timestamp ‖ enclave_signing_pubkey ‖ sensor_id ‖ mission ‖ processing_level ‖ radiometric_units) and verify it equals the document's user_data field. The sensor_id is a uint32 key into the IrisGovernance sensor registry (design_q1 §1.2.2.d), parsed by iris-fetch inside the enclave from the GeoTIFF metadata (design_q1 §1.2.1.b) before the attestation is sealed. The mission / processing_level / radiometric_units triple is parsed from the same metadata pass (sensor parser surface in [2.4.3][2.4.5]) and carried into the Manifest as plain fields the verifier reads back; the recompute therefore takes the claimed triple from the Manifest as input and asserts mismatch when the contributor declares (e.g.) (Sentinel2Msi, L2A, Boa) but the enclave actually sealed (Sentinel1Sar, L1Grd, Sigma0Db). This binding is what makes (a) cross-request attestation replay structurally impossible, (b) FraudType::WrongSensorClaimed reachable (sensor A vs. sensor B), and (c) FraudType::MislabeledProduct reachable (declared product vs. attested product). Any mismatch yields VerifyError::UserDataMismatch — the same code path exercised by adversarial scenarios [4.5.1.7] (cross-round replay) and [4.5.1.11] (mislabeled product).
  7. Timestamp Freshness. Verify the vendor-signed timestamp falls within attestation_freshness_window_seconds of the DataRequest event's block timestamp. The default is 900 seconds (manifest_deadline_seconds = 300 plus a 600 s margin covering chain finality, enclave/host clock skew, and round-FSM bookkeeping). The window is governance-tunable via IrisGovernance post-MVP; until then the value lives in the local [attestation] section of iris.toml. Tightening it improves replay resistance against a compromised enclave (per threat model §2.4); loosening it breaks honest nodes whose vendor clocks drift. The default sits comfortably above the worst-case honest round duration and well below the time-scale at which the underlying AoI's pixel content meaningfully changes.

If any step fails, the attestation is rejected, the associated payload is discarded, and the contributing node's GossipSub peer score is penalized (see §4.4 — Peer Scoring). The lists of accepted vendor roots, accepted enclave measurements, and approved Data Provider hostnames all live under the [attestation] section of iris.toml (see §4.8). Phase 5's IrisGovernance supersedes them with on-chain whitelists; until then the local lists are authoritative for each node.

Trust Assumptions & the Attestation Roadmap

Trust Model: This section is the deep-dive for the Attestor row in §1.3 Trust Model.

Each accepted TEE vendor is a root of trust for the attestations its hardware produces. Understanding that boundary is critical for evaluating Iris's security guarantees.

What the TEE vendor can do

What the TEE vendor cannot do

What a compromised attestation hardware could do

Attestation Trust Model — Phase Roadmap

The progression below decentralizes the vendor footprint, not the cryptographic trust per attestation (which is hardware-rooted from day one). The same IrisGovernance-controlled whitelist drives every phase; what changes is how many distinct vendors and measurements it admits. The "Phase 1–4" labels are local to this roadmap; their mapping to product releases is in the Product phase column. (See next_steps.md §Phase Roadmap for the canonical product taxonomy.)

PhaseAccepted attestorsProduct phaseDescription
Phase 0 — Mock attestor onlyMockAttestor signed by an ed25519 fixture root[Demo v0.0.1]Local devnet only. No real TEE hardware required. Mock tag excluded from any testnet/mainnet whitelist by construction. Default-on mock Cargo feature on iris-data. (crates/iris-data/src/attestor/mock.rs.)
Phase 1 — Multi-vendor whitelistAWS Nitro Enclaves, Intel SGX (DCAP), AMD SEV-SNP[MVP v0.1.0]The on-chain whitelist accepts attestations from any of three vendors from day one. Operators choose based on their hardware: Nitro for AWS-hosted nodes (lowest barrier to entry — Nitro Enclaves themselves are a $0 surcharge on EC2), SGX for owned Xeon-Scalable (Ice Lake-SP+) servers, SEV-SNP for owned EPYC Milan+ servers or bare-metal at OVH/Hetzner
Phase 2 — Add Intel TDXPhase 1 vendors + Intel TDX[V1 v1.0.0]Once Sapphire Rapids hardware is more broadly available and dcap-rs's TDX path stabilizes, add Intel TDX to the whitelist. TDX is operationally simpler than SGX (whole-VM rather than per-process attestation) and is the most operator-friendly path for Intel-based self-hosting. No protocol change — a whitelist append via IrisGovernance
Phase 3 — Cross-vendor quorum requirementPhase 2 vendors, with diversity rule[V1 v1.0.0]Tighten the consensus rule so that the BFT-quorum subset (the t > 2n/3 contributors whose signatures finalize a round) must span at least 2 distinct vendors. A round whose entire quorum runs on a single vendor is not finalized; the round either retries with a more diverse contributor set or falls back to the §8.8 failure path. Eliminates single-vendor-class-break risk without requiring honest-majority within any one vendor
Phase 4 — Threshold attestation (research)Phase 3 vendors + research-grade k-of-m schemes[V1+ post-v1]The longer-term direction, kept open as the §5 spec evolves: combine attestations from k of m independently-attested fetches into a single committee-level threshold attestation, eliminating any per-vendor trust at the cryptographic layer. Multiple research lines feed this slot — threshold-MPC TLS notarization (the original §5 direction), hardware-rooted threshold signatures across vendors, and proof-aggregation schemes over heterogeneous attestation formats. None is currently production-grade; the slot exists so the roadmap explicitly does not stop at vendor diversity
Bottom Line: From the MVP onward, Iris's first link in the Chain of Provenance is a hardware-vendor-signed attestation of the actual fetched bytes. The MVP threat boundary is a multi-vendor whitelist (Nitro + SGX + SEV-SNP); a single compromised vendor is absorbed by the BFT threshold (t > 2n/3); Phase 3 promotes vendor diversity from probabilistic to required at the quorum level. There is no third-party Notary, no hosted MPC service, no alpha-quality dependency on the critical path of MVP.

Operator hardware notes

For node operators who want to self-host rather than rent cloud TEEs:

Data Normalization Engine (The Reconstruction Pipeline)

Images received from different satellite constellations possess varying spectral bands, resolutions, and perspectives. Before the network can compare these images — before it can reconstruct a panel — they undergo a rigorous normalization pipeline implemented natively in Rust.

Orthorectification

Raw 2D imagery is projected onto a shared 3D Digital Elevation Model (DEM) — IrisDEM, the Foundation-maintained reference elevation product whose rollout tiers (D0 constant-plane for Phase-1 demo, M1 Copernicus GLO-30 for MVP, V1 Copernicus + NASADEM void-fill for v1.0, V1+ commercial/radar augmentation post-v1.0) are specified in data_normalization.html §IrisDEM. The DEM corrects for the satellite's viewing angle and the Earth's curvature, aligning all pixels into a unified target space ℝ^(a × N × M). The result: disparate images from different satellites now occupy the same coordinate grid and can be compared element-wise.

MVP implementation note ([3.6]). The reference implementation in iris-normalize::ortho is a pure-Rust per-pixel bilinear sampler with a parallax correction delta_row_px = h_terrain * tan(θ_view) / gsd_m_per_row applied at every output coordinate. The per-pixel terrain height h_terrain comes from the configured DemResampler trait — ConstantPlaneResampler at Demo, BilinearResampler at MVP, governed by the IrisDEM tier in force. Two entry points coexist: orthorectify_flat_earth (legacy reference, used by iris-demo synthetic-imagery paths and the [3.6.3] determinism test) and orthorectify_with_dem (the production default — public orthorectify dispatches here). When the resampler returns h = 0 everywhere, the two paths produce bit-identical output within consensus.matrix_tolerance so the migration is transparent for callers that haven't wired up real DEM data yet. Per-input sensor view zenith is plumbed alongside the [3.9.7] envelope check; until that lands the parallax correction uses DEFAULT_VIEW_ZENITH_DEG (10°, the Demo placeholder from architecture.md §2.3) as a safe MVP stand-in.
Why pure-Rust over GDAL FFI for the warp. The [3.3] GeoTIFF loader uses GDAL FFI (feature-gated) to handle real-world COGs with arbitrary compression / multi-band / BigTIFF — that's I/O complexity that's not worth re-implementing. The warp itself stays pure-Rust because the bilinear math is small, the determinism contract (bit-identical across nodes, see §6.2 determinism note) is easier to enforce when the float-op order is owned by Iris, and the GDAL warp API does not expose a clean per-pixel hook for the DemResampler seam without either dropping to C callbacks or pre-rasterizing the DEM into a sidecar dataset (both of which add operational fragility). The pure-Rust path is faster to audit and reproduce; the GDAL FFI dependency stays scoped to the loader. Revisit at V1+ if a calibration target needs a feature only GDAL provides (e.g. RPC-based projection per design_q1 §1.3.3.c).

Known Demo-tier concerns (not deferred — documented). Two limitations of the current optical warp are accepted at Demo and the §6.1.1 dispatch table is the load-bearing mitigation:

(a) Redundant ortho over already-orthorectified inputs. Sentinel-2 ships as L1C (TOA reflectance, orthorectified to UTM via ESA's collinearity pipeline against PlanetDEM/GLO-30) or L2A (same ortho + sen2cor BOA correction); both arrive already terrain-corrected. The Demo handler re-runs the parallax warp on top of this — correct to within DEM accuracy but redundant compute. The V1 follow-up is to branch on Manifest.processing_level and skip the DEM term when the vendor has already terrain-corrected (tracked at [3.6]).

(b) Optical-only scope; SAR is not handled. Sentinel-1 GRD/SLC arrive in slant-/ground-range geometry with σ⁰ backscatter radiometry — neither the collinearity-equation warp nor the reflectance assumptions hold. A contributor who hands the Demo handler S1 bytes would silently produce garbage if the dispatch table didn't intercept. The §6.1.1 registry fails closed on every (Sentinel1Sar, _, _) tuple with UnsupportedAtDemo; production SAR handling lands as a dedicated MVP track (Range-Doppler terrain correction, deburst, multilook, speckle filter — math chain entirely separate from optical).

Both limitations are visible to operators via the parameters.html §14.2.8 accepted-tuple matrix. New vendor / mission rows extend the matrix additively; the dispatch trait surface in §6.1.1 is the seam.

6.1.1 Preprocessing Handler Dispatch

The normalization entry point in iris-normalize does not assume a single preprocessing pipeline. Different missions need fundamentally different math — Sentinel-2 MSI (push-broom optical, vendor-orthorectified, reflectance) shares almost no machinery with Sentinel-1 SAR (range-coordinated, slant-range, backscatter). The pipeline therefore looks up a per-tuple handler against the declared (mission, processing_level, radiometric_units) from the Manifest (cryptographically bound into the attestation's user_data per §5.4 step 6, so the contributor cannot misdeclare and walk away).

fn select_preprocessor(
    mission:           Mission,
    processing_level:  ProcessingLevel,
    radiometric_units: RadiometricUnits,
) -> Result<&'static dyn Preprocessor, UnsupportedProduct>

At Demo the registry holds two active entries and two explicit fail-closed entries:

(mission, level, units)HandlerState at Demo
(Sentinel2Msi, L1C, Toa)optical_parallax_warp (existing [3.6] path)✅ active
(Sentinel2Msi, L2A, Boa)optical_parallax_warp (same path; redundant ortho per §6.1 concern (a))✅ active
(Sentinel1Sar, L1Grd, _)Err(UnsupportedAtDemo("Range-Doppler terrain correction; MVP track"))🟡 documented stub
(Sentinel1Sar, L1Slc, _)Err(UnsupportedAtDemo("deburst + Range-Doppler; MVP track"))🟡 documented stub
any other tupleErr(UnsupportedProduct)⚪ V1+

The registry is the only enforcement point. A contributor's Manifest declaring an unsupported tuple is dropped during Tier-1 verification with a Rejection { kind: "preprocessing" } rather than producing nonsense pixels. The matrix in parameters.html §14.2.8 is the operator-visible reference for which tuples are accepted on the current network version.

Similarity Metrics

The network computes three independent metrics on aligned tensor pairs (Ā, ):

Band-shape compatibility (MVP rule, [3.5]). Computing any of the three metrics on a pair of tensors with mismatched spectral bands produces statistical noise rather than a meaningful similarity score — adding a Sentinel L1C visible band to a Maxar WV3 NIR band gives a number, but the number doesn't measure what we want. The MVP rule is the simplest defensible answer: a pair of AlignedImages is comparable iff (a) both have the same band count, and (b) per-band centre wavelengths agree within normalization.band_tolerance_nm (default 5 nm — comfortably wider than typical Sentinel-2 vs Landsat 8 visible-band offsets, narrow enough to reject visible-vs-NIR pairings as a configuration error). The [3.5.1] assert_band_shape_compatible helper enforces this as a precondition inside compare(); the resulting NormalizeError::BandMismatch carries a per-band breakdown that flows up into Tier-1 Rejection diagnostics. The harder cross-vendor band-mapping problem (the data_normalization.html "Non-Intersecting Band Problem" / "Subset Band Problem") is post-MVP — production deployments today configure their committee to ingest from constellations with overlapping spectral bands.
Determinism contract ([3.5.4] + [3.5.5]). Every reduction in MAD/MSE/SAM must produce bit-identical results across architectures (within consensus.matrix_tolerance, default 1e-6) — otherwise the §7.4 step-2 self-edge check fails on heterogeneous fleets. The implementation guarantees this via: (1) no rayon parallelism inside per-cell reductions (parallel f64 summation isn't associative); (2) fixed row-major iteration order via ndarray::iter().zip(); (3) f64 accumulators over f32 inputs; (4) acos() clamped to [-1, 1] against floating-point overshoot; (5) identical-vector short-circuit before the arccos(1 - epsilon) round-off boundary. The cross-arch-determinism.yml CI workflow runs a fixed-seed fingerprint test on x86_64-unknown-linux-gnu AND aarch64-unknown-linux-gnu and rejects PRs whose matrices diverge.

Similarity Scoring

The three metrics are combined into a single Similarity Score via a physics-based exponential decay function:

𝒮(μ) = 100 · e^(−(β₁μ₁ + β₂μ₂ + β₃μ₃))

The β tuning vector controls how aggressively each type of error destroys the similarity score. Defaults are defined in config and empirically tuned during testnet.

Average Scenario (Medoid) Selection

Given n observations from n nodes, the Leader Node computes all C(n, 2) pairwise Similarity Scores and builds a similarity matrix. Rather than mathematically averaging the pixels to create a synthetic composite image (which would destroy the cryptographic Chain of Provenance), the Leader Node uses the matrix to find the Medoid — it selects the single, existing image with the highest mean similarity to all others.

This selected image — termed the Average Scenario — becomes the panel's canonical reconstruction. Because the Average Scenario is an original, unaltered payload fetched by one of the nodes, it inherently retains its original TLS proof, allowing the rest of the network to verify its authenticity.

Note: For the complete mathematical formulation and definitions of the tensors used in this pipeline, refer to the Data Normalization Specification.

Consensus Engine (Iris-BFT)

The Iris-BFT consensus drives the Round State Machine (Section 3.1). Its role is to coordinate the network through the observation, aggregation, and signing phases for each panel reconstruction.

Leader Election

A deterministic, stake-weighted round-robin algorithm selects the Leader Node for each round:

leader_index = keccak256(block_hash ‖ request_id) % total_stake

Keccak-256 is specified here — not BLAKE2b — because this computation must be reproducible in smart contracts. Keccak-256 is EVM-native and available as a cheap precompile on all target chains; off-chain, tiny-keccak produces the same result in Rust. The input is always small (~64 bytes), so Keccak-256's slower throughput on large payloads is irrelevant here.

The index is mapped to the node whose cumulative stake range covers that value. Because the inputs (block hash, request ID) are publicly known, every node independently computes the same Leader Node without extra communication.

Why Stake-Weighted is Safe Here: While a node with more stake will be elected Leader Node more frequently, the Leader Node possesses zero subjective power. The Leader Node cannot "smudge" the results or provide a sub-optimal average because the normalization pipeline and Average Scenario selection are 100% deterministic (see Section 6.4). If a wealthy Leader Node attempts to propose an image that isn't the exact mathematical winner, the Regular Nodes will compute the discrepancy during their independent verification and reject the proposal. The Leader Node is merely a designated worker to handle the IPFS pinning and proposal broadcasting, not a dictator of the truth.

7.1.1 Election Inputs & Cross-Chain Compatibility

InputDefinition (EVM MVP)Where it comes from
block_hashThe Keccak-256 block-header hash of the host-chain block in which the DataRequest event was emittedEach committee member runs a host-chain light client and reads the block header directly; every node sees the same value once the block is finalized
request_idbytes32 identifier of the on-chain DataRequest eventGenerated by the Requester contract; included in the event payload that the Relayer transports to GossipSub
total_stakeSum of all stake_weight values in the active committee (§3.4)Read from the latest IrisStaking snapshot referenced by the current epoch's committeeHash (§8.3); consistent for all nodes within an epoch

For MVP-target EVM chains (Ethereum, Polygon, Arbitrum, Optimism, Base), block_hash is well-defined and reproducible inside IrisVerifier. For future non-EVM host chains (Solana, Cosmos, NEAR, Bitcoin), the formula's block_hash input must be replaced by a chain-appropriate finalized random beacon — Solana's recent_blockhash, a Cosmos VRF output, Bitcoin's block-header hash for the corresponding block, etc. The deployment-specific adapter is responsible for producing a value that (a) is uniform-random from the perspective of the Iris committee, (b) is committed before the Requester's DataRequest is included in a block, and (c) is reproducibly readable by every node observing that chain's state. Non-EVM deployments are out of scope for the MVP and are flagged in the multi-chain roadmap (review item 4).

7.1.2 Block-Hash Grinding

A malicious host-chain validator producing the block in which a DataRequest event lands could attempt to grind the block hash — repeatedly varying nonce / extraData / transaction ordering to bias leader_index toward a colluding node. The grinding itself is computationally cheap (each trial is one Keccak hash), and a typical Ethereum validator has perhaps 10–20 bits of effective control per slot via legal block-construction choices. Iris's defense is therefore not "grinding is hard" — it's "grinding has no positive payoff":

  1. The Leader has zero subjective power (§7.1 preamble + §7.4 caveat). A forged proposal is rejected by > 2n/3 Regular Nodes; the only attack a colluding Leader Node can mount is sub-optimal-medoid selection, bounded by the §7.4 Tier 2 spot-check probability and the post-hoc fraud-proof recourse (submitFraudProof(BadMedoid) per §8.8).
  2. Being Leader is a net cost. A round Leader Node bears extra bandwidth (pulling all contributors' GeoTIFFs), CPU (running the full normalization pipeline), and IPFS pinning costs, while receiving the same per-round fee share as any other contributing committee member (§6.3 similarity-score weighting). There is no "leader fee." Grinding into the Leader role grinds the colluder's own costs upward without granting it extra revenue; grinding a target into the Leader role for griefing imposes ≤$100 of Leader-specific costs per round (16 GB worst case), well below the validator's MEV opportunity cost on a serious chain.
  3. Stake-weighted election already grants natural skew. If a colluder controls fraction f of total stake, they are already elected with probability f per round under honest randomness. Grinding can lift that to ~f + ε, but only at marginal benefit on top of the existing stake-weight share.

Phase 2 mitigation (planned). When the active set crosses ~64 nodes — at which point even small grinding leverage could be combined with high-value §7.4 attacks against specific committee compositions — the network will transition block_hash from host-chain randomness to a committee-generated random beacon: each round, a fresh BLS threshold signature over keccak256("iris/beacon/v1" ‖ epoch ‖ round_number) is produced by the active committee itself and used as the leader-election seed. This eliminates host-chain validator influence entirely, at the cost of one extra threshold-signing operation per round. Until then, the host-chain block_hash path is documented as the bootstrapping compromise.

7.1.3 Leader Failure & Round Failure

A Leader Node can fail in exactly one architecturally observable way after election: failing to broadcast a Proposal to iris/consensus/v1 within proposal_deadline_seconds (default 240s after Phase 2 closes). This deadline covers GeoTIFF transfer (~130s for typical 1–4 GB payloads at 1 Gbps Leader ingress), tensor normalization, medoid selection, and IPFS pinning. Sub-failures — refusing to open /iris/geotiff/1.0.0 streams, failing the normalization pipeline, failing to pin to IPFS — all manifest externally as no Proposal matching request_id being observed.

For deployments expecting 16 GB worst-case payloads with high contributor counts, both proposal_deadline_seconds and the §8.7 T1 value MUST be raised in tandem via governance — the architecture's invariant is T1 ≥ manifest_deadline_seconds + proposal_deadline_seconds + voting_timeout_seconds + chain_finality_margin, so the on-chain timeout fallback never fires before the off-chain round can legitimately complete.

MVP behavior — single-shot fail. When proposal_deadline_seconds elapses without a matching Proposal, every node finalizes the round as Failed:

  1. Each node updates its local round record to state = Failed with the elapsed-deadline reason.
  2. The originally-elected Leader Node incurs a reputation decrement equivalent to a missed BLSShare contribution (§8.7 reputation inputs). Pattern-detected repeated failures cross the slashing threshold under the proposed Leader-equivocation rule in threat_model.html §3.1.
  3. The Requester is notified by the absence of a ReportDelivered event before requestSubmissionDeadline = blockTimestamp + T1 (§8.7); after that deadline the Requester (or any reputation-qualified address from Phase 2 onward) can re-submit a fresh DataRequest for the same AoI. Re-submission produces a new request_id and a new leader election with high probability of selecting a different Leader Node, because block_hash is drawn from a later block.
  4. Committee members who delivered valid Phase 1 / Phase 2 messages receive partial fee credit for the failed round, prorated against the no-Leader-fee outcome (per the schedule in governance.html); the failed Leader Node receives nothing.

Recovery completeness after Proposal broadcast. Once the Leader Node has broadcast a valid Proposal, its continued participation is no longer required for round completion: any committee member can independently verify the proposal, contribute its BLS share, observe the threshold-signed report on gossip, and (subject to §8.7 access rules) submit the on-chain deliverReport. The Leader's IPFS pin is the only Leader-unique resource, and the proposed CID is content-addressable — any node holding the underlying GeoTIFF can re-pin it. This makes the Voting and Commit phases robust to Leader disappearance after PreCommit.

Why no view-change in MVP. A view-change protocol — pre-electing top-k candidate Leaders and rotating to the next on timeout — would extend a worst-case round duration by up to (k − 1) · proposal_deadline_seconds and complicate reputation attribution (which Leader gets penalized when?). Single-shot fail keeps round duration bounded and lets the Requester retry rather than wait through escalating view-change rounds.

Phase 2 enhancement — bounded view-change (planned). A future upgrade can pre-compute the top-k candidate leaders via leader_index_r = keccak256(block_hash ‖ request_id ‖ r) mod total_stake for r ∈ {0, 1, …, k−1}, rotating to the next candidate on a single Leader timeout without requiring Requester re-submission. The proposed cap is k = 3. This is deferred to post-MVP because it is not load-bearing while round durations are dominated by GeoTIFF transfer and the Foundation Relayer's uptime SLA covers most observed failure modes.

Observation Window

The observation window is split into two sequential deadlines. This allows the Leader Node to detect non-participating nodes early and fail the round fast — before resources are spent downloading multi-GB payloads — rather than waiting the full window for nodes that were never going to contribute.

Phase 1 — TLS Commitment (default: 60 seconds)

Within 60 seconds of the DataRequest being received, each Regular Node must:

  1. Spawn its iris-fetch TEE enclave with the request parameters
  2. Have the enclave complete the TLS handshake with the Data Provider API
  3. Publish a lightweight TlsCommitment message to iris/observations/v1 containing {request_id, provider, bounding_box, tls_session_id, attestor, enclave_pubkey, node_signature} (where attestor: Attestor tags the vendor and enclave_pubkey is the ephemeral key the enclave will sign its request record with)

The TlsCommitment proves the node is actively participating and that the in-enclave TLS authentication with the Data Provider succeeded. It does not require the payload to be downloaded yet — only that the enclave is live, measured, and has completed the TLS handshake to the Provider.

Why 60s: This covers typical Data Provider API authentication latency (5–30s) plus enclave bootstrap (~1–5s on Nitro, ~10–20s on first-time SGX with new collateral fetch), with headroom for slower nodes. Empirically validated against AWS Nitro and Intel DCAP in [2.4].

Nodes that fail to publish a TlsCommitment within 60s are excluded from the round immediately. The Leader Node does not request their GeoTIFF during the Aggregating phase and their GossipSub peer score is penalized. If the remaining committed nodes fall below the threshold t, the round fails early — saving all nodes the cost of the Aggregating phase.

Phase 2 — Manifest Finalization (default: 300 seconds from round start)

Within 300 seconds of the DataRequest, each node that published a valid Phase 1 TlsCommitment must complete the full pipeline and publish a final Manifest to iris/observations/v1 containing {request_id, image_hash, bounding_box, timestamp, attestation_hash, mission, processing_level, radiometric_units, node_signature}. The mission / processing_level / radiometric_units triple is the contributor's declaration of what was fetched — cryptographically bound into the attestation's user_data (§5.3 / §5.4 step 6) and consumed by the §6.1.1 preprocessing dispatch table.

This requires the enclave to finish downloading the full GeoTIFF payload (with a streaming BLAKE2b-512 hasher computing image_hash in-enclave), to call the platform attestation API binding image_hash into user_data, and for the host process to persist both the GeoTIFF and the attestation document and compute attestation_hash = BLAKE2b(att_doc_bytes).

Why 300s: At the minimum specified node connection (1 Gbps / 125 MB/s), a 16 GB payload takes ~130s to download plus ~24s to hash. 300s provides headroom for API transfer variability while bounding the maximum round duration. Operators on slower connections may raise this via manifest_deadline_seconds in iris.toml (with the matching Phase 1 key being observation_phase1_deadline_seconds and the body sub-budget being geotiff_body_deadline_seconds, both renamed/added in [4.5.17]).

A Phase 1 node that fails to deliver its Phase 2 manifest by the 300s deadline is treated as a non-contributor: its slot is dropped from the aggregation pool and it incurs a GossipSub score penalty. If the remaining finalized manifests fall below t, the round fails at this point rather than proceeding to Aggregating with insufficient data.

Full GeoTIFFs are never gossiped — only the lightweight TlsCommitment and Manifest messages transit GossipSub. The Leader Node pulls the actual payloads via direct libp2p streams during the Aggregating phase.

Single-provider commitment per round

Each Regular Node publishes exactly one TlsCommitment and exactly one Manifest per round, naming exactly one Data Provider (the provider: Provider field on both messages is a single value, not a set; the Round struct in §3.1 tracks one of each via my_tls_commitment and my_manifest). A node may concurrently attempt the in-enclave TLS handshake against multiple enabled providers during Phase 1 to improve liveness on slow / unavailable upstreams, but as soon as one handshake completes the others must be cancelled — once the TlsCommitment names provider X, Phase 2 cannot switch to provider Y without violating the 60s deadline contract. Multi-provider diversity therefore arises across the committee (different nodes, different operators, different enabled sets in iris.toml), not within a single node's round, and is the basis for threat model §2.3 Data Provider response spoofing being absorbed by the consensus medoid.

Signing scheme for TlsCommitment and Manifest

Both message types carry a node_signature: Ed25519Sig field. To make signatures interoperable across implementations, the canonical signing input is fixed:

node_signature = Ed25519Sign(
    NodeIdentity.secret_key,
    BLAKE2b(ciborium::encode(<message struct with node_signature field omitted>))
)

That is: serialize the message body via ciborium (CBOR, the same encoding already used at the GossipSub wire-format layer per §4.4), with the signature field absent from the serialized struct (not zeroed — omitted); take the BLAKE2b-512 digest; sign the digest with the node's ed25519 secret key. Verifiers reconstruct the same digest and check the signature against the node's PeerId-derived public key. This scheme avoids the "zero-out and rewrite" pattern (which is fragile under field reordering) and reuses the same hash-and-sign primitives the protocol already depends on. The iris-tools MockAttestor fixture generator (crates/iris-tools/src/attestation_fixture.rs) follows the same pattern.

Aggregation & Proposal

The Leader Node collects manifests, retrieves full GeoTIFFs via direct streams, runs the normalization pipeline, selects the Average Scenario, pins it to IPFS, and broadcasts a Proposal containing the CID and similarity evidence.

Verification & Signing

A naïve verification scheme — every node downloads every other node's GeoTIFF and recomputes the full similarity matrix — costs O(n²) aggregate bandwidth and O(n³) aggregate compute, which is prohibitive at multi-GB payloads. A pure O(1) shortcut where each node verifies only its own edge of the similarity graph is cheap but lets a colluding Leader Node propose a sub-optimal medoid as long as the chosen image scores above the acceptance threshold for > 2n/3 nodes (the original review-item-2.2 attack). Iris splits the difference with a two-tier verification scheme: every node performs O(1) mandatory checks; each node also performs O(k) probabilistic spot-checks with k small (default k = 1). Cumulatively the network covers the full similarity matrix with high probability without any single node bearing O(n) work.

What the Leader publishes

The Proposal broadcast on iris/consensus/v1 carries the full pairwise similarity matrix in addition to the chosen medoid:

struct Proposal {
    request_id:          RequestId,
    selected_image_hash: ContentHash,         // hash of the chosen Average Scenario
    selected_index:      u8,                  // row of M corresponding to the medoid
    ipfs_cid:            Cid,                 // pinned panel directory (GeoTIFF + metadata)
    contributors:        Vec<ContributorId>,  // n PeerIds in matrix-row order
    similarity_matrix:   Vec<f64>,            // upper-triangular n(n-1)/2 entries
    leader_signature:    ed25519::Signature,
}

For a 20-node committee this adds ~1.5 KB to the Proposal (190 unique entries × 8 bytes); at the upper bound of 100 nodes the matrix is ~40 KB, well within GossipSub's per-message budget. For larger committees (>~150) the matrix can be anchored to IPFS instead, with the Proposal carrying only the matrix CID — but this is forward-looking and not required for MVP committee sizes.

Tier 1 — Mandatory checks (every Regular Node, O(1) bandwidth)

Every node performs the following before broadcasting a BLS share. Any failure produces a Rejection instead of a share, with the failure reason attached.

  1. Attestation provenance. The TEE attestation document for selected_image_hash is fetched (from the Leader Node or from a peer cache via /iris/attestation/1.0.0) and verified per §5.4.
  2. Self-edge correctness. The node fetches the proposed GeoTIFF via /iris/geotiff/1.0.0, runs the normalization pipeline against its own local image, and computes its own similarity score. This must equal the matrix entry M[me][selected_index] within matrix_tolerance (default 10⁻⁶, well above floating-point round-off), and must exceed acceptance_threshold.
  3. Medoid correctness. The node verifies that selected_index corresponds to the row of M with the highest mean similarity — the medoid by definition (§6.4). This is a pure compute step on the published matrix; no extra fetching required.
  4. Contributor-set integrity. The node verifies that contributors matches the set of nodes that delivered valid Phase 2 manifests for request_id, and that the matrix dimensions agree.

Step 3 alone catches the simplest sub-optimal-medoid attack: a Leader Node who publishes an honest matrix but claims a non-medoid row is rejected by every honest node without any extra GeoTIFF fetching. Steps 1, 2, and 4 catch forgery, self-edge tampering, and contributor-set tampering respectively.

Tier 2 — Probabilistic spot-checks (every Regular Node, O(k) bandwidth)

After Tier 1, each node selects k = spot_check_count (default k = 1) other contributors via a deterministic PRF that the Leader Node cannot precompute against:

spot_i = keccak256(request_id ‖ my_peer_id ‖ "spot-check" ‖ i) mod n    for i ∈ {0, …, k−1}

For each spot_i that is neither the node's own index nor selected_index:

  1. Fetch contributor spot_i's GeoTIFF via /iris/geotiff/1.0.0.
  2. Run the normalization pipeline against the node's own local image.
  3. Verify the resulting similarity matches M[me][spot_i] within matrix_tolerance.

A discrepancy beyond tolerance yields a Rejection carrying the offending cell coordinates, the node's recomputed value, and the node's own GeoTIFF hash — together a self-contained fraud-proof seed.

Coverage analysis. With n = 20 and k = 1, each round randomly samples 20 off-diagonal cells out of C(n, 2) = 190. Probability of detecting any single fraudulent matrix entry per round is ≈ 20/190 ≈ 10.5%. A Leader Node attempting to consistently fabricate the same fraudulent entry faces compounding detection probability 1 − 0.895^r over r rounds — exceeding 99% by round 42. Operators may raise spot_check_count for higher-value deployments at proportional bandwidth cost (one extra GeoTIFF fetch per spot-check per round).

The deterministic spot-target derivation uses request_id (fixed before the round started) and the verifying node's peer_id (fixed at identity generation). The Leader Node cannot precompute which cells will be checked by whom and therefore cannot construct a matrix whose fabrications happen to fall outside the spot-check coverage.

Fraud-proof recourse

The committed similarity_matrix becomes part of the on-chain panel record (anchored via IPFS in the panel directory and hashed into the on-chain CID). After the round finalizes, any third party with access to the contributors' original GeoTIFFs — committee members, archivers, downstream application auditors — can recompute any matrix entry off-line. A discrepancy is submittable as a fraud proof via submitFraudProof(requestId, FraudType.BadMedoid, evidence), causing the Leader's stake to be slashed via IrisStaking and their reputation zeroed. The on-chain interface, evidence schema, and gas budget are specified in §8.8; the threat-model framing is in threat_model.html §3.2.

This raises the cost of consistent matrix fabrication arbitrarily: a Leader Node cannot be confident their lie won't be caught a year later by a determined auditor. Combined with the reputation system (§8.7), even probabilistic detection at consensus time accumulates into chronic-Leader penalties — repeated near-misses in the same direction are themselves statistically detectable.

Bounded-payoff caveat. Even when a sub-optimal-medoid attack escapes both real-time spot-checking and post-hoc fraud-proofing, the chosen image must by construction score above acceptance_threshold for > 2n/3 nodes — meaning it is near-optimal in absolute terms. The economic value of swapping image A for image B when both pass the threshold is bounded by within-threshold variance in 𝒮(μ), which for sensible threshold values (0.85–0.95 on the [0, 100] scale of §6.3) is small relative to the value of the underlying GIS data. Applications with extreme sensitivity to within-threshold variation (e.g., adversarial environmental monitoring or high-stakes ground-truth tokenization) should configure a higher acceptance_threshold and/or a larger spot_check_count.

The Leader Node collects t > ⌊2n/3⌋ shares and aggregates them into a single threshold signature.

Wire-Format Input Discipline

Tier-1 verification (iris-normalize::verify) consumes data that arrives over GossipSub from an untrusted Leader: the Proposal.similarity_matrix upper-triangle flattening, Proposal.contributors, Proposal.selected_index. A malicious Leader Node can produce any byte pattern that survives CBOR decoding — including length / index combinations that don't correspond to any real n × n matrix. The verifier's contract is "every malformed input becomes a typed Rejection reason; no malformed input crashes a Regular Node." Three patterns apply at every wire-format boundary:

  1. Indexed access uses .get(idx).copied(), never slice[idx]. A Proposal whose similarity_matrix is shorter than n*(n-1)/2 is a malformed proposal, not a panic. matrix_lookup returns Option<f64> and the caller maps None to a ContributorSetMismatch rejection. The same rule applies to any handler that reads a CBOR-decoded Vec<T> field by computed index — serde can produce a length the encoder swore was correct.
  2. Index parameters get bounds-checked against n before they reach lookup helpers. verify_self_edge, verify_spot_cell, and the medoid-pick verifier all check my_index < n, selected_index < n, and (i, j) < n at function entry. Pushing the bounds check down into matrix_lookup preserves panic-safety but loses caller-side attribution (a None could mean "out-of-bounds index" or "short matrix"; the caller must distinguish so the Rejection.reason is accurate).
  3. Slice-length invariants are validated against the declared shape, not the expected shape. A 4-contributor matrix should have 4*3/2 = 6 entries. The check is upper_triangle.len() == n * (n-1) / 2 where n = proposed_contributors.len() — using the proposed n rather than a hard-coded constant, so the rejection variant carries the actual length the verifier saw. Helpers error out before doing any arithmetic on the wire-format slice if its length doesn't match the declared committee size.

These patterns collectively turn "the Leader is malicious" from a panic-vector into a rejection-stream: a Regular Node that hits any of these conditions emits a Rejection with the corresponding Tier1Failure variant and proceeds to the next round; nothing crashes the node or stalls the committee.

Threshold Cryptography

Smart Contract Integration

While the heavy computation (fetching, TEE attestation, tensor comparisons) happens off-chain, the final results must be verifiable on-chain for Requester dapps to consume. The on-chain layer is intentionally minimal: it stores the committee's aggregate public key, verifies BLS threshold signatures over finalized panels, anchors (request_id, ipfs_cid) pairs in the chain's event log, and forwards results to consuming Requester contracts. All other state — observations, GeoTIFF payloads, similarity matrices, the attestation documents themselves — lives off-chain in IPFS and node caches.

MVP scope — Ethereum testnet only. The Iris Protocol has no production budget at MVP launch. Deploying to mainnet or paying for multi-chain support is out of scope. The MVP targets an Ethereum testnet (Sepolia or Holesky) on Pectra+, which provides native EIP-2537 BLS12-381 precompiles, deterministic finality, free testnet ETH for Relayer gas, and the same EVM semantics the protocol will eventually run on at mainnet. L2s (Arbitrum, Optimism, Base) and alternative L1s (Polygon PoS, Avalanche) are deferred to post-MVP — they would re-introduce a verifier abstraction layer (library fallback for chains without EIP-2537), per-chain finality margin tuning, and gas-pricing variance, none of which are load-bearing for a budget-constrained MVP whose goal is to validate the consensus + provenance design end-to-end. Every contract design choice in this section is sized for the single-testnet target.

This section specifies that on-chain surface in enough detail to be implementable: the contract inventory (§8.1), the IrisVerifier interface (§8.2), how committees rotate without ever crossing the BFT trust boundary (§8.3), how BLS signature verification uses the EIP-2537 precompile on the MVP target (§8.4), how the Requester push callback isolates buggy consumer contracts (§8.5), indicative gas targets that size the relay-fee schedule (§8.6), the access-control / decentralization roadmap for the Relayer role (§8.7), and the on-chain round-failure surface (§8.8). Bytecode-level decisions (storage slot packing, proxy patterns, inline-assembly optimizations) are deferred to a dedicated smart_contracts.md spec.

External services such as Chainlink DONs that wish to consume Iris data without implementing IIrisReceiver can do so via the pull model: subscribe to ReportDelivered events and call claimReport(requestId) to retrieve the IPFS CID — see §8.5.

Contract Inventory

The Iris on-chain layer comprises three protocol-deployed contracts and one interface that Requester dapps implement:

ContractRoleTrust Surface
IrisVerifierStores the committee aggregate key & epoch; verifies deliverReport BLS signatures; anchors finalized (request_id, ipfs_cid) pairs; forwards results to IIrisReceiver. The single canonical contract on the MVP target chain — every Requester interacts with this one.Trustless. Security follows from BLS verification + committee threshold signing (§7.5)
IrisStakingCustodies IRIS token stakes; emits Staked / Unstaked events that the off-chain DKG ceremony (§3.4) and IrisVerifier.updateCommittee honor; executes slashing when proven misbehavior is reported per threat_model.html §5.Trustless once governance is decentralized; trusted owner during MVP for parameter tuning
IrisGovernanceApproves committee membership changes, governance-tunable parameters (reputation threshold, fee schedule, slash amounts), and contract upgrades.Trusted multisig during MVP; transitions to token governance per the roadmap in governance.html
IIrisReceiver (interface only — not deployed)Implemented by Requester dapp contracts that wish to receive a push callback when their request is fulfilled. Every Requester deploys their own.Out of scope. Requester is responsible for the safety of their own callback handler

The three protocol contracts are deployed once on the MVP target chain (Sepolia or Holesky). IrisVerifier is the only one Requester dapps interact with directly during normal operation; the other two are management surfaces for committee operators and governance participants. Multi-chain expansion would re-deploy the same trio per added chain, but that is post-MVP.

IrisVerifier Interface

The minimum implementable interface for IrisVerifier. Concrete implementations may add storage layout optimizations, proxy upgradeability, and chain-specific tuning, but the externally-visible function signatures, events, and errors below are normative.

interface IIrisVerifier {
    /* ─── Events ─── */

    /// Emitted when a finalized panel report is recorded on-chain.
    event ReportDelivered(
        bytes32 indexed requestId,
        bytes   ipfsCid,
        uint256 epoch,
        address relayer,
        uint8   contributorCount,  // nodes that submitted a valid BLS share this round
        uint8   thresholdUsed      // t value active for this epoch; ratio = health signal
    );

    /// Emitted when a round fails after requestSubmissionDeadline elapses.
    /// reason is advisory (set by the caller); not BFT-verified in MVP.
    /// See §8.8 and error_handling_strategies.md.
    event RoundFailed(
        bytes32 indexed requestId,
        uint8           reason,    // FailureReason enum value
        address         reportedBy
    );

    /// Emitted when the IIrisReceiver callback succeeded.
    event CallbackSucceeded(bytes32 indexed requestId, address requester);

    /// Emitted when the IIrisReceiver callback reverted; the report is still
    /// recorded on-chain and recoverable via claimReport (see §8.5).
    event CallbackFailed(bytes32 indexed requestId, address requester, bytes reason);

    /// Emitted on each successful committee rotation (see §8.3).
    event CommitteeRotated(uint256 indexed newEpoch, bytes newAggregateKey);

    /// Emitted when a TTL-triggered scheduled rotation pays the rotation bounty
    /// to the address that submitted updateCommittee. See §8.3.
    event RotationBountyClaimed(uint256 indexed epoch, address caller, uint256 bountyWei);

    /// Emitted when the active committee commits a new reputation Merkle root.
    event ReputationRootCommitted(uint256 indexed epoch, bytes32 root);

    /// Emitted when a panel's persistence window is extended (see §3.3.1).
    event PersistenceExtended(bytes32 indexed requestId, uint256 newExpiresAt, address fundedBy);

    /* ─── Errors ─── */

    error InvalidSignature();
    error UnknownRequest(bytes32 requestId);
    error DuplicateReport(bytes32 requestId);
    error DeadlineNotReached(bytes32 requestId, uint256 deadline, uint256 current);
    error RoundAlreadyFinalized(bytes32 requestId);
    error EpochMismatch(uint256 expected, uint256 actual);
    error RelayerNotAuthorized(address sender);
    error MerkleProofFailed();
    error ReputationBelowThreshold(uint256 score, uint256 threshold);
    error RotationAlreadyInitialized();
    error PersistenceExtensionExpired(bytes32 requestId);
    error ProtocolVersionRegression(uint32 current, uint32 attempted);

    /* ─── Read-only state ─── */

    function aggregateKey() external view returns (bytes memory);
    function currentEpoch() external view returns (uint256);
    /// Minimum protocol version nodes must run to be eligible for committee membership
    /// in the current epoch. Format: packed uint32 as (major << 16 | minor << 8 | patch).
    function minProtocolVersion() external view returns (uint32);
    /// Non-zero when a governance vote has scheduled an epoch transition but
    /// updateCommittee has not yet been called for that epoch.
    function pendingEpoch() external view returns (uint64);
    /// Seconds after epochLastTransitionAt before a scheduled re-key is valid.
    /// 0 = scheduled rotation disabled. Governance-tunable. See §8.3.
    function epochRotationTtl() external view returns (uint256);
    /// block.timestamp of the most recent updateCommittee call.
    /// Scheduled rotation window opens at epochLastTransitionAt + epochRotationTtl.
    function epochLastTransitionAt() external view returns (uint256);
    /// Wei paid from the protocol treasury to the caller of a TTL-triggered
    /// updateCommittee. 0 in Phase 1 (Foundation pays directly). See §8.3.
    function epochRotationBounty() external view returns (uint256);
    function reports(bytes32 requestId) external view returns (
        bytes memory ipfsCid,
        uint256 epoch,
        address relayer,
        bool callbackSucceeded,
        uint256 panelExpiresAt   // §3.3.1: warm-layer reachability commitment expires at this timestamp
    );
    function reputationRoot() external view returns (bytes32);
    function requestSubmissionDeadline(bytes32 requestId) external view returns (uint256);
    function foundationRelayer() external view returns (address);

    /* ─── Mutating entry points ─── */

    /// Records a finalized report, verifies the BLS threshold signature, and
    /// invokes IIrisReceiver. Reverts with InvalidSignature, RelayerNotAuthorized,
    /// DuplicateReport, MerkleProofFailed, or ReputationBelowThreshold per the
    /// access rules defined in §8.7.
    function deliverReport(
        bytes32 requestId,
        bytes calldata ipfsCid,
        bytes calldata blsSignature,
        bytes calldata reputationProof  // Merkle proof of relayer reputation; empty in Phase 1
    ) external;

    /// Pulls the recorded report when the push callback failed (or the
    /// Requester never implemented IIrisReceiver). Authorization: caller
    /// must be the Requester contract that originated the DataRequest.
    /// See §8.5.
    function claimReport(bytes32 requestId) external returns (bytes memory ipfsCid);

    /// Top up the persistence fund for a previously-delivered panel,
    /// extending panelExpiresAt[requestId]. Anyone can call. msg.value
    /// converts to additional persistence time at the current
    /// persistence_price_per_gb_year rate. Reverts if the panel is past
    /// expiry by more than extension_grace_period_seconds. See §3.3.1.
    function extendPersistence(bytes32 requestId, uint256 additionalSeconds) external payable;

    /// Rotates the active committee and optionally bumps the protocol version floor.
    /// The OLD committee threshold-signs a rotation message containing
    /// (newEpoch, newAggregateKey, committeeHash, newMinProtocolVersion).
    /// Verifies the signature against the current aggregate key and atomically
    /// replaces the committee state. Reverts with EpochMismatch if
    /// newEpoch != currentEpoch + 1. Reverts with ProtocolVersionRegression if
    /// newMinProtocolVersion < minProtocolVersion. See §8.3.
    function updateCommittee(
        uint256 newEpoch,
        bytes calldata newAggregateKey,
        bytes32 committeeHash,
        uint32 newMinProtocolVersion,
        bytes calldata blsSignature
    ) external;

    /// Commits a new reputation Merkle root. Authorization: BLS threshold
    /// signature from the active committee. Cadence: aligned with epoch
    /// boundaries (§3.4).
    function commitReputationRoot(
        bytes32 root,
        bytes calldata blsSignature
    ) external;

    /// One-shot bootstrap, callable only by the deployer / governance multisig.
    /// Reverts with RotationAlreadyInitialized after the first call. The
    /// minProtocolVersion0 argument pins the genesis version floor; from epoch 1
    /// onward it is monotonically non-decreasing per ProtocolVersionRegression.
    /// See §8.3.
    function initializeCommittee(
        uint256 epoch0,
        bytes calldata aggregateKey0,
        bytes32 committeeHash0,
        uint32 minProtocolVersion0
    ) external;

    /// Records a round failure after requestSubmissionDeadline elapses.
    /// Emits RoundFailed, triggers partial fee refund to the Requester (fee
    /// minus contributor credits described by contributorCreditsRoot).
    /// Authorization: round participants only during the first 120s after
    /// deadline; any address permissionlessly after that. See §8.8.
    /// Reverts with DeadlineNotReached or RoundAlreadyFinalized.
    function reportFailure(
        bytes32 requestId,
        uint8   reason,                  // FailureReason enum value
        bytes32 contributorCreditsRoot   // merkle root of (address, creditWei) pairs
    ) external;

    /// Submits cryptographic evidence of attributable misbehavior, triggering
    /// a stake slash via IrisStaking. Phase 2+ only. See §8.8.
    /// fraudType 0 = BadMedoid, 1 = LeaderEquivocation.
    function submitFraudProof(
        bytes32 requestId,
        uint8   fraudType,
        bytes calldata evidence
    ) external;
}

Replay protection. reports[requestId] is the canonical de-dup gate: any second deliverReport for the same requestId reverts with DuplicateReport, regardless of which Relayer submits. Combined with the BLS signature committing to the exact (requestId, ipfsCid) pair, this prevents both replay and CID substitution.

Domain-separated message digests. All BLS signatures verified by this contract are computed over a domain-separated digest: BLS_HASH("iris/v" || epoch_number || "/" || function_selector || abi.encode(arguments)). The epoch number is encoded as a decimal ASCII string (e.g., epoch 3 produces "iris/v3/"). The function_selector byte ensures a deliverReport signature cannot be replayed as a commitReputationRoot signature, even if their argument encodings happened to collide. The epoch prefix ensures signatures from epoch N are invalid against epoch N+1's aggregate key, preventing cross-epoch replay attacks even if a compromised old committee leaks its private shares. Historical panels remain verifiable by reading IrisVerifier.currentEpoch() at the block height the panel was committed.

Committee Updates & Epoch Rotation

Epoch Triggers

A new epoch is created by exactly one of three events, all routed through IrisGovernance:

TriggerInitiatorGovernance gateEffect
Committee membership changeAny staking member via IrisGovernance.proposeCommitteeChange()≥67% voteNew DKG ceremony + new epoch
Protocol version bumpAny staking member via IrisGovernance.proposeUpgrade(newMinProtocolVersion)≥67% voteminProtocolVersion bump + new epoch
Scheduled key rotationAutomatic, on expiry of epoch TTL parameterNo vote requiredDKG re-key; minProtocolVersion unchanged

Epochs are event-driven, not time-driven. During stable periods an epoch can span months; during active development the committee can schedule rapid succession. The epoch cadence is itself governance-tunable.

When a governance vote passes, IrisGovernance emits:

event UpgradeScheduled(
    uint64 indexed targetEpoch,
    uint32 newMinProtocolVersion,
    bool   committeeChanging   // true when a DKG ceremony is required
);

Nodes subscribe to this event as the signal to begin upgrading software before the epoch transition. IrisVerifier.pendingEpoch() returns the scheduled target epoch once governance has locked the parameters on-chain, allowing nodes to confirm the transition is imminent.

At most one transition may be pending at a time. While pendingEpoch() != 0, a second proposeUpgrade or proposeCommitteeChange reverts with UpgradePending. pendingEpoch is cleared back to 0 only when updateCommittee completes the transition (or is reset by an emergency intervention). This serializes committee-membership and protocol-version changes so a node never has to reconcile two in-flight epoch targets.

Rotation Constraints

Committee rotations are gated on three invariants:

  1. Off-chain DKG must complete first. New aggregate keys and per-node shares are produced by the DKG ceremony described in §3.4. updateCommittee cannot be a source of new keys — it can only ratify what DKG has already produced.
  2. The OLD committee must sign the rotation message. The contract trusts the current aggregate key; the current committee threshold-signs (newEpoch, newAggregateKey, committeeHash, newMinProtocolVersion); the contract verifies that signature, then atomically replaces the committee state. The new committee inherits authority directly from the old, and there is never a moment where an unsigned aggregate key is treated as authoritative.
  3. Epoch monotonicity. newEpoch must equal currentEpoch + 1. This prevents replay of stale rotation messages.

Node Behavior at Epoch Boundaries

When a node detects an UpgradeScheduled event from IrisGovernance:

  1. Version check. If the node's software version is below newMinProtocolVersion, it enters WaitingForUpgrade state: it continues serving rounds under the current epoch but does not accept new committee assignments for the target epoch.
  2. DKG participation. If committeeChanging = true, the node participates in the Feldman VSS DKG ceremony before updateCommittee is called.
  3. Round-in-flight handling. Rounds that opened under epoch N seal under the epoch N aggregate key. No round spans an epoch boundary — if updateCommittee fires while a round is in the Voting or Commit phase, the round completes under epoch N and the transition applies to the next round.
  4. updateCommittee submission. Once DKG completes (or immediately for protocol-version-only upgrades), any committee member or the Relayer submits the signed rotation to IrisVerifier. The call is permissionless once the signature is produced.

No mixed-version rounds. A node is either running the required minProtocolVersion and participates fully, or it is excluded from the committee at the epoch boundary. There is no Leader Node bridging between message versions, and no dual-topic subscription period.

Upgrade Notice Minimums

The protocol does not enforce notice windows — only epoch monotonicity. The following minimums should be observed as governance policy:

Network phaseMinimum noticeRationale
DevnetNoneSingle-operator environment
Public testnet1 weekMulti-operator; CI/CD runway needed
Mainnet early4 weeksInstitutional operators; audited upgrades
Mainnet stableGovernance-decided per transitionUpgrades become rare

Scheduled Key Rotation (TTL)

IrisVerifier stores two values that define the scheduled rotation window:

When block.timestamp >= epochLastTransitionAt + epochRotationTtl, the committee nodes detect this via their chain watcher, run an off-chain same-committee DKG re-key (no membership change), and submit updateCommittee without a prior governance vote. IrisVerifier validates the TTL window and accepts the call. minProtocolVersion is unchanged by a scheduled rotation.

Default TTL values by network phase:

PhaseepochRotationTtlNotes
Devnet0 (disabled)Event-driven only
Testnet30 daysExercises re-key code path
Mainnet early90 daysQuarterly; forward secrecy vs. ceremony cost balance
Mainnet stableGovernance-decidedTunable once committee is mature

If the scheduled DKG fails (DkgAborted), epochLastTransitionAt is not updated and the TTL window re-opens immediately after the one-epoch re-submission cooldown (§3.4).

Rotation Bounty

For TTL-triggered rotations, IrisVerifier transfers epochRotationBounty() wei from the protocol treasury to msg.sender and emits RotationBountyClaimed. The bounty is governance-tunable and sized at 1.2× the estimated updateCommittee gas cost on the deployment chain, making the call always profitable for any address monitoring for the TTL expiry.

For governance-triggered rotations (membership change or upgrade), no bounty is paid.

In Phase 1, the Foundation Relayer always calls updateCommittee and the treasury reimburses gas at cost (bounty = 0). In Phase 2+, any address can call and claim the premium.

Bootstrap. The very first committee has no prior committee to sign for it. IrisVerifier is therefore deployed with a one-shot initializeCommittee(epoch0, aggregateKey0, committeeHash0, minProtocolVersion0) callable only by the deployer / governance multisig, set to revert with RotationAlreadyInitialized after the first call. The minProtocolVersion0 argument pins the genesis version floor: from epoch 1 onward it can only increase, enforced by the ProtocolVersionRegression revert in updateCommittee. From epoch 1 onward, all rotations follow the OLD-signs-NEW pattern.

Failed DKG. Per §3.4, if a DKG ceremony enters DkgAborted, the prior committee retains its keys and the on-chain epoch does not advance. There is no on-chain action — updateCommittee is simply not called. Pending join candidates must re-submit via IrisGovernance after the one-epoch cooldown defined in §3.4.

Slashed exits between rotations. A single slashing does not require an epoch rotation — the threshold t is sized so that a small number of compromised shares does not cross the safety boundary. A rotation is triggered only when committee membership or the threshold itself changes materially: a size change, multiple slashings within an epoch, scheduled epoch turnover, or a governance-approved join.

Why updateCommittee is not gated on IrisStaking events. The chosen design keeps IrisVerifier ignorant of staking mechanics — IrisStaking is the staking ledger; the off-chain DKG is the share-generation ceremony; IrisVerifier only ratifies the aggregate keys those processes produce. This separation lets IrisStaking and the DKG iterate without forcing an IrisVerifier upgrade. The full versioning strategy, including the CBOR→Protobuf migration timeline, is specified in versioning_strategies.md.

BLS Signature Verification (EIP-2537 Precompile)

IrisVerifier performs a BLS12-381 pairing check on every deliverReport, updateCommittee, and commitReputationRoot. The MVP target — Ethereum testnet on Pectra+ (Sepolia or Holesky) — provides BLS12-381 operations as native precompiles per EIP-2537, at roughly 50–80k gas per pairing. The contract calls the precompile directly; there is no verifier abstraction, no deploy-time selector, and no library fallback in the MVP.

Why precompile-only at MVP

A deploy-time blsVerifier constructor parameter (selecting between the EIP-2537 precompile wrapper, a bls12-381-solidity library implementation, and a speculative ZK-verifier path) was considered and rejected for the MVP. Reasoning:

Failure modes (MVP)

Even within a single-chain target, two failure modes the architecture must tolerate are documented in threat_model.html §4.3:

Multi-chain expansion (post-MVP)

When the protocol targets chains beyond Ethereum testnet, the verifier abstraction returns. Specifically:

None of this work is in the MVP scope. The architecture preserves the capacity for it — IrisGovernance can deploy a new IrisVerifier at any time and rotate the committee through it — but the MVP code paths are EIP-2537-only.

Requester Callback Semantics (IIrisReceiver)

The IIrisReceiver push callback lets Requester dapps receive a notification the moment their request is fulfilled, but it must not allow a buggy or malicious Requester contract to brick deliverReport for everyone else. Iris adopts a push-with-pull-fallback pattern: try the callback under a bounded gas budget, catch any revert, and let the Requester recover via an explicit pull if the push failed.

Interface

interface IIrisReceiver {
    /// Called by IrisVerifier after a report is verified and recorded.
    /// Implementers MUST verify msg.sender == known IrisVerifier address —
    /// otherwise an attacker could spoof reports. The function is invoked
    /// under a fixed gas budget (CALLBACK_GAS_BUDGET, see below); reverts and
    /// out-of-gas conditions are caught by IrisVerifier and surface as
    /// CallbackFailed events — they do NOT roll back the report record.
    function onIrisDataReceived(bytes32 requestId, bytes calldata ipfsCid) external;
}

Push-with-pull-fallback pattern

deliverReport invokes the callback inside a try / catch with a bounded gas stipend, after the report has been written to storage and ReportDelivered has been emitted:

function _invokeCallback(address requester, bytes32 requestId, bytes memory ipfsCid) internal {
    try IIrisReceiver(requester).onIrisDataReceived{gas: CALLBACK_GAS_BUDGET}(requestId, ipfsCid) {
        emit CallbackSucceeded(requestId, requester);
        reports[requestId].callbackSucceeded = true;
    } catch (bytes memory reason) {
        emit CallbackFailed(requestId, requester, reason);
        // The report remains recorded; Requester can recover via claimReport.
    }
}

CALLBACK_GAS_BUDGET is a contract constant set at ~100k gas — enough to write to a few storage slots and emit an event, not enough to do significant computation. Computational consumers MUST use the pull model.

OutcomeWhat IrisVerifier does
Callback succeedsEmits CallbackSucceeded, sets reports[requestId].callbackSucceeded = true
Callback reverts with reasonEmits CallbackFailed(requestId, requester, reason); report stays recorded
Callback runs out of gasCaught by catch; same as revert
Reentrancy attemptBlocked by per-request reentrancy guard around deliverReport
Requester does not implement IIrisReceiverCaught by catch; Requester must use claimReport

Pull recovery. When the callback failed (or never existed), the Requester dapp retrieves the result with claimReport(requestId). This costs one extra transaction but is robust against any Requester-side bug. The CallbackFailed event lets a Requester detect the situation in their own off-chain monitoring without polling on-chain state.

Reentrancy. deliverReport follows checks-effects-interactions: it verifies the BLS signature, writes reports[requestId], emits ReportDelivered, then invokes the callback. A re-entered deliverReport for the same requestId reverts with DuplicateReport; for a different requestId it proceeds normally but is bounded by the outer call's overall gas stipend.

Why caught and not reverted. A revert from the callback bubbling up to deliverReport would mean a single buggy or malicious Requester contract could stall the relay-fee market for everyone — Relayers would observe failures, refuse to attempt that request, and the on-chain timeout fallback (§8.7) would not help because the failure is deterministic. Catching the revert isolates the buggy Requester and lets every other request complete normally.

External consumers without callbacks. Aggregator services (e.g., Chainlink DONs) and analytics platforms can consume Iris results entirely via the pull model: subscribe to ReportDelivered events, filter by requestId, and read reports[requestId].ipfsCid — no contract code on their side required.

Gas Cost Targets

The targets below are non-binding budgets used to size the relay-fee schedule on the MVP target chain (Ethereum testnet on Pectra+). Real costs will track EVM opcode prices and IrisVerifier's final storage layout; the figures assume the EIP-2537 precompile path documented in §8.4.

OperationGas (EIP-2537 path)Notes
deliverReport (callback succeeds)200–300kDominated by the BLS pairing
deliverReport (callback fails)250–350kAdds CallbackFailed event + 100k callback budget consumed
claimReport50–80kNo BLS check; Requester-authorized read+ack
updateCommittee250–400kBLS verify + storage writes for new key + epoch + committee hash + minProtocolVersion; TTL-triggered calls additionally transfer epochRotationBounty to caller
commitReputationRoot100–150kBLS verify + single 32-byte storage write
initializeCommittee (one-shot)80–120kOwner-only, no BLS check on first commit
extendPersistence60–100kSingle storage write to panelExpiresAt; emits PersistenceExtended; no BLS check (anyone can fund)
reportFailure80–130kDeadline check + merkle root write + RoundFailed event + fee refund transfer; no BLS check
submitFraudProof150–250kSignature verification + IrisStaking slash call; no BLS pairing (ed25519 verify only)

Relay fee accounting. A portion of the request fee paid by the Requester to IrisVerifier covers Relayer gas. The remainder is split between honest committee members (per the §6.3 similarity-score weighting) and the protocol treasury per the schedule in governance.html. Because the MVP is testnet-only, "Relayer gas" is funded from a Foundation-held faucet allocation rather than collected fees — the relay-fee schedule still sizes properly (so that mainnet expansion doesn't require a re-architecture), but no real-money flows are required for the MVP to operate.

Why these are targets, not commitments. Gas costs vary with chain congestion, opcode repricing (EIP-1153, EIP-1559 base fee dynamics), and storage layout choices made during IrisVerifier implementation. The architecture spec fixes the order of magnitude for fee-schedule sizing; the actual figures live in tokenomics governance.

Multi-chain note. A library-fallback column (~10–15× higher per-row than the EIP-2537 column) was published in earlier drafts and removed for the MVP per §8.4. It returns when the protocol expands to chains without native EIP-2537. Until then, every figure here assumes the precompile path on Sepolia/Holesky.

Relayer Trust Model & Phase Roadmap

Trust Model: This section is the deep-dive for the Relayer row in §1.3 Trust Model.

The Relayer's safety properties are established by cryptography (§1.2) — no Relayer can forge a valid threshold signature or alter a finalized payload. The Relayer's liveness properties, however, depend on at least one honest Relayer being willing to transport messages. A single Relayer that refuses to relay can censor specific requests, stall the network, or front-run competing Relayers for fee revenue. This subsection defines what a malicious Relayer can and cannot do, the reputation-based mitigation, and the phased decentralization of the role.

What a malicious Relayer can do

What a malicious Relayer cannot do

Mitigation: Reputation-Gated Opt-In Role

The Relayer is structured as an opt-in sub-role atop a regular Iris node, identified at the network layer by PeerId but gated economically by the operator's on-chain staking address. Eligibility requires the staking address to hold a reputation score above a governance-tunable threshold. The threshold is set high enough that the time, stake-time, and compute required to accumulate it represent a serious opportunity cost — grinding to the threshold purely to censor forfeits months of relay-fee revenue and risks slashing of the underlying stake. Reputation is bound to the staking address (not just the PeerId) precisely so that selling the libp2p keypair does not transfer reputation: an attacker would have to acquire the underlying stake too, restoring the opportunity-cost argument. Multiple eligible Relayers compete naturally; the on-chain timeout fallback (below) ensures a single censor cannot stall the network.

Reputation Inputs (Intent, not Formula)

Reputation is computed off-chain by every committee member from observable round events. Inputs include:

Penalties and decay:

Concrete weights, decay constants, the score formula, and per-phase eligibility thresholds are specified in tokenomics.html §9 (Reputation Algorithm) and §10 (Reputation Thresholds & Tiered Access); all are governance-tunable per epoch. The architecture spec fixes the inputs and intent, not the parameterization.

Reputation Commitment Mechanism

Because reputation is derived from observable round events, every honest committee member arrives at the same score for every staking address. The active committee periodically commits a Merkle root of (staking_address, reputation_score) tuples to IrisVerifier via the existing t-of-n BLS threshold signature, at a cadence aligned with epoch boundaries (§3.4). Relayers submit Merkle proofs alongside deliverReport calls so the contract can verify eligibility cheaply. This reuses the committee-signed root-of-trust pattern already established for finalized panels — no new cryptography, no new trust assumption beyond what the contract already places in the committee aggregate key.

Censorship Fallback Mechanism

IrisVerifier tracks requestSubmissionDeadline[requestId] = blockTimestamp(DataRequest) + T1, with T1 ≈ 600s (twice the Phase 2 manifest deadline of 300s, providing headroom for normalization, voting, and on-chain finality). Before the deadline, the access-control rule for deliverReport matches the current phase (see roadmap below). After the deadline, any address proving reputation above the threshold via Merkle proof against the latest committed root may submit — regardless of phase, and regardless of whether the originally-expected Relayer is still online. This is the core anti-censorship guarantee: a single non-relaying Relayer cannot indefinitely stall a request, because any reputable peer can step in and collect the relay fee instead.

Relayer Trust Model — Phase Roadmap

The Relayer role is a known bootstrapping compromise on the liveness axis only — safety is fully decentralized from day one across all phases. Iris addresses the liveness gap through a three-phase progression. The "Phase 1–3" labels are local to this roadmap; their mapping to product releases is in the Product phase column. (See next_steps.md §Phase Roadmap for the canonical product taxonomy.)

PhaseOperatorProduct phaseDescription
Phase 1 — Foundation RelayerIris Foundation[MVP v0.1.0]The Iris Foundation operates the sole Relayer. The on-chain deliverReport allowlist contains only the Foundation's address. The Foundation commits to a multi-region deployment with a published uptime SLA, but liveness is still concentrated in a single organization. The on-chain timeout fallback is disabled (or set to a very long window) during this phase because no eligible reputation set yet exists — this phase is honestly framed as a liveness compromise, not a solution
Phase 2 — Foundation + Reputable Opt-In RelayersIris Foundation + reputation-qualified node operators[V1 v1.0.0]The on-chain access rule for deliverReport accepts (a) the Foundation's address, or (b) any address with a valid Merkle proof of reputation above the threshold against the latest committed root. Multiple Relayers compete; the timeout fallback activates so any reputable Relayer can pick up a censored request after T1. The Foundation Relayer remains as a guaranteed-available fallback while the reputation set grows large enough to be censorship-resistant on its own
Phase 3 — Permissionless Reputation-GatedAny node operator above the reputation threshold[V1+ post-v1]The Foundation steps away from its privileged position. deliverReport becomes purely permissionless above the reputation threshold; the Foundation address has no on-chain status beyond what its own reputation score earns. The role is fully decentralized, gated only by accumulated honest behavior and the underlying stake
Bottom Line: In the MVP, the Relayer is the Iris Foundation. This concentrates liveness — not safety — on a single operator, mitigated by a multi-region deployment and an uptime SLA. The roadmap progressively transfers Relayer eligibility from the Foundation → reputable opt-in operators above a reputation threshold → a fully permissionless reputation-gated pool. Across all three phases, safety is unchanged: no Relayer can forge a threshold signature, alter a payload, or fabricate an on-chain request at any phase.

Round Failure Handling

This section is the normative on-chain specification for round failures. The four-strategy comparison (silent drop / on-chain events / signed certificates / layered taxonomy) and the engineering rationale behind each decision below are preserved in the archived error_handling_strategies.md (§§10–12).

Failure Taxonomy

Every round failure belongs to one of three categories:

CategoryDefinitionResponse
TransientTemporary conditions (node dropout, API jitter, Leader timeout) where a retry is likely to succeedRoundFailed event + partial fee refund; Requester resubmits
AttributableIdentifiable misbehavior from a specific node with on-chain evidenceRoundFailed + submitFraudProof triggers stake slash via IrisStaking
PermanentConditions that cannot be resolved by retry (BFT threshold violated, Provider discontinued)RoundFailed(Permanent) + full fee refund; resubmission gated by cooldown

FailureReason Enum

enum FailureReason : uint8 {
    InsufficientCommitments,  // 0 — < t nodes published TlsCommitment (Phase 1)
    InsufficientManifests,    // 1 — < t nodes published Manifest (Phase 2)
    LeaderTimeout,            // 2 — no Proposal within proposal_deadline_seconds
    InsufficientSignatures,   // 3 — < t BLS shares during Voting
    IPFSPinFailure,           // 4 — Proposal broadcast but IPFS CID unreachable
    Permanent,                // 5 — Phase 2: unrecoverable; gates resubmission
    Unknown                   // 6 — catch-all; caller could not determine cause
}

FailureReason is advisory in MVP — it is set by whoever calls reportFailure and is not BFT-verified. The Requester uses it as a retry heuristic, not as a security-critical signal.

reportFailure Authorization

Contributor Credit Schedule

On reportFailure, contributor credits are distributed from the fee pool before refunding the Requester. Default credit tiers (governance-tunable):

Work completedCredit (% of base contributor share)
Phase 1 only (TlsCommitment published)15%
Phase 1 + Phase 2 (Manifest published)60%
Phase 1 + Phase 2 + BLS share submitted80%

The remaining gap to 100% represents the value of the finalized result the Requester did not receive. No contributor earns full credit on a failed round.

contributor_credit_pool = sum of credited contributors' shares
Requester refund        = fee − contributor_credit_pool − protocol_treasury_cut

Contributors submit a merkle proof against contributorCreditsRoot to claim their credit.

Auto-Resubmission

DataRequest accepts an optional auto_resubmit: bool field. When set, the Requester escrows a second fee at submission. If the round fails with InsufficientCommitments, InsufficientManifests, or LeaderTimeout, IrisVerifier automatically creates a new round from the escrowed fee with a new request_id. Capped at one automatic resubmission; manual resubmission is always available after. The escrowed fee is returned if the first round succeeds.

Fraud Proofs (round-failure phase 2 / [V1 v1.0.0])

[MVP v0.1.0] ships threshold slashing only — submitFraudProof is wired but the fraud-type table is empty. [V1 v1.0.0] activates submitFraudProof with two fraud types at launch:

fraudTypeEvidence requiredSlashes
0BadMedoid(i, j) matrix cell + both contributor GeoTIFF hashes proving the Leader's similarity score is wrongLeader Node stake
1LeaderEquivocationTwo signed Proposal messages with the same request_id, different ipfs_cid, same Leader ed25519 signatureLeader Node stake

LeaderEquivocation is distinct from DuplicateReport: DuplicateReport fires when a second deliverReport is submitted for the same request_id (protecting contract state). submitFraudProof(LeaderEquivocation) proves the Leader Node broadcast two conflicting Proposals at the GossipSub layer (punishing the equivocator). Both mechanisms coexist.

Post-MVP fraud-type extensions:

Phase 2 — Automatic Retry (k=3 Leader Rotation)

In Phase 2, the round FSM pre-computes k=3 candidate Leader Nodes at round start using:

leader_index_r = keccak256(block_hash ‖ request_id ‖ r) mod total_stake

for r ∈ {0, 1, 2}. On LeaderTimeout, the FSM rotates to candidate r+1 without Requester resubmission. The same fee pool covers all attempts; failed Leader partial contributor credits drain from it. After k failures, reportFailure(LeaderTimeout) is called against the remaining pool.

The T1 invariant must be updated to accommodate k attempts:

T1 ≥ k × proposal_deadline_seconds + manifest_deadline_seconds
     + voting_timeout_seconds + chain_finality_margin

A single upfront Requester fee covers all k attempts; the schedule must be sized to absorb the worst case (k−1 failed Leaders draining the 60% credit tier, leaving enough for one successful round's payouts). The fee is paid once, not per attempt, to avoid mid-round callbacks to the Requester contract — those would open a reentrancy surface. If the pool drains below the minimum viable contributor payout before k attempts are exhausted, the remaining attempts are skipped and the Requester receives a partial refund.

Settled decisions (summary)

The phased rollout is Strategy B (on-chain failure events) at [MVP v0.1.0], extended to Strategy D (layered taxonomy with attribution + retry) at [V1 v1.0.0]. Consolidated decision ledger:

DecisionValue
MVP failure reportingreportFailure callable by round participants (T1 → T1+120s), then any address permissionlessly
FailureReason verificationUnverified in MVP (advisory); fraud proofs add attribution in Phase 2
Fee refund on failurePartial: contributors credited per the credit schedule above, remainder refunded to Requester
Automatic retry in MVPNo — Requester resubmits; optional auto_resubmit flag escrows a second fee at submission
Automatic retry in Phase 2Yes — k=3 Leader rotation for LeaderTimeout only; same fee pool, no Requester resubmission
Slashing for attributable failuresPhase 2 — submitFraudProof for BadMedoid and LeaderEquivocation first
Permanent failure handlingPhase 2 — FailureReason.Permanent gates resubmission with a governance-tunable cooldown
ReportDelivered observabilityAdd contributorCount and thresholdUsed fields; no new event type
LeaderEquivocation detection vs. slashingDuplicateReport guards contract state; submitFraudProof(LeaderEquivocation) punishes the equivocator

How the Layers Connect — A Full Request Walkthrough

Note: This walkthrough intentionally revisits the state transitions from §3.1 and the provenance chain from §5. It is designed as a consolidated reference summary that traces a single request through all four state layers end-to-end.

One request, traced end-to-end

To tie everything together, here is a single request traced through all four state layers:

  1. Committee State is already established: 7 staked nodes have completed DKG, the aggregate public key is registered on-chain, t = 5.
  2. A Requester emits a DataRequest event on Ethereum. The Relayer picks it up and publishes it to iris/requests/v1.
  3. Each node's Node State spawns a new Round (Layer 1). The round enters Idle → Observing.
  4. In Observing Phase 1 (≤60s), each node spawns its iris-fetch TEE enclave, the enclave completes the TLS handshake with its Data Provider, and the node publishes a TlsCommitment. In Phase 2 (≤300s from round start), each committed node completes the full in-enclave payload download, the enclave returns the hardware-attested document, the node computes attestation_hash = BLAKE2b(att_doc_bytes), and publishes its Manifest. The Node State caches the GeoTIFF and the attestation document locally.
  5. The elected Leader Node's round transitions to Aggregating. The Leader Node retrieves all GeoTIFFs, runs the normalization pipeline, selects the Average Scenario, and pins it to IPFS.
  6. The Leader Node's round transitions to Pre-Commit and broadcasts the proposal.
  7. Regular Nodes verify and transition to Voting, broadcasting their BLS signature shares.
  8. The Leader Node collects 5 shares, aggregates them, and transitions to Commit.
  9. The Relayer submits the report on-chain. The Panel (Layer 3) is now finalized — one more facet of the geodesic sphere.
  10. Committee State is unchanged (no nodes joined or left this round). The round is cleaned up from each node's Node State.
flowchart TD
    subgraph External ["External Entities"]
        direction LR
        Req(("Requester
(Dapp)")) Chain{{"Host Blockchain
(Smart Contract)"}} API[/"Commercial APIs
(Maxar/Planet)"/] end subgraph L4 ["Layer 4: Committee State"] DKG[("Active Set & PubKey
(Maintains t > 2n/3)")] end subgraph L2 ["Layer 2: Node State"] direction LR PeerID["Cryptographic Identity
(ed25519 & BLS Keys)"] Cache[("Local Storage Cache
(GeoTIFFs & Attestations)")] end subgraph L1 ["Layer 1: Round State (Per-Request FSM)"] direction LR FSM_Obs["Observing
(Fetch & Manifest)"] --> FSM_Agg["Aggregating
(Leader Computes Avg)"] FSM_Agg --> FSM_Pre["Pre-Commit
(Proposal)"] FSM_Pre --> FSM_Vote["Voting
(Verify & Sign)"] FSM_Vote --> FSM_Com["Commit
(Aggregate Sig)"] end subgraph L3 ["Layer 3: Panel State"] FinalPanel[/"Finalized Panel
(IPFS CID + Metadata)"/] end %% Relationships Req -- "1. Emits Request" --> Chain Chain -- "2. Relayed to Network" --> FSM_Obs L2 -. "3. Spawns Round" .-> L1 PeerID -. "Signs Manifests & Shares" .-> L1 DKG -. "Defines Threshold 't'" .-> FSM_Vote FSM_Obs -- "4. Downloads Imagery (in-enclave)" --> API API -- "Raw Data + TEE Attestation" --> Cache Cache -- "Loads Tensor" --> FSM_Agg FSM_Agg -- "5. Pins to IPFS" --> FinalPanel FSM_Com -- "6. Seals with BLS Sig" --> FinalPanel FinalPanel -- "7. deliverReport()" --> Chain Chain -- "8. onIrisDataReceived()" --> Req classDef ext fill:#eceff1,stroke:#607d8b,stroke-width:2px,color:#000; classDef l4 fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px,color:#000; classDef l2 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#000; classDef l1 fill:#e3f2fd,stroke:#2196f3,stroke-width:2px,color:#000; classDef l3 fill:#fff3e0,stroke:#ff9800,stroke-width:2px,color:#000; class External,Req,Chain,API ext; class L4,DKG l4; class L2,PeerID,Cache l2; class L1,FSM_Obs,FSM_Agg,FSM_Pre,FSM_Vote,FSM_Com l1; class L3,FinalPanel l3;

Versioning & Upgrade Strategy

Epoch-anchored upgrades

Protocol upgrades are anchored to the IrisVerifier epoch counter. Breaking changes in any axis (message schema, transport protocol ID, contract interface, BLS domain) require a ≥67% governance vote via IrisGovernance and are activated at an epoch boundary. Non-breaking changes (additive contract functions, optional message fields) deploy immediately with no governance gate.

The full strategy — including the four-strategy comparison, the hybrid A+C recommendation, epoch generation design, and the CBOR→Protobuf migration timeline — is specified in versioning_strategies.md. The interface additions to IrisVerifier that support this strategy are defined in §8.2 (minProtocolVersion(), pendingEpoch(), modified updateCommittee) and the epoch rotation mechanics are in §8.3.

Key invariants:

System Complexity and Compute Requirements

Given the strict demands of the Iris Protocol to ingest and process satellite imagery at a nation-state resistant level, nodes require high-end prosumer hardware. Below are the estimated complexity and resource requirements for a single consensus round.

Cryptographic Compute (Hashing)

Iris uses two hash functions with deliberately separate roles:

FunctionUsed forRationale
BLAKE2b-512ContentHash (GeoTIFF payloads), AttestationHash (.att files), local cache addressingBulk off-chain hashing of multi-GB payloads
Keccak-256Leader election seed keccak256(block_hash ‖ request_id)Must be reproduced cheaply in EVM smart contracts

Why BLAKE2b-512 for bulk content

Network transfer is the bottleneck in Iris, not hashing — so the decision trades raw throughput for cryptanalytic security margin. The candidates and why BLAKE2b wins:

BLAKE2b-512 runs at ~1 GB/s per core, uses 12 mixing rounds (the widest cryptanalytic margin of the candidates), and provides a 256-bit pre-image security margin against Grover's algorithm — twice that of SHA-256. It is battle-tested in WireGuard, Argon2, Zcash, and libsodium, and has over a decade of public cryptanalysis without a meaningful break.

Bandwidth & Network Throughput

Network transfer is the primary time-constraining factor due to the large payload sizes:

Memory (RAM) Requirements

The Data Normalization Engine must hold multiple large tensors in memory to calculate pairwise similarity scores:

Normalization Pipeline (CPU/GPU Compute)

The mathematical processing (Orthorectification, Mean Absolute Distance, Spectral Angle Mapper) is highly parallelizable:

Observability & Monitoring

Iris is a distributed system that runs unattended across committee members who may be geographically dispersed. Operators need structured, correlated telemetry to diagnose round failures, detect degradation before it becomes an outage, and verify that consensus is healthy. This section defines the observability contract that every crate must meet.

Instrumentation Layers

Three complementary layers are required:

LayerTechnologyPurpose
Structured logstracing crate with JSON subscriberPer-event, human- and machine-readable records. Every significant state transition emits a log line with correlated context fields
Metricsprometheus counters/gauges/histograms via metrics + metrics-exporter-prometheusAggregated, time-series data scraped by Prometheus and visualised in Grafana. Suitable for alerting thresholds
Distributed tracestracing-opentelemetry + OTLP exportSpan-level correlation across async tasks and crate boundaries. Enables per-round latency breakdown from DataRequest receipt to deliverReport submission

Required Tracing Spans

Every round opens a root span at DataRequest receipt and closes it at ReportDelivered or RoundFailed. All child spans must carry request_id and peer_id as context fields so logs from multiple nodes can be correlated in a central collector.

SpanCrateFields
roundiris-consensusrequest_id, leader_id, epoch, contributor_count
observing.phase1iris-datarequest_id, provider, tls_session_id
observing.phase2iris-datarequest_id, image_hash, download_bytes
aggregating.transferiris-networkrequest_id, peer_id, bytes, duration_ms
aggregating.normalizeiris-normalizerequest_id, n_contributors, selected_index
voting.collectiris-consensusrequest_id, shares_received, shares_needed
commit.deliveriris-networkrequest_id, tx_hash, gas_used
epoch.transitioniris-networkold_epoch, new_epoch, min_protocol_version

Required Metrics

All metrics are prefixed iris_. The following are the minimum set for a production-ready node:

Round Health

MetricTypeDescription
iris_rounds_started_totalCounterIncremented on each DataRequest received
iris_rounds_completed_totalCounterLabels: outcome={success,failed}
iris_round_failure_reason_totalCounterLabels: reason={InsufficientCommitments,InsufficientManifests,LeaderTimeout,InsufficientSignatures,IPFSPinFailure,Unknown}
iris_round_duration_secondsHistogramEnd-to-end round latency. Buckets: 30s, 60s, 120s, 240s, 360s, 600s
iris_contributor_countGaugeBLS shares received in the last completed round
iris_leader_elections_totalCounterLabels: outcome={elected,failed}

Network Health

MetricTypeDescription
iris_connected_peersGaugeCurrent peer count
iris_gossipsub_mesh_sizeGaugePer-topic mesh peer count. Labels: topic
iris_peer_scoreHistogramDistribution of GossipSub peer scores across connected peers
iris_geotiff_transfer_bytes_totalCounterLabels: direction={inbound,outbound}
iris_geotiff_transfer_duration_secondsHistogramLatency of completed GeoTIFF transfers
iris_geotiff_transfer_failures_totalCounterLabels: reason={hash_mismatch,timeout,connection_reset}

Node Health

MetricTypeDescription
iris_uptime_secondsGaugeSeconds since node start
iris_epochGaugeCurrent on-chain epoch
iris_protocol_versionGaugeRunning binary's packed ProtocolVersion uint32
iris_cache_bytes_usedGaugeGeoTIFF cache disk usage
iris_cache_evictions_totalCounterLRU evictions triggered by disk pressure
iris_reputation_scoreGaugeNode's current off-chain reputation score

Health Check Endpoint

Every iris-node binary exposes an HTTP health endpoint on a configurable port (default 9001):

PathReturnsHealthy when
GET /health/live200 OKProcess is running
GET /health/ready200 OK / 503Node is meshed on all three GossipSub topics AND connected to ≥ D_low (4) peers AND on-chain epoch matches local state
GET /metricsPrometheus text formatAlways — scrape endpoint for Prometheus

The /health/ready endpoint is the primary signal for orchestration systems (Docker healthcheck, Kubernetes readiness probe) to know whether a node is fit to participate in rounds.

Operators deploying to production should configure alerts on the following conditions:

AlertConditionSeverity
IrisNodeDowniris_uptime_seconds absent for > 2 minCritical
IrisRoundFailureSpikerate(iris_rounds_completed_total{outcome="failed"}[5m]) > 0.5Warning
IrisLeaderTimeoutSpikerate(iris_round_failure_reason_total{reason="LeaderTimeout"}[10m]) > 0.3Warning
IrisMeshDegradediris_gossipsub_mesh_size < 3 for any topicWarning
IrisPeerCountLowiris_connected_peers < 5Warning
IrisCachePressureiris_cache_bytes_used / node_filesystem_size > 0.85Warning
IrisEpochStaleOn-chain epoch via RPC ≠ iris_epoch for > 5 minCritical
IrisVersionBelowFlooriris_protocol_version < on_chain_min_protocol_versionCritical

Configuration (iris.toml — Observability Section)

[observability]
metrics_port      = 9001          # HTTP port for /metrics and /health endpoints
log_level         = "info"        # RUST_LOG override; valid: trace, debug, info, warn, error
log_format        = "json"        # "json" for production, "pretty" for local development
otlp_endpoint     = ""            # Optional: OTLP gRPC endpoint for distributed traces
                                  # e.g., "http://jaeger:4317"; empty disables tracing export

A reference Grafana dashboard should ship with the node binary. Minimum panels:

  1. Round success rateiris_rounds_completed_total{outcome="success"} / iris_rounds_started_total over 1h
  2. Round duration p50/p95/p99 — from iris_round_duration_seconds histogram
  3. Failure reason breakdown — stacked bar of iris_round_failure_reason_total by reason
  4. Mesh healthiris_gossipsub_mesh_size per topic over time
  5. GeoTIFF transfer throughput — rate of iris_geotiff_transfer_bytes_total
  6. Peer score distribution — heatmap of iris_peer_score
  7. Epoch timelineiris_epoch vs. time with annotations at epoch transitions

End-to-End Fault Tolerance

Five layers of tolerance

The Iris Protocol is designed as a defense-in-depth pipeline. Because the system bridges off-chain commercial satellite imagery with on-chain smart contracts, a single type of fault tolerance is insufficient. Instead, the architecture combines multiple layers of tolerance to create an unbroken Chain of Provenance:

  1. Cryptographic Fault Tolerance (Data Ingestion): Using multi-vendor TEE remote attestation, the protocol guarantees the origin and integrity of the data, hardware-rooted at each accepted vendor's PKI and bounded against single-vendor compromise by both the BFT threshold and the Phase 3 cross-vendor-quorum rule.
  2. Deterministic Verification Tolerance (Data Normalization): By enforcing a 100% deterministic Rust pipeline, the network tolerates malicious preprocessing. Any attempt to alter the data during orthorectification is caught by independent Verifiers.
  3. Sybil & Impersonation Tolerance (Network Layer): The Noise Protocol and ed25519-bound PeerIDs prevent node impersonation, while GossipSub v1.1 peer scoring drops malicious or spamming nodes, thwarting Eclipse and Sybil attacks.
  4. Byzantine & Collusion Tolerance (Consensus): The Iris-BFT engine ensures the network can agree on a valid reconstruction as long as f < n/3 nodes are malicious. The BLS threshold signature acts as a cryptographic lock, making it impossible for a minority of nodes to forge a finalized consensus report.
  5. Rational Fault Tolerance (Smart Contracts): The staking and slashing tokenomics assume nodes are economically rational, heavily penalizing any malicious deviations and aligning financial incentives with data integrity.

By forcing an attacker to simultaneously break TEE attestation hardware (across multiple vendors), deterministic pipelines, threshold signatures, and economic incentives, the Iris Protocol achieves a robust end-to-end Byzantine and Cryptographic tolerance for geospatial data.

Protocol Parameters

Moved to parameters.html

The protocol parameter catalog moved to parameters.html. Section numbering (§14.1, §14.2.x, §14.3) and anchors (#1428-vendor-preprocessing-matrix, #143-parameter-history, #142-current-parameter-set-v001-demo, etc.) are preserved verbatim — existing cross-doc links continue to resolve with just the file-name swap.