Skip to content

Commit c0ef33f

Browse files
authored
feat(bridge): ERC-8004 Day 3 — normalizer + score_ingest + trust score wiring
67/67 unit tests PASS. Backward-compat verified (no-attestation path identical to existing _source_reputation_score). Lazy bridge import keeps main backend lean. 3 mainnet-shaped snapshot fixtures + production DEPLOY.md included. Day 1 is_admissible tz bug fixed inline.
1 parent 6501e46 commit c0ef33f

23 files changed

Lines changed: 1813 additions & 8 deletions
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# ERC-8004 Bridge — Production Deployment
2+
3+
End-to-end deploy guide for the `agentgraph_bridge_erc8004` package in prod.
4+
5+
## Prerequisites
6+
7+
1. **`ETH_RPC_URL`** — Alchemy or Quicknode HTTPS endpoint to Ethereum mainnet.
8+
Free tier sufficient (300M Alchemy CU/month vs. our ~100 calls/day).
9+
Already deployed in prod (`.env.production`) as of 2026-05-22.
10+
11+
2. **EIP-8004 contract addresses** — currently placeholder zeros pending the
12+
canonical EIP-8004 mainnet deployment. Once verified, set:
13+
```
14+
ERC8004_IDENTITY_ADDRESS=0x...
15+
ERC8004_REPUTATION_ADDRESS=0x...
16+
ERC8004_VALIDATION_ADDRESS=0x...
17+
```
18+
Until these land, the bridge is functionally inert in prod (any
19+
read attempt against `0x000...000` returns `RegistryReadError`).
20+
Composite trust score behavior unchanged (the lazy import in
21+
`src/trust/score.py::_external_score_with_attestations` only fires
22+
when caller passes attestations, which the sync job won't do
23+
until real addresses exist).
24+
25+
3. **`pip install agentgraph[erc8004]`** — pulls `web3 + eth-account`.
26+
Not a hard dep on the main backend; only required when the bridge
27+
is actually loaded.
28+
29+
## One-liner deploy (after EIP-8004 addresses land)
30+
31+
```bash
32+
ssh -i ~/.ssh/agentgraph-key.pem ec2-user@98.94.217.37
33+
cd ~/agentgraph
34+
cat >> .env.production <<EOF
35+
36+
# ERC-8004 (filled in 2026-MM-DD when EIP-8004 mainnet deploy verified)
37+
ERC8004_IDENTITY_ADDRESS=<real-address>
38+
ERC8004_REPUTATION_ADDRESS=<real-address>
39+
ERC8004_VALIDATION_ADDRESS=<real-address>
40+
EOF
41+
42+
# Sync source dep + recreate backend
43+
git pull
44+
PG_PW=$(grep '^POSTGRES_PASSWORD=' .env.secrets | cut -d= -f2-)
45+
RD_PW=$(grep '^REDIS_PASSWORD=' .env.secrets | cut -d= -f2-)
46+
sudo POSTGRES_PASSWORD="$PG_PW" REDIS_PASSWORD="$RD_PW" \
47+
docker-compose -f docker-compose.prod.yml up -d --no-deps --force-recreate backend
48+
49+
# Smoke test
50+
sudo docker exec agentgraph-backend-1 python3 -c "
51+
from agentgraph_bridge_erc8004 import make_reader_from_env
52+
r = make_reader_from_env()
53+
print('reachable:', r.is_reachable())
54+
print('identity_count:', r.entry_count(__import__('agentgraph_bridge_erc8004').ERC8004Registry.IDENTITY))
55+
"
56+
```
57+
58+
## Background sync job (post EIP-8004 deploy)
59+
60+
The trust score recompute job runs every cycle. Fetching ERC-8004 attestations
61+
live on every recompute would burn RPC quota + slow the job. Instead:
62+
63+
1. Cron a separate job (~ every hour) that scans the 3 registries for new
64+
entries since last sync watermark
65+
2. Normalize each entry via `attestation_normalizer.normalize()`
66+
3. Cache verified attestations in a new `erc8004_attestations` table keyed
67+
on `subject_did` + `source_urn`
68+
4. Trust recompute reads from the cache (synchronous, fast) and passes
69+
into `_external_score_with_attestations()`
70+
71+
Schema (proposal — not yet migrated):
72+
```sql
73+
CREATE TABLE erc8004_attestations (
74+
source_urn TEXT PRIMARY KEY, -- urn:erc8004:{registry}:{entry_id}
75+
subject_did TEXT NOT NULL,
76+
provider_did TEXT NOT NULL,
77+
claim_type TEXT NOT NULL, -- {identity, transport, authority, continuity}
78+
claim_subtype TEXT,
79+
payload JSONB NOT NULL,
80+
signature_verified BOOLEAN NOT NULL,
81+
registry_signature_verified BOOLEAN NOT NULL,
82+
issued_at TIMESTAMPTZ NOT NULL,
83+
expires_at TIMESTAMPTZ,
84+
last_synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
85+
);
86+
CREATE INDEX idx_erc8004_subject ON erc8004_attestations(subject_did);
87+
```
88+
89+
The sync job is task #88 follow-on; not blocking v0.3.2 publish.
90+
91+
## Failure modes + observability
92+
93+
- **RPC unreachable**`RegistryReadError` from reader, log warning, skip
94+
this sync cycle. No trust score impact (cache stays).
95+
- **JWKS fetch fails**`NormalizationError` from normalizer, log warning,
96+
attestation dropped from this sync. Not retried until next sync window.
97+
- **Signature mismatch**`NormalizationError`. SIGNAL-WORTHY: attestation
98+
was rejected, log at ERROR + emit metric `erc8004.signature_mismatch_total`.
99+
- **Address not yet deployed**`RegistryReadError` (contract returns
100+
empty bytes for non-existent contracts). Expected until EIP-8004
101+
mainnet addresses land; suppress to DEBUG-level log.
102+
103+
## Health check endpoint
104+
105+
The bridge surfaces `is_reachable()` for liveness probes:
106+
107+
```python
108+
from agentgraph_bridge_erc8004 import make_reader_from_env
109+
reader = make_reader_from_env()
110+
healthy = reader.is_reachable() # True if RPC responds AND chain_id matches
111+
```
112+
113+
Wire into the FastAPI healthcheck under `/health/erc8004` if needed.
114+
Optional — the bridge has no critical-path dependency.
115+
116+
## Cost (Alchemy free tier)
117+
118+
| Operation | CU cost | Volume | Monthly CU |
119+
|---|---|---|---|
120+
| `eth_blockNumber` (per is_reachable) | 16 | 1/hr | ~12K |
121+
| `getEntry(uint256)` (per attestation sync) | 16 | ~50/day | ~24K |
122+
| `EntrySubmitted` event scan | 16/block × ~50 blocks | 1/hr | ~600K |
123+
| **Total estimated monthly CU** | | | **~640K** |
124+
125+
Vs. Alchemy free tier limit of 300M CU/month. **0.2% utilization.** No cost
126+
concern; comfortable headroom for 100x growth in attestation volume.
127+
128+
## Rollback
129+
130+
The bridge is opt-in. To disable entirely:
131+
1. Unset `ERC8004_IDENTITY_ADDRESS` / `_REPUTATION_ADDRESS` / `_VALIDATION_ADDRESS`
132+
2. Restart backend
133+
3. Trust recompute reverts to community-signal-only external scoring
134+
135+
No data migration required — the bridge writes to its own cache table
136+
which can be left in place or `DROP TABLE` cleanly.

