Skip to content

Latest commit

 

History

History
65 lines (50 loc) · 16.9 KB

File metadata and controls

65 lines (50 loc) · 16.9 KB

Execution Kernel Protocol

Composable execution infrastructure for Web3 intents. Intents are resolved by an on-chain ExecutionEngine that scores competing IExecutionModule implementations (via ScorePolicy) and executes the best one, rather than routing through a single monolithic solver.

Project overview

The repo is a monorepo. Every package/app under packages/ and apps/ named in the README's repository-structure diagram is now implemented: packages/contracts, packages/types, packages/sdk, packages/config, apps/execution-node, apps/indexer, apps/api, apps/frontend, and apps/landing (see below). The top-level scripts/ directory holds one local-dev convenience script (deploy-local.sh, see below); docs/ is still not populated — see the scan-scope rules below for docs/.

  • packages/contracts (Foundry/Solidity) — the on-chain execution core.

    • src/core/ExecutionEngine.sol (entrypoint: executeIntent, module selection loop) and IntentRegistry.sol (owner-gated intent-type registration).
    • src/modules/ExecutionModuleBase.sol (abstract base implementing IExecutionModule), RouterModule.sol and MevProtectionModule.sol (two competing modules registered for the same ROUTE intent type, proving out real module-selection behavior beyond test mocks).
    • src/policy/ScorePolicy.sol, the pluggable weighted-scoring contract ExecutionEngine calls to rank module simulation results. Scores are signed (int256) — a module whose penalties outweigh its quality legitimately scores negative rather than reverting. Weights are governable post-deploy via the owner-gated updateWeights(), effective immediately.
    • src/registry/ModuleRegistry.sol, owner-gated intent-type → active-module-address mapping.
    • src/types/ExecutionQuote.sol, the canonical struct modules return from simulate() and ScorePolicy scores.
    • src/interfaces/IExecutionModule.sol, IExecutionQuote.sol.
    • src/access/ProtocolRoles.sol, the single shared owner (owner/transferOwnership/isOwner) that ModuleRegistry, IntentRegistry, and ScorePolicy all defer to instead of each holding their own owner state. Deliberately not multi-role RBAC for now.
    • There is no src/settlement/ — a standalone settlement layer was dropped from the design (see README). The winning module's execute() call is the on-chain settlement; there's no separate settlement step.
    • test/ — Foundry tests (ExecutionEngine.t.sol, ModuleCompetition.t.sol — real competing modules, not mocks, exercising ScorePolicy weighting — and Governance.t.solProtocolRoles/ScorePolicy governance) plus test/mocks/ (mock modules used in ExecutionEngine.t.sol).
    • script/Deploy.s.sol — deploys the 5 core kernel contracts (ProtocolRoles → registries/ScorePolicyExecutionEngine) for a given PROTOCOL_OWNER, with zero intents/modules registered by default — the shape a real customer deployment needs (see docs/architecture/provisioning.md). The demo ROUTE intent + both example modules are wired only when DEPLOY_DEMO_MODULES=true is explicitly set, and only when PROTOCOL_OWNER is left to default to the broadcaster (demo wiring registers on-chain as the broadcaster, so it can't also honor a separate owner in the same run). run() parses env vars and delegates to deploy(DeployParams), which holds the actual logic and is what test/Deploy.t.sol calls directly.
  • packages/types — zero-runtime-dependency TypeScript mirrors of the on-chain types: ExecutionQuote, ScorePolicy.Weights, module/intent registration shapes and events. uint256 fields are bigint; the ScorePolicy.evaluate() score is signed (ExecutionScore). Consumed as TS source (no build step) — see sdk/execution-node below.

  • packages/sdk — a viem-based typed client (intent/, execution/, registry/, abi/ subdirs). createExecutionKernelClient(...) bundles one client per contract (intentRegistry, moduleRegistry, scorePolicy, execution, module); intentBuilder.intentType(label) hashes a label exactly like Solidity's keccak256("label"). Every onlyOwner/mutating call simulates via publicClient.simulateContract before submitting. examples/quickstart.ts is a runnable, verified end-to-end example against a local anvil deployment — read it before writing new sdk code.

  • apps/execution-node — the off-chain pipeline consuming packages/sdk: engine/intentProcessor.ts (raw request → Intent), engine/executionGraphBuilder.ts (gas-free off-chain preview of what ExecutionEngine would currently select — reproduces the on-chain scoring exactly via read calls, doesn't reimplement it), solvers/solver.ts (one generic solver, not per-module — every module is scored identically, so a routerSolver/mevSolver split would be redundant boilerplate today), execution/executor.ts (submission). index.ts's runIntent(...) ties process → solve → submit together. "Execution graph" here means the current one-round competing-modules model, not chained multi-module execution (still just a future direction — see README).

  • apps/indexer — off-chain observability, also consuming packages/sdk. listeners/eventListener.ts is generic over (address, abi, eventName) (backfill via getContractEvents + a live watchContractEvent variant) rather than one listener per contract — every contract's events decode the same way through viem. processors/kernelEventProcessor.ts backfills all 5 kernel contracts' events into db/memoryStore.ts (deliberately in-memory — no real DB dependency until persistence across restarts actually matters). metrics/executionMetrics.ts derives totalExecutions/executionsByModule/moduleWinRate from the store. index.ts's createIndexer(...) backfills into a fresh store and returns it with the metrics bound.

  • packages/configlocalAnvil (a viem Chain), localAnvilAddresses/localAnvilModules (the deterministic addresses that fall out of deploying ProtocolRoles → IntentRegistry → ModuleRegistry → ScorePolicy → ExecutionEngine → RouterModule → MevProtectionModule, in that order, from anvil's default account #0 on a fresh chain — not a persistent deployment), ROUTE_INTENT_TYPE, and LOCAL_API_URL (apps/api's local dev default). Deliberately does not depend on packages/sdk (defines its own structurally-equivalent address shape instead) since sdk's own examples/quickstart.ts depends on config — a config → sdk → config cycle would exist otherwise. No real testnet/mainnet chain entry exists yet; add one only when an actual deployment happens.

  • apps/api — Fastify, consuming sdk/execution-node/indexer/config. Deliberately read-only: registry state (intentsController, modulesController), off-chain gas-free predictions (/modules/:intentType/predict, wraps execution-node's solve()), indexer metrics (metricsController: /metrics/executions, /metrics/executions/:intentType/win-rate/:module), and raw per-transaction history (executionsController: GET /executions?intentType=&limit=, most-recent-first, capped at 100 — exposes apps/indexer's individual IntentExecutionRecords, which previously only the aggregate metrics endpoints surfaced). Every controller backfills a fresh indexer from block 0 on every request — fine on local anvil's handful of blocks, revisit before pointing this at a chain with real history. No execute/submit route: that would mean the API custodying a private key on callers' behalf, with its own auth/rate-limiting design to work out first — a separate decision, not folded in here. utils/json.ts's toJsonSafe() is required on every response — Fastify's default serializer throws on bigint (most reads return one) and silently drops a Map (executionsByModule()'s return type) to {}. Defaults to port 4000 (not 3000 — that collides with apps/frontend's default next dev/next start port, and the two are meant to run side by side), with open CORS registered (@fastify/cors) since apps/frontend fetches it cross-origin.

  • apps/frontend — Next.js (App Router — not pages/, despite the README's original sketch predating the App Router becoming the default; src/app/ holds layout.tsx/page.tsx/providers.tsx), React, TypeScript, wagmi + viem. Styled as a restrained "protocol console" (Tailwind v4 token system in globals.css: --bg/--surface/--border/--ink/--muted/--accent + a --success/--warning/--danger trio kept visually distinct from --accent — every component consumes these tokens, both themes fully specified, not just <body>; Geist Sans/Mono, already fetched by the original scaffold but previously overridden by a stray literal font-family: Arial, now actually used: Sans for UI text, Mono for every technical value). Unlike apps/api, this one does submit transactions — through the user's own connected wallet (useWalletClient()), never a server-held key, so the custody concern that kept apps/api read-only doesn't apply here.

    • components/execution/ExecutionConsole.tsx is the primary surface: intent → every candidate module's real score, not just the winner's (CandidateModuleRow.tsx, visually distinct "Selected" state) → execute (explicit phase text: submitting/confirming/confirmed/failed, plus block number and gas used, not just a bare tx hash) → protocol metrics. components/protocol/OverviewStats.tsx and components/metrics/RecentExecutions.tsx (real per-transaction history via a new apps/api /executions endpoint — deliberately doesn't show historical candidate scores, since the indexer never persisted the losing candidates' quotes at execution time; fabricating them would violate the app's own data-integrity rule) sit in a side rail on desktop, stacking on mobile. components/layout/AppHeader.tsx always shows network identity (right vs. wrong chain, connected or not) — a wallet-writing app with no such indicator was a real gap; a wrong-network wallet would otherwise fail silently at execute time.
    • services/kernelClient.ts bridges wagmi's viem clients into sdk's createExecutionKernelClient(...); hooks/ wrap that in React Query (useIntents, useModules, usePrediction — reuses execution-node's solve() directly, since it's pure viem and works fine in the browser, not just Node; useExecutionMetrics/useRecentExecutions are the two hooks that do NOT talk to the chain directly — they call apps/api, completing the chain-event → indexer → api → frontend loop instead of the frontend re-deriving this itself). state/ isn't populated — local component state is enough at this app's current complexity; don't add a global store speculatively.
    • lib/wagmiConfig.ts sets ssr: true — required, not optional: without it wagmi's hooks report "disconnected, no connectors" on the server but read the real wallet on the client's first render, and React throws that mismatch away as a hydration error (any real visitor with a wallet extension installed hits this, not just a test). Only connects to localAnvil (from packages/config) — needs a browser wallet pointed at a local anvil node to use.
    • e2e/full-flow.spec.ts (Playwright) — the actual "connect wallet → read registry → construct intent → simulate → wallet confirmation → executeIntent() → transaction confirmed → indexer observes event → API exposes updated metrics → frontend displays result" flow, driven through a real headless browser against the real dev server, real wagmi hooks, real sdk calls, a real anvil chain, and the real apps/indexer/apps/api packages. No real wallet extension exists to automate, so the one thing replaced is the wallet's confirmation popup: page.addInitScript injects an EIP-1193 provider (both legacy window.ethereum and an EIP-6963 announcement, so wagmi's injected() connector finds it either way) that proxies every RPC call straight to anvil, which auto-signs eth_sendTransaction for its own unlocked default accounts — no private key material touches the browser. Get eth_accounts vs eth_requestAccounts right if you touch this: the former must return [] until the latter has actually been called once, or wagmi silently auto-reconnects on page load before the test (or a real user) ever clicks anything. Requires anvil (fully deployed, same sequence as packages/sdk/examples/quickstart.ts) and apps/api already running before npm run e2e; playwright.config.ts manages only the frontend dev server itself.
  • apps/landing — the public marketing site for exekpro.com, deliberately separate from apps/frontend: frontend is the technical protocol console (wallet-connected, chain-reading); landing is a static, no-wallet product page explaining the architecture to prospective developers/customers. Has zero @execution-kernel-protocol/* dependencies and no wagmi/viem/React Query — pure marketing content, so it carries none of the console's runtime complexity. One deliberate dark theme (not the console's adaptive light/dark), reusing the same Geist Sans/Mono fonts for brand continuity without a shared design-token file (the two apps' visual identities are intentionally distinct — console is a data-dense tool, landing is a product page). lib/links.ts centralizes the only three external URLs the page uses (GitHub repo, a doc anchor, and the console at https://exekpro.com/apps/frontend's own Cloudflare Worker, deployed alongside this app at https://exekpro.com/about). Content is careful never to claim mainnet deployment, real customers, revenue, or an open permissionless module ecosystem — none of which exist yet.

Dependency direction: packages/typespackages/contracts (mirrors on-chain types) → packages/sdk (wraps contract bindings) → apps/execution-node/apps/indexer (both consume the SDK: one to orchestrate execution, the other to observe it) → apps/api/apps/frontend (api consumes sdk+execution-node+indexer together, read-only; frontend consumes sdk+execution-node directly from the browser, and can write via the user's own wallet). packages/config sits alongside sdk (depends only on types, not sdk) and is consumed by sdk/execution-node/indexer's example scripts plus apps/api/apps/frontend directly. apps/landing sits outside this dependency graph entirely — it consumes nothing from packages/. Never point a dependency the other way (e.g. contracts must not import from sdk, and config must not import from sdk).

Scan / audit scope rules

  • Priority for security review, in order: packages/contracts/src/core, packages/contracts/src/modules, packages/contracts/src/policy, packages/contracts/src/access. These hold the intent-execution and module-selection trust boundary.
  • Do not read packages/contracts/test/mocks or docs/ unless the task explicitly asks for them — mocks are test fixtures, not production logic, and docs/ is currently just a placeholder.
  • packages/contracts/out/ and packages/contracts/cache/ are Foundry build artifacts, gitignored — never read or grep them; if they're missing, that's expected, not an error.
  • node_modules/ anywhere in the repo is gitignored and never worth reading/grepping — same rule as out//cache/.
  • Before any analysis of the contracts, run forge build (from packages/contracts/) first to surface compile errors before reasoning about the code.

Development workflow

  • After editing anything under packages/contracts/src/core/ or packages/contracts/src/modules/, always run forge test (from packages/contracts/) before considering the task done.
  • After editing anything under packages/types/, packages/sdk/, packages/config/, apps/execution-node/, apps/indexer/, or apps/api/, run npm run typecheck (from the repo root) before considering the task done. None of these six have a build step — they're consumed as source, so a clean typecheck is the bar, not a successful build.
  • After editing anything under apps/frontend/, run npm run typecheck (repo root) same as the others, but also run next build (from apps/frontend/) — unlike the other six, this one is a real Next.js app with an actual build/bundle step, and tsc --noEmit alone won't catch everything next build does (e.g. the client/server component boundary).
  • Never commit changes touching packages/contracts/src/core/ or packages/contracts/src/access/ without first showing and getting explicit review of the diff — these are the trust-boundary paths.

Commit conventions

Conventional Commits, scoped to this repo's packages:

<type>(<scope>): <description>
  • Types: feat, fix, refactor, test, docs, chore, perf
  • Scopes: contracts, sdk, execution-node, types
  • One logical change per commit — don't bundle unrelated edits across scopes.
  • Never use --no-verify.
  • Squash WIP commits before merging.
  • Ask before committing if the diff touches access/, even if tests pass.