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.
| Document | Relationship |
|---|---|
| Glossary | Canonical 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 |
| Whitepaper | High-level vision and motivation. This architecture doc is the technical realization of the whitepaper's goals |
| Data Normalization Specification | Complete mathematical formulation of the tensor metrics summarized in §6 |
| Implementation Plan — Demo / — MVP | Phased engineering breakdown that maps directly to the layers defined in §3. Demo covers Phases 0 → 4.5; MVP covers Phase 5+ |
| Protocol Parameters | Versioning model + current parameter set + allowed-override ranges + per-version history (formerly architecture.md §14) |
| Workspace Crate Map | Per-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 Model | Companion 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 |
| Governance | Off-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 |
| Tokenomics | Concrete 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 |
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.
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;
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.
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.
iris-fetch enclave) that produces a hardware-vendor-signed attestation document; produce a Manifest containing the BLAKE2b image hash and the attestation hash; independently verify the Leader's proposed medoid against their own observation; cast a BLS signature share on accepted proposals.IrisStaking to join the committee and receive a share of the request fee proportional to participation. Misbehavior is penalized via on-chain slashing.t-of-n threshold signature requires agreement from t > ⌊2n/3⌋ committee members. Attestation forgery is cryptographically infeasible without breaking the host TEE vendor's attestation PKI; the multi-vendor whitelist (§5.5) bounds the blast radius of any single-vendor compromise.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.
Manifest messages from committee members over the observation window; retrieve full GeoTIFF payloads from Data Providers via Noise-encrypted direct streams; run the normalization pipeline to compute pairwise similarity scores and select the medoid (Average Scenario); broadcast the proposal to the committee; aggregate valid BLS shares into the round's threshold signature.t committee members. The deterministic election formula (§7.1) prevents self-nomination and persistent leadership capture.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.
NSM_GetAttestationDoc on Nitro, DCAP/PCS on Intel, KDS/VLEK on AMD); publish revocation lists for compromised platform instances.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.
DataRequest events and broadcast them to the Iris GossipSub mesh; receive finalized panel reports from the Iris network and submit them on-chain to IrisVerifier; cover gas costs from relay-fee revenue.t-of-n BLS private key shares it cannot produce a valid committee threshold signature; it merely transports cryptographically verifiable messages. Liveness — A censoring Relayer can drop events or withhold reports, but the on-chain timeout fallback (requestSubmissionDeadline) limits the damage window. See §8.7.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.
Decentralized applications (dapps) or smart contracts on a host blockchain that initiate the protocol by submitting DataRequest transactions and consume the resulting verified panels.
DataRequest on-chain specifying an Area of Interest (AoI), a timestamp, and the request fee; retrieve the finalized panel from IPFS using the CID recorded on-chain; verify the BLS threshold signature against the IrisVerifier aggregate public key.§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.
| Actor | Trusted to do | Not trusted to do | Consequence of misbehavior | Deep-dive |
|---|---|---|---|---|
| Requester | Pay the request fee; provide a valid AoI / timestamp pair on-chain | Influence consensus, leader election, or panel content beyond the request parameters | Request stalls or is rejected on-chain; no slashing (Requester holds no stake) | §1.2 |
| Data Provider | Serve imagery over TLS using a stable hostname, with a valid TLS certificate chain | Be honest about pixel content — only the TLS-attested fact of delivery is trusted; pixel-level lies are filtered by the consensus medoid | None 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 payloads | Removal from the on-chain accepted-attestor whitelist; cross-vendor-quorum rule (Phase 3) bounds blast radius of a single-vendor compromise | §5.5 |
| Regular Node | Honestly 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 proposals | Forge consensus alone; equivocate; submit data anomalous to the medoid; tamper with bytes between the enclave and the local cache | Stake slashed for invalid attestation, anomalous data, or attributable DKG misbehavior | §3.4, the threat model §Slashing |
| Leader Node | Aggregate manifests; run the normalization pipeline; propose the medoid (Average Scenario); aggregate t valid BLS shares | Forge consensus alone (each Regular Node independently re-verifies the proposal); withhold a successful proposal indefinitely | Round fails (no proposal advances → re-elect or timeout); attributable misbehavior is slashable | §7.1–§7.4 |
| Relayer † | Transport DataRequest events on-chain → off-chain and finalized panels off-chain → on-chain | Forge consensus; alter the CID or signature payload; fabricate a DataRequest | Lost 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 root | Be individually honest — only the collective threshold is assumed; any individual member may be Byzantine | Network-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 Foundation † | Operate 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 completes | Override consensus; hold BLS key shares; modify finalized on-chain panels | Reputational damage and governance pressure; phase roadmaps progressively reduce dependency to zero | §5.5, §8.7 |
A handful of cross-cutting invariants underpin the per-actor trust grants above. These are network-wide assumptions, not properties of any single actor:
n/3 of the active committee is malicious at any given time. This is the foundational safety assumption; if violated, the threshold signature can no longer be trusted to represent committee consensus.t honest participants are present and correct at every committee DKG ceremony (per §3.4). A successful DKG is a precondition for a usable aggregate key.IrisVerifier aggregate key, threshold value, and committee membership are updated is deferred to a future governance specification. Until that specification lands, Foundation-stewarded governance is part of the Phase 1 trust footprint.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.
To prevent misinterpretation of the protocol's capabilities, the following are explicitly out of scope for the Iris architecture as specified in this document:
DataRequest for each observation.iris.toml. The protocol does not broker, subsidize, or manage provider subscriptions.Before diving into subsystems it helps to have a mental model for what the state machine is actually building.
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:
𝒮) to measure how close each photograph is to every other.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.
The geodesic model clarifies several architectural decisions:
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.
𝒮 = α · σ_px/m + β · sec(θ_g). A single protocol-wide scalar bar with Foundation-set (α, β) — no on-chain governance vote at demo. Tighter than the V1 working figure but loose enough to cover most Sentinel-2 acquisitions, while the composite term lets the demo committee empirically feel out whether sec(θ_g) radiometric stress adds signal the similarity kernel misses. Paired with IrisDEM-D0 (constant-plane) and flat-Earth bilinear ortho. Decision shape per design_q1_effective_nadir_angle.md §1.1.1.a + §1.3.1.d.σ_px/m^max[sensor] + sigma_max_default fallback. Drops the composite (β = 0) — at MVP the envelope cap is purely geometric, and BRDF/atmospheric stress is absorbed by the Phase 4 similarity kernel (deferred to V1+ per design_q1 §1.3.2.d). Per-sensor entries are populated empirically from each whitelisted sensor's reported off-nadir distribution against per-detector-class formulas (pushbroom / whiskbroom / frame); unknown sensors fall back to a 15°-equivalent σ_px/m default after clearing the §1.2.3 reputation gate. Paired with IrisDEM-M1 (Copernicus GLO-30) and DEM-aware 3D reprojection. Calibrated against the §1.4.1 a+b harness — pixel-budget plus medoid-flip-rate.user_data (§1.3.3.c).(α, β) revisited. The decision to drop the composite 𝒮 at MVP is conditional on V1+ BRDF-aware similarity metrics absorbing the radiometric stress (design_q1 §1.3.2.d). If field experience shows the kernel cannot absorb it, the composite returns under governance-tuned (α, β).user_data / calibration-target trade-offs are decided in design_q1_effective_nadir_angle.md (12 leaf questions, 11 resolved). The only open leaf is the MVP+ §1.2.3 reputation threshold for unknown-sensor graduation from soft-reject to sigma_max_default fallback; placeholder is fixed-10-rounds until MVP-period harness data lands.θ of an AoI in a given window grows roughly with sin θ at fixed altitude, so the Demo → MVP lift (single-scalar 10°-equivalent → per-sensor σ_px/m^max table) materially improves round-availability and reduces tail-latency for low-demand regions. The per-sensor table also admits high-altitude tasking imagery (Maxar, Planet) that a single near-nadir scalar would reject.data_normalization.html § Effective Nadir Angle.sec θ_g), BRDF asymmetry, and occlusion shadows degrade the similarity matrix faster than geometric mis-registration does — accepting those captures would require BRDF-aware similarity metrics rather than a wider envelope cap. The β · sec(θ_g) term retired at MVP per design_q1 §1.3.2.d returns at V1+ if the BRDF-aware kernel can't absorb the radiometric stress on its own.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;
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. ThevNsuffix on every GossipSub topic in this section (iris/requests/v1,iris/observations/v1,iris/consensus/v1) tracks the message-schema axis defined inversioning_strategies.md§1. It advances independently of the on-chaincurrentEpoch()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 activatesvN+1at 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 | Who is active | What is being tracked | Exit condition |
|---|---|---|---|
| Idle | All nodes | Subscription 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. |
| Observing | All Regular Nodes | Two-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). |
| Aggregating | Leader Node only | The 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-Commit | Leader Node broadcasts, all Regular Nodes listen | The 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. |
| Voting | Leader Node collects | The 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). |
| Commit | Leader Node finalizes | The 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. |
| Failed | Any 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. |
// ── 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.)
}
This layer persists across rounds. It represents the node's long-lived identity and operational status.
| State field | What it tracks | Mutated when |
|---|---|---|
| Identity | ed25519 keypair, derived PeerId, BLS private key share | Node first boot (keypair generated) or DKG ceremony (BLS share issued) |
| Committee Membership | List of known committee members, their PeerIds, stake weights, BLS public key shares, and the aggregate public key | DKG ceremony completes after a committee change |
| Provider Credentials | API keys/tokens for Data Providers (Maxar, Planet, Sentinel) | Configured by operator in iris.toml |
| Local Cache | Content-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 contributor | After every successful fetch; LRU eviction on cache GC |
| Active Rounds | Map 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 limits | New request arrives / round finalizes |
| Peer Table | Kademlia routing table + GossipSub mesh peers | Continuously, via libp2p discovery |
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,
}
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
}
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:
Panel data structure into a standardized .json metadata file..json metadata file and the finalized .tiff GeoTIFF image into a single IPFS directory.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.
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.
| Parameter | Default | Where it lives |
|---|---|---|
persistence_period_seconds | 31,536,000 (one year) | Specified per-request by the Requester in the DataRequest; defaulted from iris.toml if omitted |
persistence_price_per_gb_year | governance-tunable; ~$2 / GB-year reference at MVP launch | IrisGovernance parameter, denominated in IRIS token at the prevailing oracle rate |
persistence_fee (per request) | payload_size_GB × persistence_period_years × persistence_price_per_gb_year | Computed 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.
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.
| Layer | What it stores | Duration | Funded by | MVP operator |
|---|---|---|---|---|
| Hot — committee local cache | GeoTIFF + TEE attestation document in ~/.iris/cache/objects/<blake2b>.tiff and ~/.iris/attestations/<hash>.att on each contributing node | local_cache_retention_days (default 30) from delivery, then GC-eligible | Implicit in the round fee (committee members already hold the bytes) | Each node's own disk |
| Warm — long-term pinning service | The full panel IPFS directory (GeoTIFF + JSON metadata + attestation documents + similarity matrix) | Until panelExpiresAt[requestId], extendable via extendPersistence | The Requester's persistence_fee | Phase 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 depth | The underlying GeoTIFF bytes; anyone who still holds them can re-pin under the same CID | Indefinite (best-effort) | None — opportunistic | The 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.
The persistence operator follows the same phased-decentralization shape as the §5.5 attestation roadmap and §8.7 Relayer roadmap:
| Phase | Operator | Status | Persistence guarantee |
|---|---|---|---|
| Phase 1 — Foundation pinning service | Iris Foundation operates a pinning cluster | MVP | Single-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 deals | Foundation funds Filecoin deals on the Requester's behalf out of persistence_fee; deals are renewable at every epoch boundary | Planned | Cryptographic proofs of replication and proofs of spacetime; deal failures are detectable on-chain and trigger Foundation re-deployment |
| Phase 3 — Permissionless proof-of-storage market | Any provider above a reputation threshold (mirroring §8.7) can bid for persistence_fee allocations; failed proofs slash the provider's stake | Future | Trustless; safety follows from the proof scheme |
Each node MAY garbage-collect its local cache once both of the following hold for a given content hash:
Commit or Failed state./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.
panelExpiresAtPast expiry, the warm layer is no longer required to serve the content. Concretely:
panelExpiresAt, reachability is best-effort.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.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.
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 field | What it tracks | Mutated when |
|---|---|---|
| Active Set | The ordered list of (PeerId, stake_weight) tuples for all nodes currently eligible to participate in rounds | A node stakes/unstakes via the IrisStaking smart contract |
| Aggregate Public Key | The 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 |
| Epoch | A 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 expiry | updateCommittee() is called on-chain with a valid old-committee threshold signature over (newEpoch, newAggregateKey, committeeHash, newMinProtocolVersion) |
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 },
}
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
}
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:
t honest participants complete all rounds successfully.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:
PendingJoin for this transition is dropped back to the unstaked pool and must re-submit its join request through governance. Its stake is not slashed (the failure is not attributable on-chain), but it forfeits the gas spent on the join transaction.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.
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.
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.
| Layer | Technology | Purpose |
|---|---|---|
| Transport | TCP/IP with DNS resolution | Reliable, ordered byte-stream transport. DNS allows nodes to advertise human-readable addresses (/dns4/bootnode.iris.network/tcp/9000) alongside raw IPs |
| Multiplexing | Yamux | Stream 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 |
| Encryption | Noise 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 |
| Identity | ed25519 keypairs → PeerId | A 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 |
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:
ed25519 cryptographic keypair. It stores the private key securely on the local disk.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.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.
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):
ed25519 private keys.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.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)
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.
When a new node starts for the first time:
Multiaddr values from iris.toml. These are hosted by the Iris Foundation initially and are the only hardcoded entry points into the network.FIND_NODE query for its own PeerId. This self-lookup populates its routing table by discovering nodes that are close in XOR distance.PeerId to further populate its routing table and maintain diverse connections across the keyspace.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.
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;
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.
Iris defines three GossipSub topics, each carrying a specific message type at a specific phase of the round:
| Topic | Message Type | Payload Size | Published By | Consumed By | Round Phase |
|---|---|---|---|---|---|
iris/requests/v1 | DataRequest | ~200 bytes | Relayer (one node) | All nodes | Idle → Observing |
iris/observations/v1 | TlsCommitment (Phase 1) | ~250 bytes | All Regular Nodes | Leader Node | Observing (Phase 1, ≤60s) |
iris/observations/v1 | Manifest (Phase 2) | ~500 bytes | All Regular Nodes | Leader Node | Observing (Phase 2, ≤300s) |
iris/consensus/v1 | Proposal / 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.4 | Leader Node (Proposal) / Regular Nodes (BLSShare/Rejection) | All nodes | PreCommit → Voting → Commit |
GossipSub v1.1 maintains a mesh of D = 6 peers per topic (configurable via iris.toml). The mesh parameters are:
| Parameter | Value | Rationale |
|---|---|---|
D (target mesh degree) | 6 | Balances redundancy against bandwidth. Each message is forwarded to 6 peers |
D_low (minimum mesh degree) | 4 | Below this, the node GRAFTs additional peers into the mesh |
D_high (maximum mesh degree) | 12 | Above this, the node PRUNEs excess peers to limit fan-out |
D_lazy (gossip factor) | 6 | Number of peers to whom the node sends IHAVE control messages for messages not directly forwarded |
| Heartbeat interval | 1 second | How often the node evaluates mesh health and peer scores |
| Message TTL | 600 seconds | Messages 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 |
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.
All GossipSub messages are serialized using CBOR (Concise Binary Object Representation) via the ciborium crate. CBOR was chosen over Protobuf for the MVP because:
serde ecosystem in Rust provides zero-cost serialization with #[derive(Serialize, Deserialize)].Crate note:ciboriumis used instead of the olderserde-cborcrate, which has been unmaintained since 2021.ciboriumis the actively maintained CBOR implementation for theserdeecosystem 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.
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.
/iris/geotiff/1.0.0)| Field | Value |
|---|---|
| Protocol ID | /iris/geotiff/1.0.0 |
| Transport | libp2p RequestResponse behaviour over the existing Yamux-multiplexed connection |
| Request payload | GeotiffRequest { image_hash: ContentHash } — the BLAKE2b-512 hash of the desired GeoTIFF, as advertised in the sender's manifest |
| Response payload | GeotiffResponse { data: Vec<u8> } — the raw GeoTIFF bytes, streamed in chunks |
| Timeout | 3600 seconds |
| Max payload size | 16 GB |
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.
Upon receiving a GeoTIFF payload, the Leader Node:
BLAKE2b(payload) and verifies it matches the image_hash from the sender's manifest.attestation_hash references a valid, verifiable TEE attestation document (either cached locally or fetched from the sender via a separate request).~/.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.
/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).
| Field | Value |
|---|---|
| Protocol ID | /iris/attestation/1.0.0 |
| Transport | libp2p RequestResponse behaviour over the existing Yamux-multiplexed connection |
| Request payload | AttestationRequest { attestation_hash: AttestationHash } — the BLAKE2b-512 hash of the desired attestation document, as advertised in the sender's Manifest as attestation_hash |
| Response payload | AttestationResponse::Found { data: Vec<u8> } (raw .att envelope bytes) or AttestationResponse::NotFound |
| Timeout | 120 seconds |
| Max payload size | 256 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.
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:
DataRequest (~200 B) → TlsCommitment (~250 B × n) → Manifest (~500 B × n) → Proposal (~2 KB at n=20, scaling to ~40 KB at n=100 because the full similarity matrix is embedded per §7.4) → BLSShare (~300 B × n). For a 20-node committee, total GossipSub traffic per round is roughly 20–30 KB; for a 100-node committee, ~150 KB. Either is dwarfed by the direct-stream traffic below.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.
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,
}
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
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.
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:
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.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 upstreamtlsncrate is alpha-quality, that MPC preprocessing scales linearly withmax_recv_data(263 ms for 16 KiB on alpha-14), and that direct attestation of multi-GB GeoTIFF payloads cannot meet the 60 sTlsCommitmentbudget 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.
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.
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:
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.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.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.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).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.
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.
| Field | Description |
|---|---|
| Attestor | Tagged 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 Chain | The 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 |
| Measurement | Vendor-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 Data | The 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 (uint8 — L0, L1A, L1B, L1C, L1Grd, L1Slc, L2A, L2Ocn, Ard), and radiometric_units (uint8 — Dn, 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 Identity | The 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 Hash | BLAKE2b-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 Timestamp | Wall-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.
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).
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.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.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.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.DataRequest — i.e., the bounding box and timestamp in the HTTP request are consistent with the AoI being reconstructed.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).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 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.
user_data. This is what makes the attestation document a transferable proof.MRENCLAVE / PCR0 / MRTD value it signs.user_data. Two structural mitigations bound the blast radius: (a) the BFT threshold (t > 2n/3) absorbs a single forged observation, and (b) Phase 3 below requires the BFT-quorum subset of every round to span ≥2 distinct vendors, so a single-vendor break cannot swing a round on its own.Authorization header to the host. This is a credential-loss concern for the affected operator (a single API account) rather than a network-wide safety concern; standard credential-rotation procedures apply.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.)
| Phase | Accepted attestors | Product phase | Description |
|---|---|---|---|
Phase 0 — Mock attestor only | MockAttestor 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 whitelist | AWS 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 TDX | Phase 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 requirement | Phase 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 |
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.For node operators who want to self-host rather than rent cloud TEEs:
c6i.2xlarge (8 vCPU, 16 GB) at ~$245/mo on-demand or ~$110/mo with a 3-year reserved instance comfortably runs an Iris node including the enclave.MockAttestor fixture in iris-tools::attestation_fixture produces deterministic test attestations signed by a fixture root, accepted only on devnet, and lets the entire test suite run on a laptop with no TEE hardware. Periodic real-attestation integration tests can use GCP Confidential VMs (free $300 credit) or a single AWS Nitro enclave instance.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.
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 iniris-normalize::orthois a pure-Rust per-pixel bilinear sampler with a parallax correctiondelta_row_px = h_terrain * tan(θ_view) / gsd_m_per_rowapplied at every output coordinate. The per-pixel terrain heighth_terraincomes from the configuredDemResamplertrait —ConstantPlaneResamplerat Demo,BilinearResamplerat MVP, governed by the IrisDEM tier in force. Two entry points coexist:orthorectify_flat_earth(legacy reference, used byiris-demosynthetic-imagery paths and the[3.6.3]determinism test) andorthorectify_with_dem(the production default — publicorthorectifydispatches here). When the resampler returnsh = 0everywhere, the two paths produce bit-identical output withinconsensus.matrix_toleranceso 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 usesDEFAULT_VIEW_ZENITH_DEG(10°, the Demo placeholder fromarchitecture.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 GDALwarpAPI does not expose a clean per-pixel hook for theDemResamplerseam 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 perdesign_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_leveland 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 withUnsupportedAtDemo; 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.
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) | Handler | State 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 tuple | Err(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.
The network computes three independent metrics on aligned tensor pairs (Ā, B̄):
μ₁): Linear penalty for total absolute spatial deviation across all bands and pixels.μ₂): Quadratic penalty that heavily punishes localized, extreme anomalies (e.g., malicious pixel manipulation) while being forgiving of uniform noise.μ₃): The N-dimensional angle between pixel vectors. Isolates changes in actual physical materials (spectral signature) while being completely blind to changes in illumination or shadow intensity.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 ofAlignedImages is comparable iff (a) both have the same band count, and (b) per-band centre wavelengths agree withinnormalization.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_compatiblehelper enforces this as a precondition insidecompare(); the resultingNormalizeError::BandMismatchcarries a per-band breakdown that flows up intoTier-1Rejectiondiagnostics. The harder cross-vendor band-mapping problem (thedata_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 (withinconsensus.matrix_tolerance, default1e-6) — otherwise the §7.4 step-2 self-edge check fails on heterogeneous fleets. The implementation guarantees this via: (1) norayonparallelism inside per-cell reductions (parallelf64summation isn't associative); (2) fixed row-major iteration order viandarray::iter().zip(); (3)f64accumulators overf32inputs; (4)acos()clamped to[-1, 1]against floating-point overshoot; (5) identical-vector short-circuit before thearccos(1 - epsilon)round-off boundary. Thecross-arch-determinism.ymlCI workflow runs a fixed-seed fingerprint test onx86_64-unknown-linux-gnuANDaarch64-unknown-linux-gnuand rejects PRs whose matrices diverge.
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.
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.
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.
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.
| Input | Definition (EVM MVP) | Where it comes from |
|---|---|---|
block_hash | The Keccak-256 block-header hash of the host-chain block in which the DataRequest event was emitted | Each 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_id | bytes32 identifier of the on-chain DataRequest event | Generated by the Requester contract; included in the event payload that the Relayer transports to GossipSub |
total_stake | Sum 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).
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":
> 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).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.
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:
state = Failed with the elapsed-deadline reason.threat_model.html §3.1.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.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.
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.
Within 60 seconds of the DataRequest being received, each Regular Node must:
iris-fetch TEE enclave with the request parametersTlsCommitment 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.
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.
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.
TlsCommitment and ManifestBoth 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.
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.
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.
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.
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.
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./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.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.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.
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:
spot_i's GeoTIFF via /iris/geotiff/1.0.0.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.
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.
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.
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:
.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.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).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.
n private key shares.t-of-n partial signatures combine into a single 48-byte BLS signature that is verifiable by anyone holding the aggregate public key.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.
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.
The Iris on-chain layer comprises three protocol-deployed contracts and one interface that Requester dapps implement:
| Contract | Role | Trust Surface |
|---|---|---|
IrisVerifier | Stores 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) |
IrisStaking | Custodies 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 |
IrisGovernance | Approves 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.
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.
A new epoch is created by exactly one of three events, all routed through IrisGovernance:
| Trigger | Initiator | Governance gate | Effect |
|---|---|---|---|
| Committee membership change | Any staking member via IrisGovernance.proposeCommitteeChange() | ≥67% vote | New DKG ceremony + new epoch |
| Protocol version bump | Any staking member via IrisGovernance.proposeUpgrade(newMinProtocolVersion) | ≥67% vote | minProtocolVersion bump + new epoch |
| Scheduled key rotation | Automatic, on expiry of epoch TTL parameter | No vote required | DKG 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.
Committee rotations are gated on three invariants:
updateCommittee cannot be a source of new keys — it can only ratify what DKG has already produced.(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.newEpoch must equal currentEpoch + 1. This prevents replay of stale rotation messages.When a node detects an UpgradeScheduled event from IrisGovernance:
newMinProtocolVersion, it enters WaitingForUpgrade state: it continues serving rounds under the current epoch but does not accept new committee assignments for the target epoch.committeeChanging = true, the node participates in the Feldman VSS DKG ceremony before updateCommittee is called.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.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.
The protocol does not enforce notice windows — only epoch monotonicity. The following minimums should be observed as governance policy:
| Network phase | Minimum notice | Rationale |
|---|---|---|
| Devnet | None | Single-operator environment |
| Public testnet | 1 week | Multi-operator; CI/CD runway needed |
| Mainnet early | 4 weeks | Institutional operators; audited upgrades |
| Mainnet stable | Governance-decided per transition | Upgrades become rare |
IrisVerifier stores two values that define the scheduled rotation window:
epochRotationTtl() — seconds after the last transition before a re-key is due. 0 disables scheduled rotation entirely (devnet default).epochLastTransitionAt() — block.timestamp of the most recent updateCommittee call, updated on every successful rotation.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:
| Phase | epochRotationTtl | Notes |
|---|---|---|
| Devnet | 0 (disabled) | Event-driven only |
| Testnet | 30 days | Exercises re-key code path |
| Mainnet early | 90 days | Quarterly; forward secrecy vs. ceremony cost balance |
| Mainnet stable | Governance-decided | Tunable 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).
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.
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.
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:
IrisGovernance: when multi-chain expansion ships post-MVP, the verifier abstraction is re-introduced by deploying a new IrisVerifier whose constructor accepts the verifier address and rotating the committee through it. The MVP does not pay this cost up front.Even within a single-chain target, two failure modes the architecture must tolerate are documented in threat_model.html §4.3:
IrisVerifier and rotate the committee through IrisGovernance.When the protocol targets chains beyond Ethereum testnet, the verifier abstraction returns. Specifically:
blsVerifier constructor parameter is reintroduced on IrisVerifier, selecting between the precompile-wrapper contract and a deployed bls12-381-solidity library instance.governance.html for fee-schedule sizing.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.
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 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;
}
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.
| Outcome | What IrisVerifier does |
|---|---|
| Callback succeeds | Emits CallbackSucceeded, sets reports[requestId].callbackSucceeded = true |
| Callback reverts with reason | Emits CallbackFailed(requestId, requester, reason); report stays recorded |
| Callback runs out of gas | Caught by catch; same as revert |
| Reentrancy attempt | Blocked by per-request reentrancy guard around deliverReport |
Requester does not implement IIrisReceiver | Caught 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.
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.
| Operation | Gas (EIP-2537 path) | Notes |
|---|---|---|
deliverReport (callback succeeds) | 200–300k | Dominated by the BLS pairing |
deliverReport (callback fails) | 250–350k | Adds CallbackFailed event + 100k callback budget consumed |
claimReport | 50–80k | No BLS check; Requester-authorized read+ack |
updateCommittee | 250–400k | BLS verify + storage writes for new key + epoch + committee hash + minProtocolVersion; TTL-triggered calls additionally transfer epochRotationBounty to caller |
commitReputationRoot | 100–150k | BLS verify + single 32-byte storage write |
initializeCommittee (one-shot) | 80–120k | Owner-only, no BLS check on first commit |
extendPersistence | 60–100k | Single storage write to panelExpiresAt; emits PersistenceExtended; no BLS check (anyone can fund) |
reportFailure | 80–130k | Deadline check + merkle root write + RoundFailed event + fee refund transfer; no BLS check |
submitFraudProof | 150–250k | Signature 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.
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.
DataRequest events from the host chain to GossipSub, or refuse to submit finalized reports back on-chain. The network has no on-chain proof of refusal — only the absence of a relay.IrisVerifier signature check (the aggregated BLS signature commits to the exact (request_id, ipfs_cid) pair), but each rejection still consumes gas and can be used to grief the relaying address.t-of-n BLS share access, a Relayer cannot construct a valid threshold signature. Any submitted report not signed by a real committee quorum fails at the precompile.(request_id, ipfs_cid) pair. Substituting a different CID invalidates the signature.DataRequest: Requests are sourced from on-chain event logs, not from Relayer claims. A Relayer cannot inject a phantom request — committee members would not observe it on-chain when cross-checking.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 is computed off-chain by every committee member from observable round events. Inputs include:
TlsCommitment, a Phase 2 Manifest, and a valid BLSShare attested by the Leader Node.Penalties and decay:
threat_model.html) zeros or sharply reduces reputation alongside the stake slash.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.
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.
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.
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.)
| Phase | Operator | Product phase | Description |
|---|---|---|---|
| Phase 1 — Foundation Relayer | Iris 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 Relayers | Iris 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-Gated | Any 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 |
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).
Every round failure belongs to one of three categories:
| Category | Definition | Response |
|---|---|---|
| Transient | Temporary conditions (node dropout, API jitter, Leader timeout) where a retry is likely to succeed | RoundFailed event + partial fee refund; Requester resubmits |
| Attributable | Identifiable misbehavior from a specific node with on-chain evidence | RoundFailed + submitFraudProof triggers stake slash via IrisStaking |
| Permanent | Conditions that cannot be resolved by retry (BFT threshold violated, Provider discontinued) | RoundFailed(Permanent) + full fee refund; resubmission gated by cooldown |
FailureReason Enumenum 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 AuthorizationTlsCommitment or Manifest for that request_id (round participants only). Participants have direct knowledge of why the round failed and produce the most accurate reason code.On reportFailure, contributor credits are distributed from the fee pool before refunding the Requester. Default credit tiers (governance-tunable):
| Work completed | Credit (% of base contributor share) |
|---|---|
Phase 1 only (TlsCommitment published) | 15% |
Phase 1 + Phase 2 (Manifest published) | 60% |
| Phase 1 + Phase 2 + BLS share submitted | 80% |
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.
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.
[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:
fraudType | Evidence required | Slashes |
|---|---|---|
0 — BadMedoid | (i, j) matrix cell + both contributor GeoTIFF hashes proving the Leader's similarity score is wrong | Leader Node stake |
1 — LeaderEquivocation | Two signed Proposal messages with the same request_id, different ipfs_cid, same Leader ed25519 signature | Leader 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:
FraudType::InvalidAttestation — slashes a contributor whose Manifest references an attestation that fails the 7-step §5.4 verifier path (post-MVP follow-up to [5.9]).FraudType::WrongSensorClaimed — slashes a contributor whose on-chain sensor ID disagrees with the sensor_id in their attestation's user_data binding. Becomes reachable as soon as iris-fetch parses sensor identity in-enclave (per design_q1_effective_nadir_angle.md §1.2.1.b, decided "all phases from demo onward"). The fraud-proof evidence is the attestation document itself plus the contributor's on-chain Manifest — the verifier recomputes the user_data BLAKE2b binding with the claimed sensor and asserts mismatch.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.
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:
| Decision | Value |
|---|---|
| MVP failure reporting | reportFailure callable by round participants (T1 → T1+120s), then any address permissionlessly |
FailureReason verification | Unverified in MVP (advisory); fraud proofs add attribution in Phase 2 |
| Fee refund on failure | Partial: contributors credited per the credit schedule above, remainder refunded to Requester |
| Automatic retry in MVP | No — Requester resubmits; optional auto_resubmit flag escrows a second fee at submission |
| Automatic retry in Phase 2 | Yes — k=3 Leader rotation for LeaderTimeout only; same fee pool, no Requester resubmission |
| Slashing for attributable failures | Phase 2 — submitFraudProof for BadMedoid and LeaderEquivocation first |
| Permanent failure handling | Phase 2 — FailureReason.Permanent gates resubmission with a governance-tunable cooldown |
ReportDelivered observability | Add contributorCount and thresholdUsed fields; no new event type |
LeaderEquivocation detection vs. slashing | DuplicateReport guards contract state; submitFraudProof(LeaderEquivocation) punishes the equivocator |
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.
To tie everything together, here is a single request traced through all four state layers:
t = 5.DataRequest event on Ethereum. The Relayer picks it up and publishes it to iris/requests/v1.Idle → Observing.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.Aggregating. The Leader Node retrieves all GeoTIFFs, runs the normalization pipeline, selects the Average Scenario, and pins it to IPFS.Pre-Commit and broadcasts the proposal.Voting, broadcasting their BLS signature shares.Commit.
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;
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:
minProtocolVersion can only increase, never decrease. ProtocolVersionRegression is an unconditional hard revert — no governance override, no emergency exception. If a bad version is pushed, the response is always a forward patch (v1.2.1) via the expedited upgrade path, not a rollback to v1.1.x. See versioning_strategies.md OQ-3 for the full rationale."iris/v" || epoch_number || "/" binds every threshold signature to the epoch that produced it, preventing cross-epoch replay.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.
Iris uses two hash functions with deliberately separate roles:
| Function | Used for | Rationale |
|---|---|---|
| BLAKE2b-512 | ContentHash (GeoTIFF payloads), AttestationHash (.att files), local cache addressing | Bulk off-chain hashing of multi-GB payloads |
| Keccak-256 | Leader election seed keccak256(block_hash ‖ request_id) | Must be reproduced cheaply in EVM smart contracts |
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.
tokio streams, completing the cryptographic verification well within the bounds of a single consensus phase. Hashing is not the network bottleneck.Network transfer is the primary time-constraining factor due to the large payload sizes:
geotiff_timeout_seconds = 3600).The Data Normalization Engine must hold multiple large tensors in memory to calculate pairwise similarity scores:
f32 or f64 multi-dimensional arrays (tensors).The mathematical processing (Orthorectification, Mean Absolute Distance, Spectral Angle Mapper) is highly parallelizable:
ndarray and parallel iterators (rayon) allows the system to utilize all available CPU cores. Alternatively, nodes configured with prosumer GPUs (e.g., RTX 3090/4090 or equivalent) can drastically reduce the required time to compute the similarity matrix 𝒮(μ) from minutes to seconds by executing tensor operations natively on the GPU hardware.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.
Three complementary layers are required:
| Layer | Technology | Purpose |
|---|---|---|
| Structured logs | tracing crate with JSON subscriber | Per-event, human- and machine-readable records. Every significant state transition emits a log line with correlated context fields |
| Metrics | prometheus counters/gauges/histograms via metrics + metrics-exporter-prometheus | Aggregated, time-series data scraped by Prometheus and visualised in Grafana. Suitable for alerting thresholds |
| Distributed traces | tracing-opentelemetry + OTLP export | Span-level correlation across async tasks and crate boundaries. Enables per-round latency breakdown from DataRequest receipt to deliverReport submission |
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.
| Span | Crate | Fields |
|---|---|---|
round | iris-consensus | request_id, leader_id, epoch, contributor_count |
observing.phase1 | iris-data | request_id, provider, tls_session_id |
observing.phase2 | iris-data | request_id, image_hash, download_bytes |
aggregating.transfer | iris-network | request_id, peer_id, bytes, duration_ms |
aggregating.normalize | iris-normalize | request_id, n_contributors, selected_index |
voting.collect | iris-consensus | request_id, shares_received, shares_needed |
commit.deliver | iris-network | request_id, tx_hash, gas_used |
epoch.transition | iris-network | old_epoch, new_epoch, min_protocol_version |
All metrics are prefixed iris_. The following are the minimum set for a production-ready node:
| Metric | Type | Description |
|---|---|---|
iris_rounds_started_total | Counter | Incremented on each DataRequest received |
iris_rounds_completed_total | Counter | Labels: outcome={success,failed} |
iris_round_failure_reason_total | Counter | Labels: reason={InsufficientCommitments,InsufficientManifests,LeaderTimeout,InsufficientSignatures,IPFSPinFailure,Unknown} |
iris_round_duration_seconds | Histogram | End-to-end round latency. Buckets: 30s, 60s, 120s, 240s, 360s, 600s |
iris_contributor_count | Gauge | BLS shares received in the last completed round |
iris_leader_elections_total | Counter | Labels: outcome={elected,failed} |
| Metric | Type | Description |
|---|---|---|
iris_connected_peers | Gauge | Current peer count |
iris_gossipsub_mesh_size | Gauge | Per-topic mesh peer count. Labels: topic |
iris_peer_score | Histogram | Distribution of GossipSub peer scores across connected peers |
iris_geotiff_transfer_bytes_total | Counter | Labels: direction={inbound,outbound} |
iris_geotiff_transfer_duration_seconds | Histogram | Latency of completed GeoTIFF transfers |
iris_geotiff_transfer_failures_total | Counter | Labels: reason={hash_mismatch,timeout,connection_reset} |
| Metric | Type | Description |
|---|---|---|
iris_uptime_seconds | Gauge | Seconds since node start |
iris_epoch | Gauge | Current on-chain epoch |
iris_protocol_version | Gauge | Running binary's packed ProtocolVersion uint32 |
iris_cache_bytes_used | Gauge | GeoTIFF cache disk usage |
iris_cache_evictions_total | Counter | LRU evictions triggered by disk pressure |
iris_reputation_score | Gauge | Node's current off-chain reputation score |
Every iris-node binary exposes an HTTP health endpoint on a configurable port (default 9001):
| Path | Returns | Healthy when |
|---|---|---|
GET /health/live | 200 OK | Process is running |
GET /health/ready | 200 OK / 503 | Node is meshed on all three GossipSub topics AND connected to ≥ D_low (4) peers AND on-chain epoch matches local state |
GET /metrics | Prometheus text format | Always — 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:
| Alert | Condition | Severity |
|---|---|---|
IrisNodeDown | iris_uptime_seconds absent for > 2 min | Critical |
IrisRoundFailureSpike | rate(iris_rounds_completed_total{outcome="failed"}[5m]) > 0.5 | Warning |
IrisLeaderTimeoutSpike | rate(iris_round_failure_reason_total{reason="LeaderTimeout"}[10m]) > 0.3 | Warning |
IrisMeshDegraded | iris_gossipsub_mesh_size < 3 for any topic | Warning |
IrisPeerCountLow | iris_connected_peers < 5 | Warning |
IrisCachePressure | iris_cache_bytes_used / node_filesystem_size > 0.85 | Warning |
IrisEpochStale | On-chain epoch via RPC ≠ iris_epoch for > 5 min | Critical |
IrisVersionBelowFloor | iris_protocol_version < on_chain_min_protocol_version | Critical |
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:
iris_rounds_completed_total{outcome="success"} / iris_rounds_started_total over 1hiris_round_duration_seconds histogramiris_round_failure_reason_total by reasoniris_gossipsub_mesh_size per topic over timeiris_geotiff_transfer_bytes_totaliris_peer_scoreiris_epoch vs. time with annotations at epoch transitionsThe 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:
ed25519-bound PeerIDs prevent node impersonation, while GossipSub v1.1 peer scoring drops malicious or spamming nodes, thwarting Eclipse and Sybil attacks.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.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.
parameters.htmlThe 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.