Skip to content

Commit 7b747eb

Browse files
authored
Merge pull request #1 from Shaurya2k06/temp
Feat: demo-api
2 parents 36aa651 + 51cc08c commit 7b747eb

9 files changed

Lines changed: 2406 additions & 3 deletions

File tree

README.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# DCT Protocol
2+
3+
**Delegatable Capability Tokens** — a full-stack protocol for cryptographically-scoped AI agent authorization on Base.
4+
5+
| Layer | Technology |
6+
|---|---|
7+
| Identity | ERC-8004 Agent NFT Registry |
8+
| Off-chain auth | Eclipse Biscuit WASM (Ed25519 + Datalog) |
9+
| On-chain enforcement | DCTRegistry + DCTEnforcer (UUPS, Base Sepolia) |
10+
| Action attestation | TLSNotary (Dockerized notary + server-side prover) |
11+
| Gas sponsorship | ERC-4337 via Pimlico bundler + paymaster |
12+
| Backend | Node.js / Express (`server/`) |
13+
| Frontend | React / Vite (`client/`) |
14+
15+
---
16+
17+
## Quick start
18+
19+
```bash
20+
# 1 — Install all deps
21+
npm install --prefix server
22+
npm install --prefix client
23+
24+
# 2 — Copy and fill env
25+
cp server/.env.example server/.env
26+
# Required: PRIVATE_KEY, ALCHEMY_API_KEY (or RPC_URL)
27+
# Optional: PIMLICO_API_KEY, DATABASE_URL, TLSN_*
28+
29+
# 3 — Start server (WASM module support required)
30+
cd server && npm start
31+
# → http://localhost:3000
32+
33+
# 4 — Start client
34+
cd client && npm run dev
35+
# → http://localhost:5173 (Live Demo at /live-demo)
36+
37+
# 5 — (Optional) TLSNotary Docker notary
38+
docker compose -f docker-compose.tlsn.yml up -d
39+
# → notary on :7047, wstcp proxy on :55688
40+
41+
# 6 — (Optional) Dev TLS prover
42+
cd server && npm run tlsn-prover
43+
# → prover on :8090
44+
```
45+
46+
---
47+
48+
## Contract addresses — Base Sepolia (chainId 84532)
49+
50+
| Contract | Address |
51+
|---|---|
52+
| ERC8004IdentityRegistry | `0x8004A818BFB912233c491871b3d84c89A494BD9e` |
53+
| DCTRegistry | `0x2cec268a5934bfa5aa7f973ac7accf8ac17b89cf` |
54+
| DCTEnforcer | `0x256a633fa2c990a64ec4adf79685f59490a241f8` |
55+
| DCTCaveatEnforcer | `0x8e4c74b1a26663ba734fbeb7a7cc68204cf1eb68` |
56+
| NotaryAttestationVerifier | `0x58874114f6c28c1c782161ed0385680f4d26c558` |
57+
58+
---
59+
60+
## Environment checklist
61+
62+
```
63+
# server/.env (never commit)
64+
PRIVATE_KEY=<deployer / signer EOA private key>
65+
ALCHEMY_API_KEY=<or set RPC_URL directly>
66+
PIMLICO_API_KEY=<for ERC-4337 sponsored gas; omit to use EOA fallback>
67+
DATABASE_URL=<Neon / Postgres connection string; omit to disable audit log>
68+
TLSN_NOTARY_URL=http://127.0.0.1:7047
69+
TLSN_PROVER_URL=http://127.0.0.1:8090
70+
```
71+
72+
Rotate any key that was ever logged, printed, or exposed in chat.
73+
`server/.env` is in `.gitignore` — confirm before every push with `git status`.
74+
75+
---
76+
77+
## On-chain event stream
78+
79+
`GET /api/events` — Server-Sent Events (SSE) endpoint.
80+
Emits `DelegationRegistered`, `DelegationRevoked`, `TrustUpdated`, `ActionValidated` from on-chain logs.
81+
82+
```js
83+
const es = new EventSource('http://localhost:3000/api/events');
84+
es.onmessage = (e) => console.log(JSON.parse(e.data));
85+
```
86+
87+
Uses a WebSocket provider when `WS_RPC_URL` or `ALCHEMY_API_KEY` is set;
88+
falls back to HTTP polling every 6 s otherwise.
89+
90+
The **Live Demo** (`/live-demo`) shows an embedded event log fed from this endpoint.
91+
92+
---
93+
94+
## ERC-4337 execution
95+
96+
`POST /api/execute/submit` tries the ERC-4337 (Pimlico) path first when `PIMLICO_API_KEY` is set:
97+
98+
1. Looks up the token's scope metadata from the in-process Biscuit store.
99+
2. Builds and sends a `validateActionWithScope` UserOperation via Pimlico bundler + paymaster.
100+
3. Falls back to direct EOA `execute()` if `PIMLICO_API_KEY` is absent, the key is missing, or the AA path errors.
101+
102+
Response includes `"path": "aa-4337"` or `"path": "eoa"` so the frontend can surface which was used.
103+
104+
---
105+
106+
## Contracts
107+
108+
```
109+
contracts/
110+
src/
111+
DCTRegistry.sol — delegation lineage + trust scoring (UUPS)
112+
DCTEnforcer.sol — validateActionWithScope (UUPS)
113+
NotaryAttestationVerifier.sol — TLSNotary ECDSA oracle
114+
mocks/TestAgentRegistry.sol
115+
test/
116+
DCTRegistry.t.sol — 18 unit tests incl. gas snapshots + security
117+
script/
118+
DeployDCT.s.sol
119+
UpgradeDCTEnforcer.s.sol
120+
```
121+
122+
Run tests:
123+
124+
```bash
125+
cd contracts && forge test -v
126+
```
127+
128+
Gas snapshots (from `test_Gas_*`) are printed inline.
129+
Security tests cover: scope mismatch, over-spend, expiry, wrong tool, scope commitment mismatch, double-registration, revoked-parent block, ancestor revoke, deprecated `validateAction`.
130+
131+
---
132+
133+
## SDK
134+
135+
```bash
136+
npm install @shaurya2k06/dctsdk
137+
```
138+
139+
```js
140+
import { mintRootToken, attenuateToken, authorizeToken, delegate, execute, revoke } from '@shaurya2k06/dctsdk';
141+
```
142+
143+
See [`docs/LOCAL_DEV.md`](docs/LOCAL_DEV.md) for a full local Anvil walkthrough.
144+
145+
---
146+
147+
## Live Demo
148+
149+
Navigate to **`/live-demo`** in the client app for a 12-phase interactive demo:
150+
151+
| Phase | What happens |
152+
|---|---|
153+
| 0 | Health checks (chain, registry, enforcer, ERC-8004, Pimlico, TLSNotary) |
154+
| 1 | Three agents registered on ERC-8004 |
155+
| 2 | Root Biscuit token minted (off-chain, timed) |
156+
| 3 | Orchestrator → Research delegation (on-chain) |
157+
| 4 | Research → Payment delegation (on-chain) |
158+
| 5 | Successful execution — 4-check trace (revocation, identity, scope, attestation) |
159+
| 6 | Off-chain Datalog rejection — zero gas |
160+
| 7 | On-chain revert for out-of-scope action |
161+
| 8 | Single-tx cascade revocation |
162+
| 9 | Lineage walk animation |
163+
| 10 | Trust score timeline |
164+
| 11 | Summary + stats |
165+
166+
The right-hand column shows a live SSE event log from Base Sepolia.
167+
168+
---
169+
170+
## Security notes
171+
172+
- `server/.env` is `.gitignore`-listed; double-check with `git status` before any push.
173+
- `PRIVATE_KEY` / `PIMLICO_API_KEY` should be rotated if ever logged or printed to terminal.
174+
- `DCTEnforcer.validateAction` is permanently deprecated (always reverts); use `validateActionWithScope`.
175+
- All `ownerOf` calls in DCTRegistry use `view` (EVM STATICCALL), making reentrancy through the identity registry impossible by construction. `nonReentrant` guards are defense-in-depth.
176+
- Contract upgrades go through UUPS `upgradeToAndCall`; only the `owner` can upgrade.

