ccip-router 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +528 -0
- package/contracts/AttestationIndex.sol +125 -0
- package/contracts/NodeRegistry.sol +85 -0
- package/dist/attestation/eip712.d.ts +50 -0
- package/dist/attestation/eip712.d.ts.map +1 -0
- package/dist/attestation/eip712.js +31 -0
- package/dist/attestation/eip712.js.map +1 -0
- package/dist/attestation/ocp.d.ts +9 -0
- package/dist/attestation/ocp.d.ts.map +1 -0
- package/dist/attestation/ocp.js +11 -0
- package/dist/attestation/ocp.js.map +1 -0
- package/dist/attestation/withWyriwe.d.ts +11 -0
- package/dist/attestation/withWyriwe.d.ts.map +1 -0
- package/dist/attestation/withWyriwe.js +92 -0
- package/dist/attestation/withWyriwe.js.map +1 -0
- package/dist/chain/abi.d.ts +178 -0
- package/dist/chain/abi.d.ts.map +1 -0
- package/dist/chain/abi.js +94 -0
- package/dist/chain/abi.js.map +1 -0
- package/dist/chain/client.d.ts +16762 -0
- package/dist/chain/client.d.ts.map +1 -0
- package/dist/chain/client.js +21 -0
- package/dist/chain/client.js.map +1 -0
- package/dist/chain/publish.d.ts +26 -0
- package/dist/chain/publish.d.ts.map +1 -0
- package/dist/chain/publish.js +66 -0
- package/dist/chain/publish.js.map +1 -0
- package/dist/chain/register.d.ts +9 -0
- package/dist/chain/register.d.ts.map +1 -0
- package/dist/chain/register.js +20 -0
- package/dist/chain/register.js.map +1 -0
- package/dist/crypto/hash.d.ts +3 -0
- package/dist/crypto/hash.d.ts.map +1 -0
- package/dist/crypto/hash.js +12 -0
- package/dist/crypto/hash.js.map +1 -0
- package/dist/crypto/index.d.ts +3 -0
- package/dist/crypto/index.d.ts.map +1 -0
- package/dist/crypto/index.js +3 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/crypto/sign.d.ts +6 -0
- package/dist/crypto/sign.d.ts.map +1 -0
- package/dist/crypto/sign.js +60 -0
- package/dist/crypto/sign.js.map +1 -0
- package/dist/db/index.d.ts +5 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +13 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +6 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +41 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/sqlite.d.ts +19 -0
- package/dist/db/sqlite.d.ts.map +1 -0
- package/dist/db/sqlite.js +196 -0
- package/dist/db/sqlite.js.map +1 -0
- package/dist/db/types.d.ts +34 -0
- package/dist/db/types.d.ts.map +1 -0
- package/dist/db/types.js +5 -0
- package/dist/db/types.js.map +1 -0
- package/dist/gateway/__tests__/eip3668.test.d.ts +2 -0
- package/dist/gateway/__tests__/eip3668.test.d.ts.map +1 -0
- package/dist/gateway/__tests__/eip3668.test.js +31 -0
- package/dist/gateway/__tests__/eip3668.test.js.map +1 -0
- package/dist/gateway/eip3668.d.ts +14 -0
- package/dist/gateway/eip3668.d.ts.map +1 -0
- package/dist/gateway/eip3668.js +28 -0
- package/dist/gateway/eip3668.js.map +1 -0
- package/dist/lib.d.ts +12 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +8 -0
- package/dist/lib.js.map +1 -0
- package/dist/mesh/cron.d.ts +4 -0
- package/dist/mesh/cron.d.ts.map +1 -0
- package/dist/mesh/cron.js +22 -0
- package/dist/mesh/cron.js.map +1 -0
- package/dist/mesh/records.d.ts +4 -0
- package/dist/mesh/records.d.ts.map +1 -0
- package/dist/mesh/records.js +47 -0
- package/dist/mesh/records.js.map +1 -0
- package/dist/mesh/sync.d.ts +5 -0
- package/dist/mesh/sync.d.ts.map +1 -0
- package/dist/mesh/sync.js +154 -0
- package/dist/mesh/sync.js.map +1 -0
- package/dist/mesh/vni.d.ts +14 -0
- package/dist/mesh/vni.d.ts.map +1 -0
- package/dist/mesh/vni.js +52 -0
- package/dist/mesh/vni.js.map +1 -0
- package/dist/router/CcipRouter.d.ts +26 -0
- package/dist/router/CcipRouter.d.ts.map +1 -0
- package/dist/router/CcipRouter.js +85 -0
- package/dist/router/CcipRouter.js.map +1 -0
- package/dist/router/index.d.ts +3 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +2 -0
- package/dist/router/index.js.map +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Echo-Merlini (dinamic.eth)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
# ccip-router
|
|
2
|
+
|
|
3
|
+
**The coordination layer CCIP-Read was missing.**
|
|
4
|
+
|
|
5
|
+
EIP-3668 defines how clients talk to CCIP-Read gateways. It says nothing about how gateways talk to each other. `ccip-router` fills that gap — peer sync, deduplication, signed records, and cryptographic attestation for any CCIP-Read gateway.
|
|
6
|
+
|
|
7
|
+
No ENS required. No agents required. Any CCIP-Read project can plug in a resolver and get a mesh-ready gateway in minutes.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
### System overview
|
|
14
|
+
|
|
15
|
+
```mermaid
|
|
16
|
+
flowchart LR
|
|
17
|
+
Client(["CCIP-Read client\nbrowser / contract"])
|
|
18
|
+
|
|
19
|
+
subgraph Node["ccip-router node"]
|
|
20
|
+
Handler["GET /{sender}/{data}.json\nEIP-3668 handler"]
|
|
21
|
+
Resolver["Resolver fn\ncustom logic"]
|
|
22
|
+
Wyriwe["withWyriwe()\nEIP-712 attestation"]
|
|
23
|
+
DB[("SQLite\nWAL · dedup · cursor")]
|
|
24
|
+
Cron["sync cron\n*/5 * * * *"]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
subgraph Peers["Peer mesh"]
|
|
28
|
+
NodeB["Node B"]
|
|
29
|
+
NodeC["Node C"]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
subgraph Sepolia["Sepolia (chain 11155111)"]
|
|
33
|
+
AI["AttestationIndex\n0x107D…3698"]
|
|
34
|
+
NR["NodeRegistry\n0x6be4…42b7"]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
Client -- "EIP-3668 request" --> Handler
|
|
38
|
+
Handler -- "{ data: 0x... }" --> Client
|
|
39
|
+
Handler --> Resolver --> Wyriwe --> DB
|
|
40
|
+
Cron -- "GET /records" --> NodeB & NodeC
|
|
41
|
+
NodeB & NodeC -- "signed records" --> DB
|
|
42
|
+
DB -. "publishAttestation()" .-> AI
|
|
43
|
+
DB -. "register(url, sig)" .-> NR
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Per-request attestation flow
|
|
47
|
+
|
|
48
|
+
```mermaid
|
|
49
|
+
sequenceDiagram
|
|
50
|
+
participant Client as CCIP-Read client
|
|
51
|
+
participant GW as ccip-router
|
|
52
|
+
participant Res as Resolver fn
|
|
53
|
+
participant DB as SQLite
|
|
54
|
+
participant Chain as AttestationIndex
|
|
55
|
+
|
|
56
|
+
Client->>GW: GET /{sender}/{data}.json
|
|
57
|
+
GW->>Res: resolve(sender, calldata, namespace)
|
|
58
|
+
Res-->>GW: response bytes
|
|
59
|
+
|
|
60
|
+
Note over GW: withWyriwe() — attestation pipeline
|
|
61
|
+
GW->>GW: rawInputHash = keccak256(calldata)
|
|
62
|
+
GW->>GW: inputHash = rawInputHash (sentinel)<br/>or keccak256(abi.encode(raw, pipelineHash))
|
|
63
|
+
GW->>GW: outputHash = keccak256(response)
|
|
64
|
+
GW->>GW: commitmentHash = keccak256(agentId · modelHash · inputHash · outputHash · ts)
|
|
65
|
+
GW->>GW: EIP-712 sign WyriweAttestation
|
|
66
|
+
GW->>DB: INSERT OR IGNORE signed attestation record
|
|
67
|
+
GW-->>Client: { data: "0x..." }
|
|
68
|
+
|
|
69
|
+
Note over DB,Chain: async — admin-triggered batch publish
|
|
70
|
+
DB->>Chain: record(attestation, sig)
|
|
71
|
+
Chain-->>DB: signerOf[commitmentHash] anchored
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Attestation stack
|
|
75
|
+
|
|
76
|
+
```mermaid
|
|
77
|
+
graph TB
|
|
78
|
+
T["EIP-3668 · Transport\nCCIP-Read client-to-gateway"]
|
|
79
|
+
S["EIP-191 · Record signing\nkeccak256(inputHash · namespace · valueHash · ts)"]
|
|
80
|
+
W["WYRIWE · Input provenance\nsentinel path: inputHash = rawInputHash\nnon-sentinel: inputHash = keccak256(abi.encode(raw, pipelineHash))"]
|
|
81
|
+
I["ERC-8004 · Agent identity\nagentId · registryAddress declared on-chain"]
|
|
82
|
+
O["OCP / ERC-8263 · Observation commitment\ncommitmentHash = keccak256(agentId · modelHash · inputHash · outputHash · ts)"]
|
|
83
|
+
A["EIP-712 · WyriweAttestation\nstructured signing · verifiable by any peer · synced by mesh"]
|
|
84
|
+
V["VNI · Node identity\nEIP-191 signed { nodeId · signerAddress · url · version · ts }"]
|
|
85
|
+
C["On-chain anchoring · Sepolia\nAttestationIndex — signerOf · commitmentOf\nNodeRegistry — register(url, sig)"]
|
|
86
|
+
|
|
87
|
+
T --> S --> W --> I --> O --> A --> V --> C
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Contracts
|
|
93
|
+
|
|
94
|
+
Both contracts are permissionless — no owner, no admin. One deployment per chain serves all nodes.
|
|
95
|
+
|
|
96
|
+
| Contract | Sepolia address | Purpose |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `AttestationIndex` | [`0x107D706112225aC57eCf6692FBbDC283fb6E3698`](https://sepolia.etherscan.io/address/0x107D706112225aC57eCf6692FBbDC283fb6E3698) | Anchors EIP-712 `WyriweAttestation` records on-chain. Stores `signerOf[commitmentHash]` and `commitmentOf[inputHash]`. |
|
|
99
|
+
| `NodeRegistry` | [`0x6be4966596A9CBaa7260ab6EbbFFA69bBC9a42b7`](https://sepolia.etherscan.io/address/0x6be4966596A9CBaa7260ab6EbbFFA69bBC9a42b7) | Public directory of nodes. `register(url, sig)` proves key ownership via EIP-191 — the relayer (`msg.sender`) does not need to be the signing key. |
|
|
100
|
+
|
|
101
|
+
Deployed by [`0xFf9a176577Fb42b6bc9c19fd05a241e8fCd0ca14`](https://sepolia.etherscan.io/address/0xFf9a176577Fb42b6bc9c19fd05a241e8fCd0ca14) · Solc 0.8.24 · optimizer 200 runs.
|
|
102
|
+
|
|
103
|
+
**To use on Sepolia:** open the admin panel → Deploy contracts → select Sepolia → "Use these addresses →". Addresses are saved to config automatically, no deployment needed.
|
|
104
|
+
|
|
105
|
+
**To deploy to another chain:** open the admin panel → Deploy contracts → select the chain → connect wallet → two transactions (one per contract). No private key is stored — MetaMask signs everything in-browser.
|
|
106
|
+
|
|
107
|
+
Source: [`contracts/AttestationIndex.sol`](contracts/AttestationIndex.sol) · [`contracts/NodeRegistry.sol`](contracts/NodeRegistry.sol)
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Two tiers
|
|
112
|
+
|
|
113
|
+
### Basic — plug in a resolver, get a gateway + mesh
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { CcipRouter } from 'ccip-router'
|
|
117
|
+
|
|
118
|
+
const ccip = new CcipRouter({
|
|
119
|
+
namespace: 'token-metadata',
|
|
120
|
+
db,
|
|
121
|
+
gatewayKey: process.env.GATEWAY_PRIVATE_KEY,
|
|
122
|
+
resolver: async (sender, calldata, namespace) => {
|
|
123
|
+
return encodeMyResponse(calldata)
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
app.route('/', ccip.hono())
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
What you get:
|
|
131
|
+
- CCIP-Read handler (`/{sender}/{data}.json`)
|
|
132
|
+
- EIP-191 signed records written to SQLite on every call
|
|
133
|
+
- Mesh peer sync (`GET /records?since=&namespace=&limit=&cursor=`)
|
|
134
|
+
- Record deduplication — same `inputHash` never inserted twice
|
|
135
|
+
- Admin dashboard at `/admin` with peer management + sync controls
|
|
136
|
+
- Setup wizard at `/setup` on first boot
|
|
137
|
+
|
|
138
|
+
### Advanced — wrap any resolver with WYRIWE EIP-712 attestation
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { CcipRouter, withWyriwe } from 'ccip-router'
|
|
142
|
+
|
|
143
|
+
const ccip = new CcipRouter({
|
|
144
|
+
namespace: 'agent-attestations',
|
|
145
|
+
db,
|
|
146
|
+
gatewayKey: config.gatewayKey,
|
|
147
|
+
resolver: withWyriwe(myAgentResolver, {
|
|
148
|
+
gatewayKey: config.gatewayKey,
|
|
149
|
+
registryAddress: process.env.REGISTRY_ADDRESS as `0x${string}`,
|
|
150
|
+
agentId: process.env.AGENT_ID as `0x${string}`,
|
|
151
|
+
modelHash: process.env.MODEL_HASH as `0x${string}`,
|
|
152
|
+
chainId: 1,
|
|
153
|
+
// sanitizationCID: 'ipfs://Qm...', // omit for sentinel (identity) path
|
|
154
|
+
}),
|
|
155
|
+
})
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
What `withWyriwe()` adds on top of basic:
|
|
159
|
+
- Triple-hash chain — two paths:
|
|
160
|
+
- **Sentinel** (default): `sanitizationPipelineHash = keccak256("IDENTITY_SENTINEL")`, `inputHash = rawInputHash`
|
|
161
|
+
- **Non-sentinel** (`sanitizationCID` set): `sanitizationPipelineHash = keccak256(CID)`, `inputHash = keccak256(abi.encode(rawInputHash, sanitizationPipelineHash))`
|
|
162
|
+
- EIP-712 `WyriweAttestation` signed with the gateway key on every resolver call
|
|
163
|
+
- Attestation records persisted to `{namespace}:wyriwe` — synced by the mesh automatically
|
|
164
|
+
- Verifiable by any peer: recover signer from signature, match against known gateway address
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Quick start (setup wizard)
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
npm install
|
|
172
|
+
npm run dev
|
|
173
|
+
# → open http://localhost:3000
|
|
174
|
+
# → setup wizard walks you through key generation + config
|
|
175
|
+
# → config.json is written, node restarts, /admin loads
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Or configure via environment (no wizard needed):
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
cp .env.example .env
|
|
182
|
+
# set GATEWAY_PRIVATE_KEY, ADMIN_SECRET, PEERS, etc.
|
|
183
|
+
npm run dev
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Environment variables
|
|
189
|
+
|
|
190
|
+
| Variable | Required | Default | Description |
|
|
191
|
+
|---|---|---|---|
|
|
192
|
+
| `GATEWAY_PRIVATE_KEY` | Yes* | — | 32-byte hex signing key (`0x...`). Without it the node runs in dry-run mode (unsigned records). |
|
|
193
|
+
| `ADMIN_SECRET` | No | — | Protects `/admin`. Set for any non-local deployment. Without it the dashboard is open. |
|
|
194
|
+
| `PORT` | No | `3000` | HTTP port |
|
|
195
|
+
| `DB_PATH` | No | `./data.db` | SQLite file path |
|
|
196
|
+
| `SYNC_NAMESPACE` | No | `agent-attestations` | Record namespace — peers must match |
|
|
197
|
+
| `SYNC_INTERVAL` | No | `*/5 * * * *` | Cron expression for peer sync |
|
|
198
|
+
| `PEERS` | No | — | Comma-separated peer URLs |
|
|
199
|
+
| `AGENT_ID` | No | — | ERC-8004 agent identity (bytes32 hex). Enables `/identity` endpoint. |
|
|
200
|
+
| `REGISTRY_ADDRESS` | No | — | ERC-8004 on-chain registry address. Required alongside `AGENT_ID`. |
|
|
201
|
+
| `CHAIN_ID` | No | `1` | Chain where the ERC-8004 registry is deployed. |
|
|
202
|
+
| `ATTESTATION_INDEX` | No | — | Deployed `AttestationIndex` contract address. Enables on-chain anchoring. |
|
|
203
|
+
| `NODE_REGISTRY` | No | — | Deployed `NodeRegistry` contract address. Enables on-chain node registration. |
|
|
204
|
+
| `RPC_URL` | No | — | JSON-RPC endpoint. Required alongside `ATTESTATION_INDEX`. |
|
|
205
|
+
| `MODEL_HASH` | No | — | `keccak256` of model weights CID. Required to activate WYRIWE attestation. |
|
|
206
|
+
|
|
207
|
+
\* Can also come from `config.json` written by the setup wizard.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Admin dashboard
|
|
212
|
+
|
|
213
|
+
Visit `/admin` after setup. Features:
|
|
214
|
+
- Live stats: record count, peer count, last sync time
|
|
215
|
+
- Peer panel: add/remove peers, per-peer health + signer address + last sync
|
|
216
|
+
- Recent records panel: local vs peer-synced, timestamps
|
|
217
|
+
- Manual sync trigger
|
|
218
|
+
- Auto-refresh every 15 seconds
|
|
219
|
+
|
|
220
|
+
**Auth:** If `ADMIN_SECRET` is set, `/admin` requires a login (cookie session, 7-day). API routes also accept `Authorization: Bearer <secret>` for programmatic access.
|
|
221
|
+
|
|
222
|
+
**Stack status row:** A compact pill row below the header shows which tiers are active — Signing / ERC-8004 / WYRIWE / OCP — derived from `/admin/api/status`. Green = active, grey = unconfigured.
|
|
223
|
+
|
|
224
|
+
**Node logs panel:** Live ring buffer of the last 200 log lines (info/warn/error), colour-coded. Auto-refreshes every 10 seconds.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Two-node local test
|
|
229
|
+
|
|
230
|
+
Uses Hardhat dev keys — safe for local testing only, **never use in production**.
|
|
231
|
+
|
|
232
|
+
### Option A — two terminals
|
|
233
|
+
|
|
234
|
+
**Terminal 1 — node A**
|
|
235
|
+
```bash
|
|
236
|
+
PORT=3001 DB_PATH=./node-a.db \
|
|
237
|
+
GATEWAY_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
|
|
238
|
+
PEERS=http://localhost:3002 SYNC_INTERVAL="*/1 * * * *" \
|
|
239
|
+
npm run dev
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Terminal 2 — node B**
|
|
243
|
+
```bash
|
|
244
|
+
PORT=3002 DB_PATH=./node-b.db \
|
|
245
|
+
GATEWAY_PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d \
|
|
246
|
+
PEERS=http://localhost:3001 SYNC_INTERVAL="*/1 * * * *" \
|
|
247
|
+
npm run dev
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Option B — Docker
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
docker compose up --build
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Verify mesh sync
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
# trigger a CCIP call on node A — writes a signed record
|
|
260
|
+
curl http://localhost:3001/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266/0xdeadbeef
|
|
261
|
+
|
|
262
|
+
# check node A recorded it
|
|
263
|
+
curl http://localhost:3001/health | jq .records # → 1
|
|
264
|
+
|
|
265
|
+
# wait ~1 minute for sync cron, then check node B
|
|
266
|
+
curl http://localhost:3002/health | jq .records # → 1 (synced from A)
|
|
267
|
+
|
|
268
|
+
# verify by inputHash on node B
|
|
269
|
+
curl http://localhost:3002/verify/<inputHash>
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## API reference
|
|
275
|
+
|
|
276
|
+
### CCIP-Read handler
|
|
277
|
+
```
|
|
278
|
+
GET /{sender}/{data}.json
|
|
279
|
+
→ { data: "0x..." } EIP-3668 response
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Mesh sync (any peer can pull this)
|
|
283
|
+
```
|
|
284
|
+
GET /records?namespace=<str>&since=<unix>&limit=<n>&cursor=<str>
|
|
285
|
+
→ {
|
|
286
|
+
protocol: 1,
|
|
287
|
+
node_version: "0.1.0",
|
|
288
|
+
namespace: "agent-attestations",
|
|
289
|
+
records: [{ inputHash, namespace, key, value, timestamp, signature, sourcePeer }],
|
|
290
|
+
cursor: "<next>" | null
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### OCP observation commitment (ERC-8263)
|
|
295
|
+
```
|
|
296
|
+
GET /ocp/:inputHash
|
|
297
|
+
→ {
|
|
298
|
+
inputHash,
|
|
299
|
+
found: true,
|
|
300
|
+
commitmentHash: "0x...",
|
|
301
|
+
observation: { agentId, modelHash, inputHash, outputHash, timestamp },
|
|
302
|
+
namespace, sourcePeer
|
|
303
|
+
}
|
|
304
|
+
→ { inputHash, found: false } (404 — no WYRIWE attestation for this inputHash)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Attestation lookup
|
|
308
|
+
```
|
|
309
|
+
GET /verify/:inputHash
|
|
310
|
+
→ {
|
|
311
|
+
inputHash,
|
|
312
|
+
found: true,
|
|
313
|
+
proofs: [
|
|
314
|
+
{
|
|
315
|
+
namespace: "agent-attestations",
|
|
316
|
+
signingType: "EIP-191",
|
|
317
|
+
verified: true,
|
|
318
|
+
signer: "0x...",
|
|
319
|
+
signature: "0x...",
|
|
320
|
+
timestamp: 1234567890,
|
|
321
|
+
sourcePeer: null | "https://..."
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
namespace: "agent-attestations:wyriwe",
|
|
325
|
+
signingType: "EIP-712 WyriweAttestation",
|
|
326
|
+
verified: true,
|
|
327
|
+
signer: "0x...",
|
|
328
|
+
signature: "0x...",
|
|
329
|
+
timestamp: 1234567890,
|
|
330
|
+
attestation: { agentId, registry, modelHash, rawInputHash,
|
|
331
|
+
sanitizationPipelineHash, inputHash, outputHash }
|
|
332
|
+
}
|
|
333
|
+
]
|
|
334
|
+
}
|
|
335
|
+
→ { inputHash, found: false } (404)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Identity (ERC-8004)
|
|
339
|
+
```
|
|
340
|
+
GET /identity
|
|
341
|
+
→ { declared: true, agentId, registryAddress, chainId, namespace, signerAddress }
|
|
342
|
+
→ { declared: false } (404 — AGENT_ID not configured)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Node identity (VNI)
|
|
346
|
+
```
|
|
347
|
+
GET /vni
|
|
348
|
+
→ { nodeId, signerAddress, url, version, timestamp, signature }
|
|
349
|
+
→ { declared: false } (404 — NODE_URL not configured)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Peer gossip
|
|
353
|
+
```
|
|
354
|
+
GET /peers
|
|
355
|
+
→ { protocol: 1, node_version, signerAddress, peers: [{ url, signerAddress, healthy, lastSyncAt }] }
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Contributions (ERC-8275)
|
|
359
|
+
```
|
|
360
|
+
GET /contributions
|
|
361
|
+
→ { namespace, contributions: [{ source, records }] }
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Health
|
|
365
|
+
```
|
|
366
|
+
GET /health
|
|
367
|
+
→ {
|
|
368
|
+
ok, version, namespace, signerAddress,
|
|
369
|
+
identity: { agentId, registryAddress, chainId } | null,
|
|
370
|
+
tiers: { signed, erc8004, wyriwe, ocp },
|
|
371
|
+
peers, records
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Admin API (requires auth if ADMIN_SECRET set)
|
|
376
|
+
```
|
|
377
|
+
GET /admin/api/status node info, peers, recent records, tiers
|
|
378
|
+
GET /admin/api/logs last 200 log lines [{ ts, level, msg }]
|
|
379
|
+
GET /admin/api/audit per-spec compliance report (EIP-3668/WYRIWE/ERC-8004/OCP)
|
|
380
|
+
POST /admin/api/sync trigger immediate peer sync
|
|
381
|
+
POST /admin/api/publish batch-publish recent WYRIWE records to AttestationIndex
|
|
382
|
+
body: { limit?: number } (default 50, max 200)
|
|
383
|
+
→ { published, skipped, errors }
|
|
384
|
+
POST /admin/api/peers { url } — add peer
|
|
385
|
+
DEL /admin/api/peers { url } — remove peer
|
|
386
|
+
POST /admin/login { secret } — set session cookie
|
|
387
|
+
POST /admin/logout clear session cookie
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Mesh sync protocol
|
|
393
|
+
|
|
394
|
+
Any CCIP-Read gateway implementing `/records` is mesh-compatible:
|
|
395
|
+
|
|
396
|
+
```
|
|
397
|
+
GET /records?since=<unix>&namespace=<string>&limit=<n>&cursor=<string>
|
|
398
|
+
→ { protocol: 1, node_version, namespace, records: [...], cursor: string | null }
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Protocol version `1` is the current stable spec. Nodes on a different version are skipped during sync with a warning.
|
|
402
|
+
|
|
403
|
+
**Namespaces** are application-defined and scoped at the record level:
|
|
404
|
+
- `agent-attestations` — ENS Boiler / ERC-8004 agents
|
|
405
|
+
- `agent-attestations:wyriwe` — WYRIWE EIP-712 attestations (auto-produced by `withWyriwe()`)
|
|
406
|
+
- `token-metadata` — NFT gateways
|
|
407
|
+
- anything — define your own
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## ERC / spec alignment
|
|
412
|
+
|
|
413
|
+
| Spec | Layer | Role | Status |
|
|
414
|
+
|---|---|---|---|
|
|
415
|
+
| EIP-3668 | Transport | CCIP-Read client-to-gateway | ✅ implemented |
|
|
416
|
+
| WYRIWE | L2 Input trust | Triple-hash commitment, EIP-712 attestation | ✅ implemented |
|
|
417
|
+
| ERC-8004 | L1 Identity | Agent identity `agentId` + `registryAddress` in attestation | ✅ implemented |
|
|
418
|
+
| OCP / ERC-8263 | L3 Observation | Observation commitment hash | ✅ implemented |
|
|
419
|
+
| EIP-712 | L4 Attestation | Structured signing (via `withWyriwe`) | ✅ implemented |
|
|
420
|
+
| VNI | L5 Node Identity | Signed node identity, peer gossip | ✅ implemented |
|
|
421
|
+
| ERC-8275 | L6 Economics | Contribution attribution (MVP) | ✅ implemented |
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## Roadmap
|
|
426
|
+
|
|
427
|
+
### Done
|
|
428
|
+
- [x] CCIP-Read gateway (EIP-3668)
|
|
429
|
+
- [x] SQLite record store — WAL mode, composite PK `(inputHash, namespace)`, cursor pagination
|
|
430
|
+
- [x] DB versioned migrations (`schema_version` table, v1 applied on first boot)
|
|
431
|
+
- [x] EIP-191 signed records (basic tier)
|
|
432
|
+
- [x] Mesh peer sync with protocol version check
|
|
433
|
+
- [x] Setup wizard (`/setup`) — key generation, config.json persistence
|
|
434
|
+
- [x] Admin dashboard (`/admin`) — peers, records, sync
|
|
435
|
+
- [x] Admin auth — cookie session + Bearer token (`ADMIN_SECRET`)
|
|
436
|
+
- [x] `withWyriwe()` — EIP-712 attestation, triple-hash chain, IDENTITY_SENTINEL path
|
|
437
|
+
- [x] `/verify` — clean proof per namespace: `{ verified, signer, signingType, signature, attestation }`
|
|
438
|
+
- [x] ERC-8004 identity — `AGENT_ID` + `REGISTRY_ADDRESS` + `CHAIN_ID`, `/identity` endpoint, `/health` field
|
|
439
|
+
- [x] OCP / ERC-8263 — `commitmentHash` in `WyriweAttestation`, `/ocp/:inputHash` endpoint
|
|
440
|
+
- [x] Router SVG favicon, dinamic.eth design language
|
|
441
|
+
- [x] Peer signer pinning — reject records with unexpected signer after first sync
|
|
442
|
+
- [x] Peer health polling — fetch `/health` after every sync, populate `nodeVersion` + `signerAddress`
|
|
443
|
+
- [x] Graceful shutdown — `SIGTERM`/`SIGINT` → `server.close()` → `db.close()` → `process.exit(0)`
|
|
444
|
+
- [x] In-memory log ring buffer (200 lines, console-patched) → `/admin/api/logs` + colour-coded log panel
|
|
445
|
+
- [x] Stack status pills in admin header bar — Signing / ERC-8004 / WYRIWE / OCP
|
|
446
|
+
- [x] Library re-export (`src/lib.ts`) — `CcipRouter`, `withWyriwe`, `IdentityOpts`, `WyriweOpts`, `ResolverFn`, DB types
|
|
447
|
+
|
|
448
|
+
- [x] `withWyriwe()` non-sentinel path — `sanitizationCID` option; `inputHash = keccak256(abi.encode(rawInputHash, sanitizationPipelineHash))`
|
|
449
|
+
- [x] Setup wizard reconfigure flow — pre-fills current config, "Keep existing key", `/setup/current-config` endpoint, inherited admin secret
|
|
450
|
+
|
|
451
|
+
- [x] Spec audit accordion panel in admin — per-spec cards (EIP-3668 / WYRIWE / ERC-8004 / OCP), inline summary pills, expandable detail grid with missing-config hints
|
|
452
|
+
- [x] `contracts/AttestationIndex.sol` — on-chain anchor for WyriweAttestations; verifies EIP-712 sig against ERC-8004 registry domain, stores `signerOf[commitmentHash]` + `commitmentOf[inputHash]`
|
|
453
|
+
- [x] `src/chain/` — viem public + wallet clients, `publishAttestation()`, `checkOnChain()`
|
|
454
|
+
- [x] `/verify` on-chain fallback — if `inputHash` not in local DB and `ATTESTATION_INDEX` + `RPC_URL` configured, queries contract and returns on-chain proof
|
|
455
|
+
- [x] `POST /admin/api/publish` — batch-publish recent WYRIWE records to `AttestationIndex`; skips already-anchored; "Publish to chain" button in spec audit panel
|
|
456
|
+
- [x] Open node network — `GET /peers` gossip endpoint; auto-discovery pulls peer lists during sync (bounded at 10/cycle, disable with `AUTO_DISCOVER=false`)
|
|
457
|
+
- [x] VNI (Verifiable Node Identity) — `GET /vni` returns EIP-191 signed `{ nodeId, signerAddress, url, version, timestamp }`; peers verify during sync for authoritative signer resolution
|
|
458
|
+
- [x] `contracts/NodeRegistry.sol` — on-chain node directory; `register(url, sig)` proves key ownership; `POST /admin/api/register` + "Register on-chain" button in VNI spec card
|
|
459
|
+
- [x] ERC-8275 economics (MVP) — contribution attribution via `getContributions(namespace)`; `GET /contributions`; per-peer record counts surfaced in spec audit panel
|
|
460
|
+
- [x] Config: `NODE_URL`, `NODE_REGISTRY`, `AUTO_DISCOVER`; `/health` exposes `tiers.vni` + `tiers.onChain`
|
|
461
|
+
|
|
462
|
+
### Next — UI & node management
|
|
463
|
+
|
|
464
|
+
**Stack status bar**
|
|
465
|
+
- [x] Add VNI + On-chain tier pills
|
|
466
|
+
- [x] Click signer address pill to copy to clipboard (green flash feedback)
|
|
467
|
+
|
|
468
|
+
**Node info & layout**
|
|
469
|
+
- [x] Move node info bar above the peers/records panels; add namespace field
|
|
470
|
+
- [x] Toast-based error feedback in add-peer form (replaces `alert()`)
|
|
471
|
+
|
|
472
|
+
**Node config panel** *(in-dashboard, no env editing required)*
|
|
473
|
+
- [x] Full config panel — Core / Signing / Network / Identity / Chain / Admin sections
|
|
474
|
+
- [x] `GET /admin/api/config` — safe config snapshot (signer address, never the key)
|
|
475
|
+
- [x] `POST /admin/api/config` — writes `config.json`, preserves gateway key, restarts node
|
|
476
|
+
- [x] Auto-discover toggle, seed peers textarea, unsaved-changes indicator
|
|
477
|
+
|
|
478
|
+
**Wallet & signing**
|
|
479
|
+
- [x] Signing key panel — generate or import, rotate with identity-change warning, `POST /admin/api/key`
|
|
480
|
+
- [x] Dry-run banner — shown when no key configured, "Configure key →" scrolls to key panel
|
|
481
|
+
|
|
482
|
+
**Setup wizard — node owner onboarding**
|
|
483
|
+
- [x] Admin secret as dedicated step 2 — prominent warning box, two-step skip confirmation
|
|
484
|
+
- [x] Post-setup checklist — signing ✓, admin ✓/⚠, WYRIWE/ERC-8004/VNI ○ with next-step hints
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## Testing
|
|
489
|
+
|
|
490
|
+
The test suite uses the Node.js built-in test runner (`node:test`) with `tsx` for ESM TypeScript — no extra test framework required.
|
|
491
|
+
|
|
492
|
+
```bash
|
|
493
|
+
npm test
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Expected output:
|
|
497
|
+
|
|
498
|
+
```
|
|
499
|
+
ℹ tests 49
|
|
500
|
+
ℹ suites 16
|
|
501
|
+
ℹ pass 49
|
|
502
|
+
ℹ fail 0
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### What is tested
|
|
506
|
+
|
|
507
|
+
| File | Coverage |
|
|
508
|
+
|---|---|
|
|
509
|
+
| `src/__tests__/gateway.test.ts` | `decodeRequest` — address + calldata parsing, `.json` suffix stripping, `CcipRequestError` on bad inputs; `encodeResponse` envelope |
|
|
510
|
+
| `src/__tests__/crypto.test.ts` | `signRecord` / `recoverRecordSigner` round-trip; `verifyRecord` correct signer → `true`, wrong signer / tampered value → `false` |
|
|
511
|
+
| `src/__tests__/db.test.ts` | `insertRecord`, `getRecord` (with/without namespace), `getRecordsByInputHash`, `INSERT OR IGNORE` deduplication, cursor pagination, `getContributions` grouping, peer upsert + remove |
|
|
512
|
+
| `src/__tests__/ocp.test.ts` | `buildCommitmentHash` determinism, 32-byte hex output, field-sensitivity (agentId / outputHash / timestamp) |
|
|
513
|
+
| `src/__tests__/wyriwe.test.ts` | Sentinel path (`inputHash === rawInputHash`), non-sentinel path (`keccak256(abi.encode(rawInputHash, sanitizationPipelineHash))`), paths produce distinct hashes for same calldata |
|
|
514
|
+
| `src/__tests__/vni.test.ts` | `makeVni` field shape + stable `nodeId`; `verifyVni` round-trip; tamper detection (url / signerAddress / nodeId → `null`) |
|
|
515
|
+
|
|
516
|
+
Tests use `SQLiteDB(':memory:')` directly (bypassing the runtime singleton) and Hardhat dev key 0 for any signing operations — both are safe to commit and require no external services.
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## Related
|
|
521
|
+
|
|
522
|
+
- [ens-boiler](https://github.com/Echo-Merlini/ens-boiler) — opinionated ENS agent stack built on `ccip-router`
|
|
523
|
+
- [WYRIWE](https://github.com/TMerlini/wyriwe) — input provenance spec
|
|
524
|
+
- [OCP](https://github.com/damonzwicker/observation-commitment-protocol) — observation commitment protocol
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
*dinamic.eth*
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.20;
|
|
3
|
+
|
|
4
|
+
/// @title AttestationIndex
|
|
5
|
+
/// @notice On-chain anchor for WYRIWE EIP-712 attestations produced by ccip-router nodes.
|
|
6
|
+
/// Any gateway may call `record()` to anchor a signed WyriweAttestation.
|
|
7
|
+
/// Signature is verified against the ERC-8004 registry domain used at signing time.
|
|
8
|
+
///
|
|
9
|
+
/// @dev Deploy on the same chain as configured in CHAIN_ID / opts.chainId so that
|
|
10
|
+
/// block.chainid matches the chainId used in the EIP-712 domain during signing.
|
|
11
|
+
contract AttestationIndex {
|
|
12
|
+
|
|
13
|
+
// ── Types ─────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
struct WyriweAttestation {
|
|
16
|
+
bytes32 agentId;
|
|
17
|
+
address registry; // ERC-8004 registry — used as EIP-712 verifyingContract
|
|
18
|
+
bytes32 modelHash;
|
|
19
|
+
bytes32 rawInputHash;
|
|
20
|
+
bytes32 sanitizationPipelineHash;
|
|
21
|
+
bytes32 inputHash;
|
|
22
|
+
bytes32 outputHash;
|
|
23
|
+
bytes32 commitmentHash;
|
|
24
|
+
uint256 timestamp;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Constants ─────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
bytes32 private constant DOMAIN_TYPEHASH = keccak256(
|
|
30
|
+
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
bytes32 private constant ATTESTATION_TYPEHASH = keccak256(
|
|
34
|
+
"WyriweAttestation(bytes32 agentId,address registry,bytes32 modelHash,"
|
|
35
|
+
"bytes32 rawInputHash,bytes32 sanitizationPipelineHash,bytes32 inputHash,"
|
|
36
|
+
"bytes32 outputHash,bytes32 commitmentHash,uint256 timestamp)"
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// ── Storage ───────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/// @notice Returns the signer address for a commitmentHash (zero = not recorded).
|
|
42
|
+
mapping(bytes32 => address) public signerOf;
|
|
43
|
+
|
|
44
|
+
/// @notice Returns the latest commitmentHash anchored for a given inputHash.
|
|
45
|
+
mapping(bytes32 => bytes32) public commitmentOf;
|
|
46
|
+
|
|
47
|
+
// ── Events ────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
event AttestationRecorded(
|
|
50
|
+
bytes32 indexed commitmentHash,
|
|
51
|
+
bytes32 indexed inputHash,
|
|
52
|
+
bytes32 indexed agentId,
|
|
53
|
+
address signer,
|
|
54
|
+
uint256 timestamp
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// ── Write ─────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/// @notice Anchor a WyriweAttestation on-chain.
|
|
60
|
+
/// Verifies the EIP-712 signature against the domain used at signing time
|
|
61
|
+
/// (name="WyriweAttestation", version="1", chainId=block.chainid,
|
|
62
|
+
/// verifyingContract=a.registry).
|
|
63
|
+
/// @param a The attestation struct matching the off-chain signed message.
|
|
64
|
+
/// @param signature 65-byte EIP-712 signature produced by the gateway key.
|
|
65
|
+
/// @return signer Recovered signer address.
|
|
66
|
+
function record(
|
|
67
|
+
WyriweAttestation calldata a,
|
|
68
|
+
bytes calldata signature
|
|
69
|
+
) external returns (address signer) {
|
|
70
|
+
require(signerOf[a.commitmentHash] == address(0), "AttestationIndex: already recorded");
|
|
71
|
+
|
|
72
|
+
// Reconstruct the EIP-712 domain separator as signed by the gateway.
|
|
73
|
+
// verifyingContract is a.registry (ERC-8004 registry) — NOT this contract.
|
|
74
|
+
bytes32 domainSeparator = keccak256(abi.encode(
|
|
75
|
+
DOMAIN_TYPEHASH,
|
|
76
|
+
keccak256("WyriweAttestation"),
|
|
77
|
+
keccak256("1"),
|
|
78
|
+
block.chainid,
|
|
79
|
+
a.registry
|
|
80
|
+
));
|
|
81
|
+
|
|
82
|
+
bytes32 structHash = keccak256(abi.encode(
|
|
83
|
+
ATTESTATION_TYPEHASH,
|
|
84
|
+
a.agentId, a.registry, a.modelHash,
|
|
85
|
+
a.rawInputHash, a.sanitizationPipelineHash,
|
|
86
|
+
a.inputHash, a.outputHash,
|
|
87
|
+
a.commitmentHash, a.timestamp
|
|
88
|
+
));
|
|
89
|
+
|
|
90
|
+
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
|
|
91
|
+
signer = _recover(digest, signature);
|
|
92
|
+
require(signer != address(0), "AttestationIndex: invalid signature");
|
|
93
|
+
|
|
94
|
+
signerOf[a.commitmentHash] = signer;
|
|
95
|
+
commitmentOf[a.inputHash] = a.commitmentHash;
|
|
96
|
+
|
|
97
|
+
emit AttestationRecorded(a.commitmentHash, a.inputHash, a.agentId, signer, a.timestamp);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Read ──────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/// @notice Returns true if a commitmentHash has been anchored.
|
|
103
|
+
function isRecorded(bytes32 commitmentHash) external view returns (bool) {
|
|
104
|
+
return signerOf[commitmentHash] != address(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Internal ──────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
function _recover(bytes32 digest, bytes calldata sig) internal pure returns (address) {
|
|
110
|
+
require(sig.length == 65, "AttestationIndex: bad sig length");
|
|
111
|
+
bytes32 r;
|
|
112
|
+
bytes32 s;
|
|
113
|
+
uint8 v;
|
|
114
|
+
assembly {
|
|
115
|
+
r := calldataload(sig.offset)
|
|
116
|
+
s := calldataload(add(sig.offset, 32))
|
|
117
|
+
v := byte(0, calldataload(add(sig.offset, 64)))
|
|
118
|
+
}
|
|
119
|
+
if (v < 27) v += 27;
|
|
120
|
+
require(v == 27 || v == 28, "AttestationIndex: bad v");
|
|
121
|
+
address recovered = ecrecover(digest, v, r, s);
|
|
122
|
+
require(recovered != address(0), "AttestationIndex: ecrecover failed");
|
|
123
|
+
return recovered;
|
|
124
|
+
}
|
|
125
|
+
}
|