src/agentgraph_bridge_erc8004/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
"""
2727
from __future__ import annotations
2828

29+
from agentgraph_bridge_erc8004.attestation_normalizer import (
30+
NormalizationError,
31+
normalize,
32+
)
2933
from agentgraph_bridge_erc8004.models import (
3034
ERC8004Entry,
3135
ERC8004Registry,
@@ -36,6 +40,11 @@
3640
RegistryReadError,
3741
make_reader_from_env,
3842
)
43+
from agentgraph_bridge_erc8004.score_ingest import (
44+
blend_with_community_signals,
45+
score,
46+
score_breakdown,
47+
)
3948
from agentgraph_bridge_erc8004.urn_resolver import (
4049
ParsedURN,
4150
URNParseError,
@@ -46,12 +55,17 @@
4655
"ERC8004Entry",
4756
"ERC8004Registry",
4857
"ERC8004RegistryReader",
58+
"NormalizationError",
4959
"NormalizedAttestation",
5060
"ParsedURN",
5161
"RegistryReadError",
5262
"URNParseError",
63+
"blend_with_community_signals",
5364
"make_reader_from_env",
65+
"normalize",
5466
"parse_erc8004_urn",
67+
"score",
68+
"score_breakdown",
5569
]
5670

57-
__version__ = "0.1.0" # Day 2 ships registry_reader; Day 3 adds normalizer + score_ingest
71+
__version__ = "0.2.0" # Day 3 ships normalizer + score_ingest

0 commit comments

Comments
 (0)