@@ -73,7 +73,8 @@ rules. Two independent operators serving the same API with the same config produ
7373
7474** Why:** Specification-bound IDs enable cross-operator comparability without a registry. A quorum verifier can confirm
7575that multiple airnodes signed data for the same endpoint ID — meaning they all committed to calling the same API the
76- same way. When TLS proofs mature, the endpoint ID can be verified against the proven HTTP request on-chain.
76+ same way. TLS proofs extend this: the endpoint ID can be cross-checked against the proven HTTP request that backs the
77+ response.
7778
7879## Signature format
7980
@@ -82,7 +83,126 @@ v1 signed `keccak256(requestId, timestamp, airnodeAddress, data)` where the requ
8283
8384v2 signs ` keccak256(encodePacked(endpointId, timestamp, data)) ` where the endpoint ID is derived from the API spec. The
8485endpoint ID, timestamp, and data are separate top-level fields — not nested inside another hash — so on-chain contracts
85- and future TLS proof verifiers can inspect each field independently.
86+ and TLS proof verifiers can inspect each field independently.
87+
88+ ## TLS proofs for data provenance
89+
90+ v1 had no way to prove that signed data actually came from the claimed upstream API. The EIP-191 signature only proved
91+ _ who_ signed — not _ where the data came from_ . A compromised or dishonest operator could fabricate responses and sign
92+ them.
93+
94+ v2 integrates [ TLS proofs] ( /docs/concepts/proofs ) via [ Reclaim Protocol] ( https://reclaimprotocol.org/ ) . When enabled, an
95+ independent attestor participates in the upstream TLS session over MPC-TLS and signs a claim that the response actually
96+ came from the declared HTTPS endpoint and matched the configured ` responseMatches ` patterns. Airnode attaches the proof
97+ to the response alongside the signature.
98+
99+ ``` yaml
100+ settings :
101+ proof :
102+ type : reclaim
103+ gatewayUrl : http://localhost:5177/v1/prove
104+
105+ apis :
106+ - name : CoinGecko
107+ # ...
108+ endpoints :
109+ - name : coinPrice
110+ # ...
111+ responseMatches :
112+ - type : regex
113+ value : ' "usd":\s*(?<price>[\d.]+)'
114+ ` ` `
115+
116+ Proof generation is **non-fatal** — if the gateway is unavailable, Airnode still returns the signed response without the
117+ ` proof` field and logs a warning. Consumers that require provenance simply reject responses that lack a `proof`.
118+
119+ **Why:** Signatures answer "who endorsed this data." TLS proofs answer "did this data really come from the API." Pairing
120+ them turns an airnode from a trusted relay into a verifiable relay — the operator can no longer forge upstream responses
121+ undetected.
122+
123+ # # A real plugin system
124+
125+ v1 had no plugin mechanism — custom behaviour meant forking the node. Every custom auth check, metric, or response
126+ transform bled into a maintenance burden the operator carried alone.
127+
128+ v2 exposes a [plugin system](/docs/plugins) with six hooks that fire at well-defined points in the request pipeline :
129+
130+ | Hook | Type | When it fires |
131+ | ----------------- | ----------- | -------------------------------------- |
132+ | `onHttpRequest` | Mutation | After endpoint resolution, before auth |
133+ | `onBeforeApiCall` | Mutation | Before the upstream API call |
134+ | `onAfterApiCall` | Mutation | After the upstream API responds |
135+ | `onBeforeSign` | Mutation | After encoding, before signing |
136+ | `onResponseSent` | Observation | After the signed response is sent |
137+ | `onError` | Observation | When an error occurs at any stage |
138+
139+ Plugins are ordinary modules loaded from a path in config, with per-request time budgets enforced by the runtime.
140+ Mutation hooks that fail or time out **drop** the request (fail-closed — no data leaks past a broken security plugin);
141+ observation hooks are fire-and-forget.
142+
143+ The pipeline is powerful enough that several headline v2 capabilities are built as plugins rather than core features :
144+
145+ - **`fhe-encrypt`** — encrypts the signed payload for on-chain confidential compute (see below)
146+ - **`encrypted-channel`** — ECIES-encrypts responses end-to-end to a requester's ephemeral key
147+ - **`heartbeat`**, **`logger`**, **`slack-alerts`** — operational observability
148+
149+ **Why:** Airnode operators have wildly different needs — custom authorization, bespoke upstream protocols, private
150+ metrics, paid-data gating. A stable hook surface lets those live alongside the core node instead of forking it, and
151+ keeps the core small enough to audit.
152+
153+ # # FHE encryption for confidential on-chain data
154+
155+ v1 had no notion of confidential data. Every signed value was public the moment it landed on-chain — visible in calldata
156+ before inclusion (enabling front-running) and readable from storage afterward (making it impossible to sell exclusive
157+ data or keep valuations private).
158+
159+ v2 ships an [FHE encryption plugin](/docs/concepts/fhe-encryption) built on [Zama's fhEVM](https://docs.zama.ai/fhevm).
160+ The plugin intercepts the ABI-encoded response in the `onBeforeSign` hook, encrypts it with the target chain's FHE
161+ public key, and packs the resulting `(einput, inputProof)` pair as the new `data` field. Airnode signs the ciphertext,
162+ so the signature proves the encrypted data is authentic without ever revealing plaintext.
163+
164+ ```
165+ API response → ABI encode → [ fhe-encrypt plugin] → sign(ciphertext) → return to client
166+ ↓
167+ encrypt with chain's FHE public key
168+ pack (einput, inputProof) into data field
169+ ```
170+
171+ Because FHE is homomorphic, the callback contract can compute directly on the ciphertext —
172+ `TFHE.gt(price, liquidationThreshold)` returns an encrypted boolean without either value ever becoming public.
173+ Per-handle on-chain ACLs determine who is allowed to decrypt.
174+
175+ **Why:** Public oracle data leaks value. Searchers front-run price updates, premium data leaks to non-payers the instant
176+ it's consumed, and confidential valuations can't be delivered at all. FHE lets contracts use oracle data while it stays
177+ encrypted — enabling MEV-protected feeds, paid-data access control, sealed auctions, and confidential RWA pricing on the
178+ same signing and verification path as any other Airnode response. The existing `AirnodeVerifier` contract works
179+ unchanged.
180+
181+ ## Response caching
182+
183+ v1 had no response cache. Every request hit the upstream API, which was wasteful for endpoints with long-lived data
184+ (e.g. daily FX rates) and couldn't absorb bursts without rate-limiting the origin.
185+
186+ v2 has an in-memory response cache with configurable TTL, keyed by `(endpointId, sorted parameters)`. Cache config is
187+ set per-API and can be overridden per-endpoint:
188+
189+ ```yaml
190+ apis:
191+ - name: CoinGecko
192+ cache:
193+ maxAge: 30000 # 30 seconds
194+ endpoints:
195+ - name: coinPrice
196+ # inherits the 30s cache
197+ - name: realtimeTicker
198+ cache:
199+ maxAge: 1000 # override to 1 second
200+ ```
201+
202+ Entries are bounded (10,000 entries by default) and swept on a periodic timer. No external cache server is required.
203+
204+ ** Why:** Long-running processes can hold state — caching is free in this model and valuable in practice. Most oracle
205+ endpoints are called far more often than their underlying data changes.
86206
87207## Other improvements
88208
@@ -97,21 +217,6 @@ per endpoint (any-of semantics).
97217v2 endpoints support three modes: ` sync ` (default request-response), ` async ` (return 202, poll for result), and ` stream `
98218(Server-Sent Events). v1 only supported synchronous request-response.
99219
100- ### Plugin system
101-
102- v2 has a plugin system with six hooks (` onHttpRequest ` , ` onBeforeApiCall ` , ` onAfterApiCall ` , ` onBeforeSign ` ,
103- ` onResponseSent ` , ` onError ` ) and per-request time budgets. v1 had no plugin mechanism — custom logic required forking
104- the node.
105-
106- ### Caching
107-
108- v2 caches responses in memory with configurable TTL.
109-
110- ### Solidity contract
111-
112- v2 uses a single minimal Solidity contract (AirnodeVerifier) instead of v1's 30+ contracts. The test suite includes
113- unit, invariant (stateful fuzz), and symbolic (Halmos) tests.
114-
115220### Language and runtime
116221
117222v1 was a TypeScript monorepo with 10+ packages, Hardhat for testing, and ethers.js for chain interaction. v2 is a single
0 commit comments