client/src/App.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Routes, Route } from "react-router-dom";
22
import Sidebar from "./components/layout/Sidebar";
33
import TlsnDemo from "./pages/TlsnDemo";
44
import Demo from "./pages/Demo";
5+
import LiveDemo from "./pages/LiveDemo";
56

67
/**
78
* Primary UX: browser TLSNotary (/) — real tlsn-js WASM.
@@ -15,6 +16,7 @@ function App() {
1516
<Routes>
1617
<Route path="/" element={<TlsnDemo />} />
1718
<Route path="/demo" element={<Demo />} />
19+
<Route path="/live-demo" element={<LiveDemo />} />
1820
</Routes>
1921
</main>
2022
</div>

client/src/components/layout/Sidebar.jsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { NavLink } from "react-router-dom";
22
import { motion } from "framer-motion";
3-
import { Shield, Play, ExternalLink } from "lucide-react";
3+
import { Shield, Play, Zap, ExternalLink } from "lucide-react";
44

55
const navItems = [
6-
{ to: "/", label: "TLSNotary", icon: Shield, end: true },
7-
{ to: "/demo", label: "On-chain demo", icon: Play, end: false },
6+
{ to: "/", label: "TLSNotary", icon: Shield, end: true },
7+
{ to: "/live-demo", label: "Live Demo", icon: Zap, end: false },
8+
{ to: "/demo", label: "Quick demo", icon: Play, end: false },
89
];
910

1011
export default function Sidebar() {
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/**
2+
* EventLog — live on-chain event feed via SSE (GET /api/events).
3+
*
4+
* Shows DelegationRegistered, DelegationRevoked, TrustUpdated, ActionValidated
5+
* as they arrive from the server's chain-events subscriber.
6+
*
7+
* Usage:
8+
* <EventLog maxRows={50} className="..." />
9+
*/
10+
11+
import { useEffect, useRef, useState } from "react";
12+
13+
const API_BASE = import.meta.env.VITE_API_URL || "http://localhost:3000";
14+
15+
const EVENT_STYLE = {
16+
DelegationRegistered: {
17+
bg: "bg-blue-950/60",
18+
badge: "bg-blue-700",
19+
icon: "🔗",
20+
label: "Delegated",
21+
},
22+
DelegationRevoked: {
23+
bg: "bg-red-950/60",
24+
badge: "bg-red-700",
25+
icon: "🚫",
26+
label: "Revoked",
27+
},
28+
TrustUpdated: {
29+
bg: "bg-yellow-950/60",
30+
badge: "bg-yellow-700",
31+
icon: "⚡",
32+
label: "Trust",
33+
},
34+
ActionValidated: {
35+
bg: "bg-green-950/60",
36+
badge: "bg-green-600",
37+
icon: "✓",
38+
label: "Validated",
39+
},
40+
};
41+
42+
function shortHash(h) {
43+
if (!h || typeof h !== "string") return "—";
44+
return `${h.slice(0, 6)}${h.slice(-4)}`;
45+
}
46+
47+
function formatEvent(ev) {
48+
const style = EVENT_STYLE[ev.type] || {
49+
bg: "bg-zinc-800",
50+
badge: "bg-zinc-600",
51+
icon: "·",
52+
label: ev.type,
53+
};
54+
55+
let detail = "";
56+
if (ev.type === "DelegationRegistered") {
57+
detail = `child ${shortHash(ev.childId)} → agent #${ev.agentId ?? "?"}`;
58+
} else if (ev.type === "DelegationRevoked") {
59+
detail = `token ${shortHash(ev.tokenId)} — agent #${ev.agentId ?? "?"}`;
60+
} else if (ev.type === "TrustUpdated") {
61+
const dir = ev.wasViolation ? "↓ violation" : "↑ success";
62+
const score = ev.newScore
63+
? ` score=${(Number(ev.newScore) / 1e18).toFixed(4)}`
64+
: "";
65+
detail = `agent #${ev.agentId ?? "?"} ${dir}${score}`;
66+
} else if (ev.type === "ActionValidated") {
67+
detail = `agent #${ev.agentId ?? "?"} ${ev.passed ? "PASSED ✓" : "FAILED ✗"}${shortHash(ev.revocationId)}`;
68+
}
69+
70+
return { style, detail };
71+
}
72+
73+
export default function EventLog({ maxRows = 60, className = "" }) {
74+
const [events, setEvents] = useState([]);
75+
const [connected, setConnected] = useState(false);
76+
const [error, setError] = useState(null);
77+
const bottomRef = useRef(null);
78+
const esRef = useRef(null);
79+
80+
useEffect(() => {
81+
const es = new EventSource(`${API_BASE}/api/events`);
82+
esRef.current = es;
83+
84+
es.addEventListener("open", () => {
85+
setConnected(true);
86+
setError(null);
87+
});
88+
89+
es.addEventListener("message", (e) => {
90+
try {
91+
const ev = JSON.parse(e.data);
92+
setEvents((prev) => {
93+
const next = [ev, ...prev];
94+
return next.length > maxRows ? next.slice(0, maxRows) : next;
95+
});
96+
} catch {
97+
/* ignore malformed SSE payloads */
98+
}
99+
});
100+
101+
es.addEventListener("error", () => {
102+
setConnected(false);
103+
setError("Reconnecting…");
104+
});
105+
106+
return () => {
107+
es.close();
108+
};
109+
}, [maxRows]);
110+
111+
// Auto-scroll is top-prepend so newest is always first — no scroll needed.
112+
113+
const empty = events.length === 0;
114+
115+
return (
116+
<div className={`flex flex-col ${className}`}>
117+
{/* Header */}
118+
<div className="flex items-center justify-between px-3 py-2 border-b border-zinc-700/50">
119+
<div className="flex items-center gap-2 text-sm font-mono font-semibold text-zinc-200">
120+
<span className="text-base">📡</span> Chain Events
121+
</div>
122+
<div className="flex items-center gap-1.5">
123+
<span
124+
className={`h-2 w-2 rounded-full ${
125+
connected ? "bg-green-400 animate-pulse" : "bg-red-500"
126+
}`}
127+
/>
128+
<span className="text-xs text-zinc-400">
129+
{connected ? "live" : error ?? "offline"}
130+
</span>
131+
</div>
132+
</div>
133+
134+
{/* Feed */}
135+
<div className="flex-1 overflow-y-auto min-h-0 max-h-96">
136+
{empty ? (
137+
<div className="flex items-center justify-center h-24 text-zinc-600 text-sm font-mono">
138+
{connected ? "Waiting for on-chain events…" : "Connecting to event stream…"}
139+
</div>
140+
) : (
141+
<ul className="divide-y divide-zinc-800/50">
142+
{events.map((ev, i) => {
143+
const { style, detail } = formatEvent(ev);
144+
const txHash = ev.txHash;
145+
const ts = ev.ts ? new Date(ev.ts).toLocaleTimeString() : "";
146+
147+
return (
148+
<li
149+
key={i}
150+
className={`px-3 py-2 flex items-start gap-2.5 text-xs font-mono ${style.bg} transition-all`}
151+
>
152+
<span className="mt-0.5 text-base leading-none">{style.icon}</span>
153+
<div className="flex-1 min-w-0">
154+
<div className="flex items-center gap-1.5 flex-wrap">
155+
<span
156+
className={`px-1.5 py-0.5 rounded text-[10px] font-bold text-white ${style.badge}`}
157+
>
158+
{style.label}
159+
</span>
160+
<span className="text-zinc-300 truncate">{detail}</span>
161+
</div>
162+
{txHash && (
163+
<div className="mt-0.5 flex items-center gap-1">
164+
<a
165+
href={`https://sepolia.basescan.org/tx/${txHash}`}
166+
target="_blank"
167+
rel="noreferrer"
168+
className="text-blue-400 hover:text-blue-300 underline"
169+
>
170+
{shortHash(txHash)}
171+
</a>
172+
{ev.blockNumber && (
173+
<span className="text-zinc-600">
174+
block {ev.blockNumber}
175+
</span>
176+
)}
177+
<span className="ml-auto text-zinc-600">{ts}</span>
178+
</div>
179+
)}
180+
</div>
181+
</li>
182+
);
183+
})}
184+
</ul>
185+
)}
186+
<div ref={bottomRef} />
187+
</div>
188+
</div>
189+
);
190+
}

0 commit comments

Comments
 (0)