diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..e90bee08 --- /dev/null +++ b/.env.example @@ -0,0 +1,75 @@ +# Hermeschain — environment variables +# Copy to .env and fill in for local dev. + +# ============= LLM ============= +ANTHROPIC_API_KEY=sk-ant-... +HERMES_MODEL=claude-haiku-4-5-20251001 +LLM_PROVIDER=anthropic + +# ============= Database ============= +DATABASE_URL=postgresql://hermes:hermes@localhost:5432/hermeschain +# Optional read replica (TASK-322) +# READ_DATABASE_URL=postgresql://hermes:hermes@replica:5432/hermeschain +# Pool tuning (TASK-319) +PG_POOL_MAX=20 +PG_POOL_IDLE_MS=30000 +PG_POOL_CONNECT_MS=2000 +# Slow query threshold (TASK-321) +PG_SLOW_QUERY_MS=1000 + +# ============= Redis ============= +REDIS_URL=redis://localhost:6379 +# Cross-replica event bridge (TASK-330) +REDIS_BRIDGE_ENABLED=true +# Cache warmer at boot (TASK-328) — defaults true in production +CACHE_WARMER_ENABLED=true + +# ============= Auth ============= +ADMIN_TOKEN= # required to mint API keys +# ADMIN_TOKEN_SECONDARY= # set during rotation (TASK-346) + +# ============= Service role ============= +# 'web' or 'worker' +AGENT_ROLE=web + +# ============= Worker ============= +AUTO_GIT_PUSH=true # auto-push agent commits when GITHUB_TOKEN present +GITHUB_TOKEN=ghp_... +AGENT_REPO_ROOT=/path/to/repo + +# ============= Mesh ============= +HERMES_PEER_ID= +HERMES_PUBLIC_URL=https://hermeschain.io +HERMES_PUBLIC_KEY= +# Comma-separated peer URLs to announce to on boot +HERMES_BOOTSTRAP_PEERS= + +# ============= SSE ============= +# Set to 'false' on replicas that should NOT serve SSE (TASK-331) +SSE_REPLICA=true + +# ============= Block production ============= +# Override default 10 OPEN reward (TASK-039) +HERMES_BLOCK_REWARD_WEI=10000000000000000000 + +# ============= API observability ============= +# Slow request log threshold (TASK-148) +SLOW_REQUEST_MS=1000 +# Comma-separated CORS allowlist (TASK-145); empty = open +CORS_ORIGINS= +# JSON body size limit (TASK-340) +JSON_BODY_LIMIT=1mb + +# ============= Backups (TASK-323/324) ============= +S3_BACKUP_BUCKET= +S3_BACKUP_PREFIX=hermes-backups/ +AWS_REGION=us-east-1 +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +BACKUP_RETAIN_DAYS=30 +RESTORE_DATABASE_URL= + +# ============= Build info (TASK-150) ============= +# Set by deploy pipeline; falls back to git rev-parse if absent +HERMES_BUILD_COMMIT= +HERMES_BUILD_TIME= diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 00000000..bd81e507 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Something is broken +labels: bug +--- + +## What happened + + + +## Expected behavior + + + +## Reproduction + +1. +2. +3. + +## Environment + +- Hermeschain version / commit: +- Node version: +- OS: +- Browser (if HUD-related): + +## Logs + +``` + +``` diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 00000000..7e425f82 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,26 @@ +--- +name: Feature request +about: Something you'd like Hermeschain to do +labels: enhancement +--- + +## Problem + + + +## Proposed solution + + + +## Alternatives considered + + + +## Acceptance criteria + +- [ ] +- [ ] + +## Linked task ID (if any) + + diff --git a/.github/ISSUE_TEMPLATE/security.md b/.github/ISSUE_TEMPLATE/security.md new file mode 100644 index 00000000..5cb6814a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/security.md @@ -0,0 +1,32 @@ +--- +name: Security report +about: Security disclosures should usually go to the contact in security.txt instead +labels: security +--- + +> **PRIVATE DISCLOSURE PREFERRED.** For sensitive vulnerabilities, please follow +> the contact info in `frontend/public/.well-known/security.txt` rather than +> opening a public issue. + +## Severity + +- [ ] Critical (RCE, key extraction, fund loss) +- [ ] High (auth bypass, privilege escalation) +- [ ] Medium (info disclosure, DoS) +- [ ] Low (best practice violation) + +## What's the issue + + + +## Reproduction + + + +## Affected components + +- [ ] Backend / API +- [ ] Frontend / HUD +- [ ] Smart contracts (VM) +- [ ] Agent worker / Git integration +- [ ] Infrastructure / deploy diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..074e9b86 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,30 @@ +# PR + +## Summary + + + +- + +## Linked task IDs + + + +- + +## Test plan + +- [ ] `npm run build` clean +- [ ] `npm test` clean (or specific tests added/updated) +- [ ] Manual verification step: + +## Risk / blast radius + +- [ ] Local-only / docs / no behavior change +- [ ] Single endpoint or component +- [ ] Cross-system (DB schema, mempool, consensus, fork-choice) +- [ ] Public/external impact (visible URLs, message format breaking) + +## Rollback plan + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..4f15030d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,32 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/backend" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + open-pull-requests-limit: 10 + labels: + - "dependencies" + - "backend" + + - package-ecosystem: "npm" + directory: "/frontend" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + open-pull-requests-limit: 10 + labels: + - "dependencies" + - "frontend" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + labels: + - "ci" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..606e2bd9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,81 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + build-backend: + runs-on: ubuntu-latest + strategy: + matrix: + node: [20, 22] + + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: hermes + POSTGRES_PASSWORD: hermes + POSTGRES_DB: hermeschain_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U hermes" + --health-interval 5s + --health-timeout 3s + --health-retries 5 + redis: + image: redis:7-alpine + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 5s + --health-timeout 3s + --health-retries 5 + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: 'npm' + cache-dependency-path: backend/package-lock.json + + - name: Install + run: cd backend && npm ci || cd backend && npm install + + - name: Build + run: cd backend && npm run build + + - name: Test + env: + DATABASE_URL: postgresql://hermes:hermes@localhost:5432/hermeschain_test + REDIS_URL: redis://localhost:6379 + run: cd backend && npm test + + build-frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + - run: cd frontend && (npm ci || npm install) + - run: cd frontend && npm run build + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: npx prettier --check . diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..a1ffbf94 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,41 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: '47 6 * * 2' + +permissions: + actions: read + contents: read + security-events: write + +jobs: + analyze: + name: Analyze ${{ matrix.language }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + language: [javascript-typescript] + + steps: + - uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: security-extended + + - name: Build (TypeScript) + run: | + cd backend + npm install + npm run build + + - name: Analyze + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/npm-audit.yml b/.github/workflows/npm-audit.yml new file mode 100644 index 00000000..ed2664d5 --- /dev/null +++ b/.github/workflows/npm-audit.yml @@ -0,0 +1,27 @@ +name: npm audit + +on: + pull_request: + schedule: + - cron: '23 5 * * 1' + +permissions: + contents: read + +jobs: + audit: + runs-on: ubuntu-latest + strategy: + matrix: + path: [backend, frontend] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: npm audit + run: | + cd ${{ matrix.path }} + npm audit --audit-level=high diff --git a/.github/workflows/secrets-scan.yml b/.github/workflows/secrets-scan.yml new file mode 100644 index 00000000..ff92e3c1 --- /dev/null +++ b/.github/workflows/secrets-scan.yml @@ -0,0 +1,24 @@ +name: Secrets scan + +on: + pull_request: + push: + branches: [main] + schedule: + - cron: '17 4 * * 1' + +permissions: + contents: read + +jobs: + gitleaks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Run gitleaks + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..209e3ef4 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..b99b29e3 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +node_modules +dist +backend/dist +.next +build +*.min.js +*.min.css +package-lock.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..4d2523ad --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..40ff7a3e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,69 @@ +# Changelog + +All notable changes to Hermeschain. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow [SemVer](https://semver.org/). + +## [Unreleased] + +### Added (Tier-3 implementation pass — first sweep) + +- Multi-validator consensus: rotating producer + 2/3 quorum (TASK-013, TASK-014, TASK-010) +- Network peer mesh: PeerRegistry + `/api/mesh/*` routes + bootstrap heartbeat (TASK-005) +- VM JSON-op interpreter with dynamic gas + real receipt logs (TASK-061..065) +- CIMonitor file-watch with 5s debounce (TASK-O) +- Block.fromJSON deserializer + gossip-apply via `/api/mesh/block` (TASK-001, TASK-002) +- Header-only sync + bulk block fetch endpoints (TASK-003, TASK-004) +- Block timestamp drift + min-time validation (TASK-015, TASK-016) +- Mempool TTL + size cap + replacement-by-fee (TASK-019, TASK-020, TASK-021) +- Block size limit (1MB serialized) (TASK-022) +- Idempotent tx submit, mempool snapshot/by-hash, next-nonce hint (TASK-057, TASK-166, TASK-167, TASK-170) +- Per-validator block reward (env-tunable) (TASK-039) +- TPS endpoint + chain stats (TASK-051) +- Address validity checker + block search (TASK-137, TASK-153) +- 14 DB migrations (indexes, tables for slashing/contract code/storage/metadata/snapshots/peers/newsletter) (TASK-306..318, 0015) +- PG pool tuning + query histogram + slow-query log + `db.poolStats()` (TASK-319, TASK-320, TASK-321) +- pg_dump → S3 backup + restore + smoke check (TASK-323, TASK-324) +- migrate:down / migrate:status --dry-run / schema-diff CLIs (TASK-325, TASK-326, TASK-327) +- Redis cache warmer at boot + Redis pub/sub bridge for cross-replica events (TASK-328, TASK-330) +- SSE replica pinning + worker leader election (TASK-331, TASK-332) +- TypeScript SDK skeleton (`@hermeschain/sdk`) (TASK-273, TASK-274, TASK-276) +- Three-tier health checks + Prometheus `/api/metrics` + `/api/build` (TASK-149, TASK-150, TASK-152) +- Request-ID + NDJSON access log + slow-request log middleware (TASK-146, TASK-147, TASK-148) +- CORS allowlist + JSON body size cap + HTTPS redirect (TASK-145, TASK-340, TASK-360) +- Backend Sentry integration (lazy-loaded) (TASK-444) +- Newsletter signup endpoint (TASK-486) +- Counter example contract + first-contract + query tutorials (TASK-105, TASK-290, TASK-292) +- Glossary + FAQ + Roadmap + Contributing + Code of Conduct (TASK-295..300) +- Threat model + bug bounty + admin-token rotation + cert pinning + disclosure response (TASK-301, TASK-303, TASK-346, TASK-369, TASK-370) +- Five operator runbooks (db-down, agent-stuck, chain-halted, peer-partition, disk-full) (TASK-461..465) +- Multi-region deploy notes + PagerDuty mapping + Prometheus alert rules + Grafana dashboard (TASK-457, TASK-459, TASK-460, TASK-470) +- GitHub Actions: gitleaks, npm audit, CodeQL, full CI workflow (TASK-354, TASK-355, TASK-356, TASK-410) +- Issue templates + PR template + Dependabot + commitlint config (TASK-415, TASK-417, TASK-482, TASK-483) +- Prettier + Makefile + justfile + .nvmrc + .env.example + docker-compose (TASK-412, TASK-422, TASK-423, TASK-425, TASK-426, TASK-427) +- security.txt + robots.txt + RSS feed generator (TASK-302, TASK-438, TASK-485) +- Vanity address + bulk-keys generator scripts + Redis TTL audit (TASK-138, TASK-139, TASK-329) +- 490 task specs across 13 section files in `docs/backlog/queue/` + +### Fixed + +- GitHub link in HUD header (`hermeschain-dev` → `hermeschain-agent`) + +### Infrastructure + +- `paced-push.js` script — promotes commits from `tier-3-backlog` to `main` at controlled cadence (default 60/day) + +## [v0.2.0] — 2026-04-26 (Tier-2 closure) + +Tier-2 feature holes shipped. ForkManager wired into Chain.addBlock; tx +hash + account alias endpoints; DB migration runner; SSE event coverage; +`/api/hermes/chat` alias; `AUTO_GIT_PUSH` defaults true with +`GITHUB_TOKEN` present. + +## [v0.1.0] — 2026-04-25 (Tier-1 closure) + +Tier-1 security holes closed. Sign-verify on `POST /wallet/send`; admin- +gated `POST /auth/keys`; state rollback + mempool eviction on reorg; +persistent receipts. + +[Unreleased]: https://github.com/hermeschain-agent/hermeschain/compare/v0.2.0...HEAD +[v0.2.0]: https://github.com/hermeschain-agent/hermeschain/compare/v0.1.0...v0.2.0 +[v0.1.0]: https://github.com/hermeschain-agent/hermeschain/releases/tag/v0.1.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..2a4e9b6f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,19 @@ +# Code of Conduct + +Hermeschain follows the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). + +## Summary + +We pledge to make participation in this project a harassment-free experience for everyone. + +**Acceptable**: respectful disagreement, focused critique, constructive feedback. + +**Unacceptable**: trolling, insults, harassment (public or private), publishing others' private information, sexualized language or imagery. + +## Enforcement + +Report violations to the contact in [security.txt](frontend/public/.well-known/security.txt). Maintainers may remove comments, ban accounts, or close issues that violate the code. + +## Scope + +Applies in project spaces (issues, PRs, Discussions, Discord, Twitter when posting in an official capacity) and at events. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..5ebf9a7e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,41 @@ +# Contributing to Hermeschain + +Thanks for considering a contribution. This project is built primarily by an autonomous agent on a paced commit schedule, so most changes flow through the [TASK-NNN backlog](docs/backlog/queue.md). Human PRs are welcome alongside. + +## Quick start + +```bash +git clone https://github.com/hermeschain-agent/hermeschain.git +cd hermeschain +npm install +cd backend && npm install && npm run build +cd ../frontend && npm install && npm run dev +``` + +You'll need: +- Node 20+ +- (optional) PostgreSQL — without it, the backend uses in-memory fallback +- (optional) Redis — without it, caching is disabled + +## Picking work + +1. Browse [docs/backlog/queue.md](docs/backlog/queue.md) — it lists all 490 planned tasks across 13 sections, each with a detailed spec. +2. Pick a TASK-NNN from any section. +3. Read its spec in `docs/backlog/queue/NN-*.md` — it lists files, reuse hooks, API contracts, acceptance, verification. +4. Comment on a GitHub Discussion or open a draft PR claiming the task. + +## PR rules + +- Conventional commits: `feat: ...`, `fix: ...`, `docs: ...`, etc. +- One TASK per commit (or related cluster). +- Title format: `feat(area): TASK-NNN — short description`. +- Run `npm run build` and `npm test` before pushing. +- Use the [PR template](.github/PULL_REQUEST_TEMPLATE.md). + +## Review + +A maintainer or the agent will review within ~48 hours. Smaller PRs (< 200 LOC) usually merge faster. + +## Code of conduct + +[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) applies to all spaces. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..3df49163 --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +.PHONY: help install dev build test lint format clean migrate-status backup restore pace-push + +help: + @echo "Hermeschain — common targets" + @echo " install Install backend + frontend deps" + @echo " dev Start backend (ts-node-dev)" + @echo " build Build backend" + @echo " test Build + run unit tests" + @echo " lint Run prettier --check" + @echo " format Run prettier --write" + @echo " clean Remove backend/dist + frontend/dist" + @echo " migrate-status Show applied + pending migrations" + @echo " backup Run pg_dump → S3 (needs AWS env)" + @echo " restore Restore latest backup → RESTORE_DATABASE_URL" + @echo " pace-push Push next commit on tier-3-backlog → main" + +install: + cd backend && npm install + cd frontend && npm install + +dev: + cd backend && npm run dev + +build: + cd backend && npm run build + +test: + cd backend && npm test + +lint: + npx prettier --check . + +format: + npx prettier --write . + +clean: + rm -rf backend/dist frontend/dist + +migrate-status: + cd backend && npm run migrate:status + +backup: + cd backend && npm run backup + +restore: + cd backend && npm run restore -- --latest + +pace-push: + cd backend && npm run pace:push diff --git a/backend/dist/agent/PacedPusher.d.ts b/backend/dist/agent/PacedPusher.d.ts new file mode 100644 index 00000000..931d63df --- /dev/null +++ b/backend/dist/agent/PacedPusher.d.ts @@ -0,0 +1,36 @@ +/** + * PacedPusher — runs inside the Railway worker process. Every PUSH_INTERVAL_MS + * (default 24min = 60/day), fetches origin and advances refs/heads/main by + * one commit drawn from origin/PUSH_BRANCH (default tier-3-backlog). + * + * Disabled by default. Activate by setting: + * PACED_PUSH_ENABLED=true + * GITHUB_TOKEN= + * + * Optional: + * PUSH_BRANCH=tier-3-backlog + * PUSH_TARGET=main + * PUSH_REMOTE=origin + * PUSH_BATCH=1 + * PUSH_INTERVAL_MS=1440000 (24 minutes) + * POINTER_FILE=data/push_pointer.txt + */ +export declare class PacedPusher { + private interval; + private repoRoot; + private pointerFile; + private branch; + private target; + private remote; + private batch; + private intervalMs; + constructor(repoRoot: string); + start(): void; + stop(): void; + private git; + private listForwardCommits; + private readPointer; + private writePointer; + private tick; +} +//# sourceMappingURL=PacedPusher.d.ts.map \ No newline at end of file diff --git a/backend/dist/agent/PacedPusher.d.ts.map b/backend/dist/agent/PacedPusher.d.ts.map new file mode 100644 index 00000000..9f6a8e9b --- /dev/null +++ b/backend/dist/agent/PacedPusher.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"PacedPusher.d.ts","sourceRoot":"","sources":["../../src/agent/PacedPusher.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;GAgBG;AAEH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,UAAU,CAAS;gBAEf,QAAQ,EAAE,MAAM;IAa5B,KAAK,IAAI,IAAI;IAmBb,IAAI,IAAI,IAAI;IAOZ,OAAO,CAAC,GAAG;IAOX,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,IAAI;CAkCb"} \ No newline at end of file diff --git a/backend/dist/agent/PacedPusher.js b/backend/dist/agent/PacedPusher.js new file mode 100644 index 00000000..5d0cd902 --- /dev/null +++ b/backend/dist/agent/PacedPusher.js @@ -0,0 +1,177 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PacedPusher = void 0; +const child_process_1 = require("child_process"); +const fs = __importStar(require("fs")); +const path = __importStar(require("path")); +/** + * PacedPusher — runs inside the Railway worker process. Every PUSH_INTERVAL_MS + * (default 24min = 60/day), fetches origin and advances refs/heads/main by + * one commit drawn from origin/PUSH_BRANCH (default tier-3-backlog). + * + * Disabled by default. Activate by setting: + * PACED_PUSH_ENABLED=true + * GITHUB_TOKEN= + * + * Optional: + * PUSH_BRANCH=tier-3-backlog + * PUSH_TARGET=main + * PUSH_REMOTE=origin + * PUSH_BATCH=1 + * PUSH_INTERVAL_MS=1440000 (24 minutes) + * POINTER_FILE=data/push_pointer.txt + */ +class PacedPusher { + constructor(repoRoot) { + this.interval = null; + this.repoRoot = repoRoot; + this.pointerFile = path.resolve(repoRoot, process.env.POINTER_FILE || 'data/push_pointer.txt'); + this.branch = process.env.PUSH_BRANCH || 'tier-3-backlog'; + this.target = process.env.PUSH_TARGET || 'main'; + this.remote = process.env.PUSH_REMOTE || 'origin'; + this.batch = Math.max(1, Number(process.env.PUSH_BATCH || '1')); + this.intervalMs = Math.max(60000, Number(process.env.PUSH_INTERVAL_MS || '1440000')); + } + start() { + if (this.interval) + return; + if (process.env.PACED_PUSH_ENABLED !== 'true') { + console.log('[PACER] disabled (set PACED_PUSH_ENABLED=true to activate)'); + return; + } + if (!process.env.GITHUB_TOKEN) { + console.log('[PACER] disabled (no GITHUB_TOKEN)'); + return; + } + console.log(`[PACER] active — every ${Math.round(this.intervalMs / 60000)} min, ` + + `${this.batch} commit(s)/fire from ${this.branch} → ${this.target}`); + // Fire once shortly after boot, then on the interval. + setTimeout(() => this.tick(), 30000); + this.interval = setInterval(() => this.tick(), this.intervalMs); + } + stop() { + if (this.interval) { + clearInterval(this.interval); + this.interval = null; + } + } + git(args) { + try { + return (0, child_process_1.execSync)(['git', ...args].join(' '), { + cwd: this.repoRoot, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }).trim(); + } + catch (err) { + // Surface stderr so the push failure reason is visible in logs. + const stderr = err?.stderr?.toString?.() || ''; + const stdout = err?.stdout?.toString?.() || ''; + const combined = [stderr, stdout].filter(Boolean).join(' ').trim(); + const wrapped = new Error(`${err?.message || 'git failed'}${combined ? ' — ' + combined : ''}`); + throw wrapped; + } + } + listForwardCommits() { + const ref = `${this.remote}/${this.target}..${this.remote}/${this.branch}`; + try { + const out = this.git(['rev-list', '--reverse', ref]); + return out ? out.split('\n').filter(Boolean) : []; + } + catch (err) { + console.error(`[PACER] enumerate failed: ${err?.message || err}`); + return []; + } + } + readPointer() { + try { + const raw = fs.readFileSync(this.pointerFile, 'utf8').trim(); + const n = parseInt(raw, 10); + return Number.isFinite(n) && n >= 0 ? n : 0; + } + catch { + return 0; + } + } + writePointer(n) { + fs.mkdirSync(path.dirname(this.pointerFile), { recursive: true }); + fs.writeFileSync(this.pointerFile, `${n}\n`); + } + tick() { + try { + // Unshallow first — GitIntegration clones with --depth 50, so the + // intermediate commits we want to push aren't in the local object + // store yet. --unshallow is a no-op once the repo is already deep. + try { + this.git(['fetch', '--unshallow', this.remote]); + } + catch { /* already unshallow, ignore */ } + // Force-fetch all branches via explicit refspec — single-branch + // clones don't have origin/ tracking refs by default. + this.git(['fetch', this.remote, '+refs/heads/*:refs/remotes/' + this.remote + '/*']); + } + catch (err) { + console.warn(`[PACER] fetch failed: ${err?.message || err}`); + return; + } + const queue = this.listForwardCommits(); + if (queue.length === 0) { + console.log(`[PACER] nothing to push (target = branch tip)`); + return; + } + console.log(`[PACER] ${queue.length} commit(s) ahead; pushing up to ${this.batch}`); + let pushed = 0; + for (let i = 0; i < this.batch && i < queue.length; i++) { + const sha = queue[i]; + try { + const subject = this.git(['log', '-1', '--pretty=%s', sha]); + this.git(['push', this.remote, `${sha}:refs/heads/${this.target}`]); + console.log(`[PACER] pushed ${sha.slice(0, 8)} ${subject}`); + pushed++; + } + catch (err) { + console.error(`[PACER] push of ${sha} failed: ${err?.message || err}`); + break; + } + } + if (pushed > 0) { + const before = this.readPointer(); + this.writePointer(before + pushed); + } + } +} +exports.PacedPusher = PacedPusher; +//# sourceMappingURL=PacedPusher.js.map \ No newline at end of file diff --git a/backend/dist/agent/PacedPusher.js.map b/backend/dist/agent/PacedPusher.js.map new file mode 100644 index 00000000..614abab1 --- /dev/null +++ b/backend/dist/agent/PacedPusher.js.map @@ -0,0 +1 @@ +{"version":3,"file":"PacedPusher.js","sourceRoot":"","sources":["../../src/agent/PacedPusher.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/backend/dist/agent/leaderElection.d.ts b/backend/dist/agent/leaderElection.d.ts new file mode 100644 index 00000000..8a991228 --- /dev/null +++ b/backend/dist/agent/leaderElection.d.ts @@ -0,0 +1,7 @@ +import Redis from 'ioredis'; +export interface LeaderHandle { + isLeader(): boolean; + release(): Promise; +} +export declare function startLeaderElection(redis: Redis, leaseId: string, onAcquire: () => void, onLose: () => void): Promise; +//# sourceMappingURL=leaderElection.d.ts.map \ No newline at end of file diff --git a/backend/dist/agent/leaderElection.d.ts.map b/backend/dist/agent/leaderElection.d.ts.map new file mode 100644 index 00000000..ae719a19 --- /dev/null +++ b/backend/dist/agent/leaderElection.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"leaderElection.d.ts","sourceRoot":"","sources":["../../src/agent/leaderElection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,SAAS,CAAC;AAiC5B,MAAM,WAAW,YAAY;IAC3B,QAAQ,IAAI,OAAO,CAAC;IACpB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,IAAI,EACrB,MAAM,EAAE,MAAM,IAAI,GACjB,OAAO,CAAC,YAAY,CAAC,CAgEvB"} \ No newline at end of file diff --git a/backend/dist/agent/leaderElection.js b/backend/dist/agent/leaderElection.js new file mode 100644 index 00000000..5f862d50 --- /dev/null +++ b/backend/dist/agent/leaderElection.js @@ -0,0 +1,95 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.startLeaderElection = startLeaderElection; +/** + * Worker leader election (TASK-332). + * + * Two worker replicas would otherwise both run AgentWorker and produce + * duplicate commits. We hold a Redis lease keyed by 'hermes:worker:leader' + * with TTL; the leader renews periodically (atomically, only if it still + * owns the lease). A follower keeps polling and takes over within ~30s + * if the leader vanishes. + */ +const LEASE_KEY = 'hermes:worker:leader'; +const LEASE_TTL_MS = 30000; +const RENEW_INTERVAL_MS = 10000; +// Atomic compare-and-renew: only EXPIRE if the value still matches. +const RENEW_SCRIPT = ` +if redis.call('get', KEYS[1]) == ARGV[1] then + return redis.call('pexpire', KEYS[1], ARGV[2]) +else + return 0 +end +`; +const RELEASE_SCRIPT = ` +if redis.call('get', KEYS[1]) == ARGV[1] then + return redis.call('del', KEYS[1]) +else + return 0 +end +`; +async function startLeaderElection(redis, leaseId, onAcquire, onLose) { + let owner = false; + let renewTimer = null; + let pollTimer = null; + async function tryAcquire() { + const ok = await redis.set(LEASE_KEY, leaseId, 'PX', LEASE_TTL_MS, 'NX'); + return ok === 'OK'; + } + async function renew() { + const result = await redis.eval(RENEW_SCRIPT, 1, LEASE_KEY, leaseId, String(LEASE_TTL_MS)); + return result === 1; + } + async function loop() { + if (!owner) { + const got = await tryAcquire(); + if (got) { + owner = true; + console.log(`[LEADER] acquired lease as ${leaseId}`); + onAcquire(); + renewTimer = setInterval(async () => { + try { + const renewed = await renew(); + if (!renewed) { + owner = false; + if (renewTimer) + clearInterval(renewTimer); + renewTimer = null; + console.log(`[LEADER] lost lease ${leaseId}`); + onLose(); + } + } + catch (err) { + console.warn(`[LEADER] renew error: ${err?.message || err}`); + } + }, RENEW_INTERVAL_MS); + } + } + } + // First attempt + repeat for follower mode. + await loop(); + pollTimer = setInterval(loop, LEASE_TTL_MS); + if (!owner) { + console.log(`[LEADER] follower mode (${leaseId})`); + } + return { + isLeader: () => owner, + async release() { + if (renewTimer) + clearInterval(renewTimer); + if (pollTimer) + clearInterval(pollTimer); + if (owner) { + try { + await redis.eval(RELEASE_SCRIPT, 1, LEASE_KEY, leaseId); + owner = false; + console.log(`[LEADER] released lease ${leaseId}`); + } + catch (err) { + console.warn(`[LEADER] release error: ${err?.message || err}`); + } + } + }, + }; +} +//# sourceMappingURL=leaderElection.js.map \ No newline at end of file diff --git a/backend/dist/agent/leaderElection.js.map b/backend/dist/agent/leaderElection.js.map new file mode 100644 index 00000000..f2bb7e52 --- /dev/null +++ b/backend/dist/agent/leaderElection.js.map @@ -0,0 +1 @@ +{"version":3,"file":"leaderElection.js","sourceRoot":"","sources":["../../src/agent/leaderElection.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/backend/dist/api/build-info.d.ts b/backend/dist/api/build-info.d.ts new file mode 100644 index 00000000..53e1ea12 --- /dev/null +++ b/backend/dist/api/build-info.d.ts @@ -0,0 +1,3 @@ +import { Router } from 'express'; +export declare function createBuildRouter(): Router; +//# sourceMappingURL=build-info.d.ts.map \ No newline at end of file diff --git a/backend/dist/api/build-info.d.ts.map b/backend/dist/api/build-info.d.ts.map new file mode 100644 index 00000000..2e19e127 --- /dev/null +++ b/backend/dist/api/build-info.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"build-info.d.ts","sourceRoot":"","sources":["../../src/api/build-info.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAsBjC,wBAAgB,iBAAiB,IAAI,MAAM,CAM1C"} \ No newline at end of file diff --git a/backend/dist/api/build-info.js b/backend/dist/api/build-info.js new file mode 100644 index 00000000..91ef722c --- /dev/null +++ b/backend/dist/api/build-info.js @@ -0,0 +1,31 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createBuildRouter = createBuildRouter; +const express_1 = require("express"); +const child_process_1 = require("child_process"); +/** + * /api/build endpoint (TASK-150). Surfaces commit + build time + version + * for debugging deploys. Computed once at module load to avoid running + * git on every request. + */ +function readGitCommit() { + if (process.env.HERMES_BUILD_COMMIT) + return process.env.HERMES_BUILD_COMMIT; + try { + return (0, child_process_1.execSync)('git rev-parse HEAD', { encoding: 'utf8' }).trim(); + } + catch { + return 'unknown'; + } +} +const COMMIT = readGitCommit(); +const BUILD_TIME = process.env.HERMES_BUILD_TIME || new Date().toISOString(); +const VERSION = process.env.npm_package_version || '0.0.0'; +function createBuildRouter() { + const router = (0, express_1.Router)(); + router.get('/', (_req, res) => { + res.json({ commit: COMMIT, buildTime: BUILD_TIME, version: VERSION }); + }); + return router; +} +//# sourceMappingURL=build-info.js.map \ No newline at end of file diff --git a/backend/dist/api/build-info.js.map b/backend/dist/api/build-info.js.map new file mode 100644 index 00000000..3183da9a --- /dev/null +++ b/backend/dist/api/build-info.js.map @@ -0,0 +1 @@ +{"version":3,"file":"build-info.js","sourceRoot":"","sources":["../../src/api/build-info.ts"],"names":[],"mappings":";;AAsBA,8CAMC;AA5BD,qCAAiC;AACjC,iDAAyC;AAEzC;;;;GAIG;AAEH,SAAS,aAAa;IACpB,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;IAC5E,IAAI,CAAC;QACH,OAAO,IAAA,wBAAQ,EAAC,oBAAoB,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACrE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;AAC/B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAC7E,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,OAAO,CAAC;AAE3D,SAAgB,iBAAiB;IAC/B,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;IACxB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QAC5B,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC;AAChB,CAAC"} \ No newline at end of file diff --git a/backend/dist/api/health.d.ts b/backend/dist/api/health.d.ts new file mode 100644 index 00000000..7e62c475 --- /dev/null +++ b/backend/dist/api/health.d.ts @@ -0,0 +1,5 @@ +import { Router } from 'express'; +import type { Chain } from '../blockchain/Chain'; +export declare function markShuttingDown(): void; +export declare function createHealthRouter(chain: Chain): Router; +//# sourceMappingURL=health.d.ts.map \ No newline at end of file diff --git a/backend/dist/api/health.d.ts.map b/backend/dist/api/health.d.ts.map new file mode 100644 index 00000000..040a9c0a --- /dev/null +++ b/backend/dist/api/health.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/api/health.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEjC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAejD,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAoEvD"} \ No newline at end of file diff --git a/backend/dist/api/health.js b/backend/dist/api/health.js new file mode 100644 index 00000000..0b473ecf --- /dev/null +++ b/backend/dist/api/health.js @@ -0,0 +1,85 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.markShuttingDown = markShuttingDown; +exports.createHealthRouter = createHealthRouter; +const express_1 = require("express"); +const db_1 = require("../database/db"); +/** + * Three-tier health checks (TASK-149). + * + * /health/live — process is up (no dependencies) + * /health/ready — can serve traffic (DB pool capacity, Redis up, chain bootstrapped) + * /health/deep — exercises full read paths with timing (slower) + * + * Shutdown handler (existing) flips a flag to make /live also return 503 + * during drain so load balancers stop routing immediately. + */ +let isShuttingDown = false; +function markShuttingDown() { + isShuttingDown = true; +} +function createHealthRouter(chain) { + const router = (0, express_1.Router)(); + router.get('/live', (_req, res) => { + if (isShuttingDown) { + return res.status(503).json({ status: 'draining' }); + } + res.status(200).json({ status: 'live' }); + }); + router.get('/ready', async (_req, res) => { + if (isShuttingDown) { + return res.status(503).json({ ready: false, failures: [{ check: 'shutdown', reason: 'draining' }] }); + } + const failures = []; + const stats = db_1.db.poolStats(); + if (stats.max > 0 && stats.total >= stats.max && stats.idle === 0) { + failures.push({ check: 'pg_pool', reason: 'no idle connections' }); + } + if (process.env.REDIS_URL && !db_1.cache.isConnected()) { + failures.push({ check: 'redis', reason: 'disconnected' }); + } + if (chain.getChainLength() === 0) { + failures.push({ check: 'chain', reason: 'genesis not loaded' }); + } + if (failures.length > 0) { + return res.status(503).json({ ready: false, failures }); + } + res.json({ ready: true, checks: { pg: 'ok', redis: process.env.REDIS_URL ? 'ok' : 'skipped', chain: 'ok' } }); + }); + router.get('/deep', async (_req, res) => { + const failures = []; + const latency = {}; + let t = Date.now(); + try { + await db_1.db.query(`SELECT 1`); + latency.db = Date.now() - t; + } + catch (err) { + failures.push({ check: 'db', reason: err?.message || 'query failed' }); + } + if (process.env.REDIS_URL) { + t = Date.now(); + try { + await db_1.cache.get('health:deep:probe'); + latency.redis = Date.now() - t; + } + catch (err) { + failures.push({ check: 'redis', reason: err?.message || 'get failed' }); + } + } + t = Date.now(); + try { + chain.getLatestBlock(); + latency.chainHead = Date.now() - t; + } + catch (err) { + failures.push({ check: 'chainHead', reason: err?.message || 'fetch failed' }); + } + if (failures.length > 0) { + return res.status(503).json({ ready: false, failures, latencyMs: latency }); + } + res.json({ ready: true, latencyMs: latency }); + }); + return router; +} +//# sourceMappingURL=health.js.map \ No newline at end of file diff --git a/backend/dist/api/health.js.map b/backend/dist/api/health.js.map new file mode 100644 index 00000000..4a9960b4 --- /dev/null +++ b/backend/dist/api/health.js.map @@ -0,0 +1 @@ +{"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/api/health.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/backend/dist/api/jsonrpc.d.ts b/backend/dist/api/jsonrpc.d.ts new file mode 100644 index 00000000..b6d815e5 --- /dev/null +++ b/backend/dist/api/jsonrpc.d.ts @@ -0,0 +1,17 @@ +import { Router } from 'express'; +import type { Chain } from '../blockchain/Chain'; +/** + * Minimal Ethereum JSON-RPC compat layer (TASK-176). + * + * Mounted at POST /rpc. MetaMask + eth-tools that expect this protocol + * can read chain height + balance. Methods beyond this minimal set + * return -32601 (method not found). + * + * Methods implemented: + * eth_blockNumber → 0xN + * eth_getBalance(addr, _tag) → 0xN + * eth_chainId → 0x... (HERMES_CHAIN_ID env or default) + * net_version → string of chain id + */ +export declare function createJsonRpcRouter(chain: Chain): Router; +//# sourceMappingURL=jsonrpc.d.ts.map \ No newline at end of file diff --git a/backend/dist/api/jsonrpc.d.ts.map b/backend/dist/api/jsonrpc.d.ts.map new file mode 100644 index 00000000..c944b2da --- /dev/null +++ b/backend/dist/api/jsonrpc.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"jsonrpc.d.ts","sourceRoot":"","sources":["../../src/api/jsonrpc.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAGjD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAuCxD"} \ No newline at end of file diff --git a/backend/dist/api/jsonrpc.js b/backend/dist/api/jsonrpc.js new file mode 100644 index 00000000..c98beefb --- /dev/null +++ b/backend/dist/api/jsonrpc.js @@ -0,0 +1,57 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createJsonRpcRouter = createJsonRpcRouter; +const express_1 = require("express"); +const StateManager_1 = require("../blockchain/StateManager"); +/** + * Minimal Ethereum JSON-RPC compat layer (TASK-176). + * + * Mounted at POST /rpc. MetaMask + eth-tools that expect this protocol + * can read chain height + balance. Methods beyond this minimal set + * return -32601 (method not found). + * + * Methods implemented: + * eth_blockNumber → 0xN + * eth_getBalance(addr, _tag) → 0xN + * eth_chainId → 0x... (HERMES_CHAIN_ID env or default) + * net_version → string of chain id + */ +function createJsonRpcRouter(chain) { + const router = (0, express_1.Router)(); + router.post('/', (req, res) => { + const { id, method, params } = req.body || {}; + const respond = (result, error) => res.json({ jsonrpc: '2.0', id: id ?? null, ...(error ? { error } : { result }) }); + const chainId = process.env.HERMES_CHAIN_ID || '0x1f407'; + try { + switch (method) { + case 'eth_blockNumber': + return respond('0x' + chain.getChainLength().toString(16)); + case 'eth_chainId': + return respond(chainId); + case 'net_version': + return respond(String(parseInt(chainId, 16))); + case 'eth_getBalance': { + const addr = (params && params[0]) || ''; + if (!addr) + return respond(undefined, { code: -32602, message: 'invalid address' }); + const bal = StateManager_1.stateManager.getBalance(addr); + return respond('0x' + bal.toString(16)); + } + case 'eth_getTransactionCount': { + const addr = (params && params[0]) || ''; + if (!addr) + return respond(undefined, { code: -32602, message: 'invalid address' }); + const nonce = StateManager_1.stateManager.getNonce(addr); + return respond('0x' + nonce.toString(16)); + } + default: + return respond(undefined, { code: -32601, message: `method not found: ${method}` }); + } + } + catch (err) { + return respond(undefined, { code: -32603, message: err?.message || 'internal error' }); + } + }); + return router; +} +//# sourceMappingURL=jsonrpc.js.map \ No newline at end of file diff --git a/backend/dist/api/jsonrpc.js.map b/backend/dist/api/jsonrpc.js.map new file mode 100644 index 00000000..746085a4 --- /dev/null +++ b/backend/dist/api/jsonrpc.js.map @@ -0,0 +1 @@ +{"version":3,"file":"jsonrpc.js","sourceRoot":"","sources":["../../src/api/jsonrpc.ts"],"names":[],"mappings":";;AAiBA,kDAuCC;AAxDD,qCAAiC;AAEjC,6DAA0D;AAE1D;;;;;;;;;;;;GAYG;AACH,SAAgB,mBAAmB,CAAC,KAAY;IAC9C,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;IAExB,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAC5B,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;QAC9C,MAAM,OAAO,GAAG,CAAC,MAAY,EAAE,KAAyC,EAAE,EAAE,CAC1E,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;QAEpF,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,SAAS,CAAC;QAEzD,IAAI,CAAC;YACH,QAAQ,MAAM,EAAE,CAAC;gBACf,KAAK,iBAAiB;oBACpB,OAAO,OAAO,CAAC,IAAI,GAAG,KAAK,CAAC,cAAc,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC7D,KAAK,aAAa;oBAChB,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC;gBAC1B,KAAK,aAAa;oBAChB,OAAO,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;gBAChD,KAAK,gBAAgB,CAAC,CAAC,CAAC;oBACtB,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;oBACzC,IAAI,CAAC,IAAI;wBAAE,OAAO,OAAO,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;oBACnF,MAAM,GAAG,GAAG,2BAAY,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;oBAC1C,OAAO,OAAO,CAAC,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC1C,CAAC;gBACD,KAAK,yBAAyB,CAAC,CAAC,CAAC;oBAC/B,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;oBACzC,IAAI,CAAC,IAAI;wBAAE,OAAO,OAAO,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;oBACnF,MAAM,KAAK,GAAG,2BAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;oBAC1C,OAAO,OAAO,CAAC,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC5C,CAAC;gBACD;oBACE,OAAO,OAAO,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,qBAAqB,MAAM,EAAE,EAAE,CAAC,CAAC;YACxF,CAAC;QACH,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,OAAO,OAAO,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,IAAI,gBAAgB,EAAE,CAAC,CAAC;QACzF,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"} \ No newline at end of file diff --git a/backend/dist/api/metrics.d.ts b/backend/dist/api/metrics.d.ts new file mode 100644 index 00000000..b1c87128 --- /dev/null +++ b/backend/dist/api/metrics.d.ts @@ -0,0 +1,22 @@ +import { Router } from 'express'; +import type { Chain } from '../blockchain/Chain'; +import type { TransactionPool } from '../blockchain/TransactionPool'; +/** + * Prometheus exposition (TASK-152). Plain-text format consumed by + * Prometheus, Datadog (with the prom integration), Grafana cloud, etc. + * + * GET /api/metrics → text/plain; version=0.0.4 + * + * Metrics: + * hermes_chain_height + * hermes_mempool_size + * hermes_pg_pool_{total,idle,waiting,max} + * hermes_pg_query_count + * hermes_pg_query_errors_total + * hermes_pg_query_duration_ms_bucket{le=...} + * hermes_pg_query_duration_ms_sum + * hermes_pg_query_duration_ms_count + * hermes_peers_active + */ +export declare function createMetricsRouter(chain: Chain, txPool: TransactionPool): Router; +//# sourceMappingURL=metrics.d.ts.map \ No newline at end of file diff --git a/backend/dist/api/metrics.d.ts.map b/backend/dist/api/metrics.d.ts.map new file mode 100644 index 00000000..393b8c56 --- /dev/null +++ b/backend/dist/api/metrics.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/api/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAGrE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,GAAG,MAAM,CAkDjF"} \ No newline at end of file diff --git a/backend/dist/api/metrics.js b/backend/dist/api/metrics.js new file mode 100644 index 00000000..661adb11 --- /dev/null +++ b/backend/dist/api/metrics.js @@ -0,0 +1,67 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createMetricsRouter = createMetricsRouter; +const express_1 = require("express"); +const db_1 = require("../database/db"); +const queryMetrics_1 = require("../database/queryMetrics"); +const PeerRegistry_1 = require("../network/PeerRegistry"); +/** + * Prometheus exposition (TASK-152). Plain-text format consumed by + * Prometheus, Datadog (with the prom integration), Grafana cloud, etc. + * + * GET /api/metrics → text/plain; version=0.0.4 + * + * Metrics: + * hermes_chain_height + * hermes_mempool_size + * hermes_pg_pool_{total,idle,waiting,max} + * hermes_pg_query_count + * hermes_pg_query_errors_total + * hermes_pg_query_duration_ms_bucket{le=...} + * hermes_pg_query_duration_ms_sum + * hermes_pg_query_duration_ms_count + * hermes_peers_active + */ +function createMetricsRouter(chain, txPool) { + const router = (0, express_1.Router)(); + router.get('/', (_req, res) => { + const lines = []; + function gauge(name, value, help) { + lines.push(`# HELP ${name} ${help}`); + lines.push(`# TYPE ${name} gauge`); + lines.push(`${name} ${value}`); + } + function counter(name, value, help) { + lines.push(`# HELP ${name} ${help}`); + lines.push(`# TYPE ${name} counter`); + lines.push(`${name} ${value}`); + } + // Chain + gauge('hermes_chain_height', chain.getChainLength(), 'Synthetic chain height'); + gauge('hermes_mempool_size', txPool.getPendingCount(), 'Pending tx count'); + // PG pool + const pool = db_1.db.poolStats(); + gauge('hermes_pg_pool_total', pool.total, 'Total PG connections'); + gauge('hermes_pg_pool_idle', pool.idle, 'Idle PG connections'); + gauge('hermes_pg_pool_waiting', pool.waiting, 'Clients waiting for a PG connection'); + gauge('hermes_pg_pool_max', pool.max, 'Max PG connections'); + // PG query histogram + const h = (0, queryMetrics_1.getHistogram)(); + counter('hermes_pg_query_count', h.count, 'Total PG queries observed'); + counter('hermes_pg_query_errors_total', h.errors, 'Total PG query errors'); + lines.push('# HELP hermes_pg_query_duration_ms PG query latency in ms'); + lines.push('# TYPE hermes_pg_query_duration_ms histogram'); + for (let i = 0; i < h.buckets.length; i++) { + lines.push(`hermes_pg_query_duration_ms_bucket{le="${h.buckets[i]}"} ${h.cumulativeCounts[i]}`); + } + lines.push(`hermes_pg_query_duration_ms_bucket{le="+Inf"} ${h.count}`); + lines.push(`hermes_pg_query_duration_ms_sum ${h.sum}`); + lines.push(`hermes_pg_query_duration_ms_count ${h.count}`); + // Peers + gauge('hermes_peers_active', PeerRegistry_1.peerRegistry.listPeers().length, 'Active peers (last_seen within 180s)'); + res.set('Content-Type', 'text/plain; version=0.0.4'); + res.send(lines.join('\n') + '\n'); + }); + return router; +} +//# sourceMappingURL=metrics.js.map \ No newline at end of file diff --git a/backend/dist/api/metrics.js.map b/backend/dist/api/metrics.js.map new file mode 100644 index 00000000..8113388d --- /dev/null +++ b/backend/dist/api/metrics.js.map @@ -0,0 +1 @@ +{"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../src/api/metrics.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/backend/dist/api/middleware/accessLog.d.ts b/backend/dist/api/middleware/accessLog.d.ts new file mode 100644 index 00000000..5b1d08f9 --- /dev/null +++ b/backend/dist/api/middleware/accessLog.d.ts @@ -0,0 +1,3 @@ +import type { Request, Response, NextFunction } from 'express'; +export declare function accessLog(req: Request, res: Response, next: NextFunction): void; +//# sourceMappingURL=accessLog.d.ts.map \ No newline at end of file diff --git a/backend/dist/api/middleware/accessLog.d.ts.map b/backend/dist/api/middleware/accessLog.d.ts.map new file mode 100644 index 00000000..a5bef55e --- /dev/null +++ b/backend/dist/api/middleware/accessLog.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"accessLog.d.ts","sourceRoot":"","sources":["../../../src/api/middleware/accessLog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAc/D,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAuB/E"} \ No newline at end of file diff --git a/backend/dist/api/middleware/accessLog.js b/backend/dist/api/middleware/accessLog.js new file mode 100644 index 00000000..022da013 --- /dev/null +++ b/backend/dist/api/middleware/accessLog.js @@ -0,0 +1,39 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.accessLog = accessLog; +/** + * NDJSON access log + slow-request log (TASK-147 + TASK-148). + * + * Emits one JSON-line per request to stdout: ts, method, path, status, + * durationMs, bytes, requestId, ip. Easily fed into Logflare/Datadog/Axiom. + * + * Requests over SLOW_REQUEST_MS (default 1000) get tagged `slow:true` and + * also emit a console.warn so they surface in noisy log streams. + */ +const SLOW_REQUEST_MS = Math.max(1, Number(process.env.SLOW_REQUEST_MS || '1000')); +function accessLog(req, res, next) { + const startedAt = Date.now(); + res.on('finish', () => { + const durationMs = Date.now() - startedAt; + const slow = durationMs > SLOW_REQUEST_MS; + const line = { + ts: new Date().toISOString(), + method: req.method, + path: req.originalUrl, + status: res.statusCode, + durationMs, + bytes: Number(res.getHeader('content-length') ?? 0), + requestId: req.id, + ip: req.ip, + slow, + }; + if (slow) { + console.warn('[ACCESS SLOW]', JSON.stringify(line)); + } + else { + console.log(JSON.stringify(line)); + } + }); + next(); +} +//# sourceMappingURL=accessLog.js.map \ No newline at end of file diff --git a/backend/dist/api/middleware/accessLog.js.map b/backend/dist/api/middleware/accessLog.js.map new file mode 100644 index 00000000..68cf3ea7 --- /dev/null +++ b/backend/dist/api/middleware/accessLog.js.map @@ -0,0 +1 @@ +{"version":3,"file":"accessLog.js","sourceRoot":"","sources":["../../../src/api/middleware/accessLog.ts"],"names":[],"mappings":";;AAcA,8BAuBC;AAnCD;;;;;;;;GAQG;AAEH,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,MAAM,CAAC,CAAC,CAAC;AAEnF,SAAgB,SAAS,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACvE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,GAAG,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACpB,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;QAC1C,MAAM,IAAI,GAAG,UAAU,GAAG,eAAe,CAAC;QAC1C,MAAM,IAAI,GAAG;YACX,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC5B,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,IAAI,EAAE,GAAG,CAAC,WAAW;YACrB,MAAM,EAAE,GAAG,CAAC,UAAU;YACtB,UAAU;YACV,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;YACnD,SAAS,EAAG,GAAW,CAAC,EAAE;YAC1B,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,IAAI;SACL,CAAC;QACF,IAAI,IAAI,EAAE,CAAC;YACT,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QACtD,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QACpC,CAAC;IACH,CAAC,CAAC,CAAC;IACH,IAAI,EAAE,CAAC;AACT,CAAC"} \ No newline at end of file diff --git a/backend/dist/api/middleware/httpsRedirect.d.ts b/backend/dist/api/middleware/httpsRedirect.d.ts new file mode 100644 index 00000000..886c85da --- /dev/null +++ b/backend/dist/api/middleware/httpsRedirect.d.ts @@ -0,0 +1,10 @@ +import type { Request, Response, NextFunction } from 'express'; +/** + * HTTPS-only redirect middleware (TASK-360). + * + * In production behind a load balancer (Railway sets x-forwarded-proto), + * issue a 301 to the https equivalent if the request came in over http. + * No-op in development. + */ +export declare function httpsRedirect(req: Request, res: Response, next: NextFunction): void; +//# sourceMappingURL=httpsRedirect.d.ts.map \ No newline at end of file diff --git a/backend/dist/api/middleware/httpsRedirect.d.ts.map b/backend/dist/api/middleware/httpsRedirect.d.ts.map new file mode 100644 index 00000000..8d1b6c76 --- /dev/null +++ b/backend/dist/api/middleware/httpsRedirect.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"httpsRedirect.d.ts","sourceRoot":"","sources":["../../../src/api/middleware/httpsRedirect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE/D;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAQnF"} \ No newline at end of file diff --git a/backend/dist/api/middleware/httpsRedirect.js b/backend/dist/api/middleware/httpsRedirect.js new file mode 100644 index 00000000..f60c8d73 --- /dev/null +++ b/backend/dist/api/middleware/httpsRedirect.js @@ -0,0 +1,21 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.httpsRedirect = httpsRedirect; +/** + * HTTPS-only redirect middleware (TASK-360). + * + * In production behind a load balancer (Railway sets x-forwarded-proto), + * issue a 301 to the https equivalent if the request came in over http. + * No-op in development. + */ +function httpsRedirect(req, res, next) { + if (process.env.NODE_ENV !== 'production') + return next(); + const proto = req.headers['x-forwarded-proto']; + if (proto && proto !== 'https') { + const host = req.headers.host || ''; + return res.redirect(301, `https://${host}${req.originalUrl}`); + } + next(); +} +//# sourceMappingURL=httpsRedirect.js.map \ No newline at end of file diff --git a/backend/dist/api/middleware/httpsRedirect.js.map b/backend/dist/api/middleware/httpsRedirect.js.map new file mode 100644 index 00000000..f0cadca6 --- /dev/null +++ b/backend/dist/api/middleware/httpsRedirect.js.map @@ -0,0 +1 @@ +{"version":3,"file":"httpsRedirect.js","sourceRoot":"","sources":["../../../src/api/middleware/httpsRedirect.ts"],"names":[],"mappings":";;AASA,sCAQC;AAfD;;;;;;GAMG;AACH,SAAgB,aAAa,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IAC3E,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY;QAAE,OAAO,IAAI,EAAE,CAAC;IACzD,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC/C,IAAI,KAAK,IAAI,KAAK,KAAK,OAAO,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC;QACpC,OAAO,GAAG,CAAC,QAAQ,CAAC,GAAG,EAAE,WAAW,IAAI,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;IAChE,CAAC;IACD,IAAI,EAAE,CAAC;AACT,CAAC"} \ No newline at end of file diff --git a/backend/dist/api/middleware/requestId.d.ts b/backend/dist/api/middleware/requestId.d.ts new file mode 100644 index 00000000..ffefb41c --- /dev/null +++ b/backend/dist/api/middleware/requestId.d.ts @@ -0,0 +1,16 @@ +import type { Request, Response, NextFunction } from 'express'; +/** + * Request-ID middleware (TASK-146). Echoes incoming X-Request-ID or + * generates one. Available downstream as `req.id` (typed via module + * augmentation below) and on every response as X-Request-ID for client + * correlation in support tickets. + */ +declare global { + namespace Express { + interface Request { + id: string; + } + } +} +export declare function requestId(req: Request, res: Response, next: NextFunction): void; +//# sourceMappingURL=requestId.d.ts.map \ No newline at end of file diff --git a/backend/dist/api/middleware/requestId.d.ts.map b/backend/dist/api/middleware/requestId.d.ts.map new file mode 100644 index 00000000..2f592053 --- /dev/null +++ b/backend/dist/api/middleware/requestId.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"requestId.d.ts","sourceRoot":"","sources":["../../../src/api/middleware/requestId.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG/D;;;;;GAKG;AACH,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,EAAE,EAAE,MAAM,CAAC;SACZ;KACF;CACF;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAQ/E"} \ No newline at end of file diff --git a/backend/dist/api/middleware/requestId.js b/backend/dist/api/middleware/requestId.js new file mode 100644 index 00000000..3b1b1d45 --- /dev/null +++ b/backend/dist/api/middleware/requestId.js @@ -0,0 +1,14 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.requestId = requestId; +const crypto_1 = require("crypto"); +function requestId(req, res, next) { + const incoming = req.headers['x-request-id']; + const id = typeof incoming === 'string' && incoming.length > 0 && incoming.length <= 128 + ? incoming + : (0, crypto_1.randomUUID)(); + req.id = id; + res.setHeader('X-Request-ID', id); + next(); +} +//# sourceMappingURL=requestId.js.map \ No newline at end of file diff --git a/backend/dist/api/middleware/requestId.js.map b/backend/dist/api/middleware/requestId.js.map new file mode 100644 index 00000000..4a46df71 --- /dev/null +++ b/backend/dist/api/middleware/requestId.js.map @@ -0,0 +1 @@ +{"version":3,"file":"requestId.js","sourceRoot":"","sources":["../../../src/api/middleware/requestId.ts"],"names":[],"mappings":";;AAkBA,8BAQC;AAzBD,mCAAoC;AAiBpC,SAAgB,SAAS,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACvE,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7C,MAAM,EAAE,GAAG,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG;QACtF,CAAC,CAAC,QAAQ;QACV,CAAC,CAAC,IAAA,mBAAU,GAAE,CAAC;IACjB,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC;IACZ,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IAClC,IAAI,EAAE,CAAC;AACT,CAAC"} \ No newline at end of file diff --git a/backend/dist/api/middleware/sentry.d.ts b/backend/dist/api/middleware/sentry.d.ts new file mode 100644 index 00000000..18f1ecb3 --- /dev/null +++ b/backend/dist/api/middleware/sentry.d.ts @@ -0,0 +1,5 @@ +import type { Express } from 'express'; +export declare function attachSentryRequestHandler(app: Express): void; +export declare function attachSentryErrorHandler(app: Express): void; +export declare function captureException(err: any, ctx?: Record): void; +//# sourceMappingURL=sentry.d.ts.map \ No newline at end of file diff --git a/backend/dist/api/middleware/sentry.d.ts.map b/backend/dist/api/middleware/sentry.d.ts.map new file mode 100644 index 00000000..e5b8ef52 --- /dev/null +++ b/backend/dist/api/middleware/sentry.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"sentry.d.ts","sourceRoot":"","sources":["../../../src/api/middleware/sentry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAmC,MAAM,SAAS,CAAC;AAgCxE,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAI7D;AAED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAI3D;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAI1E"} \ No newline at end of file diff --git a/backend/dist/api/middleware/sentry.js b/backend/dist/api/middleware/sentry.js new file mode 100644 index 00000000..9d6457a7 --- /dev/null +++ b/backend/dist/api/middleware/sentry.js @@ -0,0 +1,56 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.attachSentryRequestHandler = attachSentryRequestHandler; +exports.attachSentryErrorHandler = attachSentryErrorHandler; +exports.captureException = captureException; +/** + * Backend Sentry integration (TASK-444). + * + * Lazy-init: only requires + initializes @sentry/node when SENTRY_DSN is + * set, so the dep is optional. Without DSN, all helpers no-op. + * + * Mount the request handler before routes and the error handler last. + */ +let initialized = false; +let SentryRef = null; +function maybeInit() { + if (initialized) + return; + if (!process.env.SENTRY_DSN) { + initialized = true; + return; + } + try { + SentryRef = require('@sentry/node'); + SentryRef.init({ + dsn: process.env.SENTRY_DSN, + environment: process.env.NODE_ENV || 'development', + release: process.env.HERMES_BUILD_COMMIT, + tracesSampleRate: Number(process.env.SENTRY_TRACES_SAMPLE_RATE || '0.1'), + }); + console.log('[SENTRY] initialized'); + } + catch (err) { + console.warn(`[SENTRY] init skipped (missing @sentry/node?): ${err?.message || err}`); + } + initialized = true; +} +function attachSentryRequestHandler(app) { + maybeInit(); + if (!SentryRef) + return; + app.use(SentryRef.Handlers.requestHandler()); +} +function attachSentryErrorHandler(app) { + maybeInit(); + if (!SentryRef) + return; + app.use(SentryRef.Handlers.errorHandler()); +} +function captureException(err, ctx) { + maybeInit(); + if (!SentryRef) + return; + SentryRef.captureException(err, ctx ? { extra: ctx } : undefined); +} +//# sourceMappingURL=sentry.js.map \ No newline at end of file diff --git a/backend/dist/api/middleware/sentry.js.map b/backend/dist/api/middleware/sentry.js.map new file mode 100644 index 00000000..cac3d910 --- /dev/null +++ b/backend/dist/api/middleware/sentry.js.map @@ -0,0 +1 @@ +{"version":3,"file":"sentry.js","sourceRoot":"","sources":["../../../src/api/middleware/sentry.ts"],"names":[],"mappings":";;AAgCA,gEAIC;AAED,4DAIC;AAED,4CAIC;AA9CD;;;;;;;GAOG;AAEH,IAAI,WAAW,GAAG,KAAK,CAAC;AACxB,IAAI,SAAS,GAAQ,IAAI,CAAC;AAE1B,SAAS,SAAS;IAChB,IAAI,WAAW;QAAE,OAAO;IACxB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC;QAAC,WAAW,GAAG,IAAI,CAAC;QAAC,OAAO;IAAC,CAAC;IAC5D,IAAI,CAAC;QACH,SAAS,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;QACpC,SAAS,CAAC,IAAI,CAAC;YACb,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU;YAC3B,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,aAAa;YAClD,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB;YACxC,gBAAgB,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,yBAAyB,IAAI,KAAK,CAAC;SACzE,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;IACtC,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,OAAO,CAAC,IAAI,CAAC,kDAAkD,GAAG,EAAE,OAAO,IAAI,GAAG,EAAE,CAAC,CAAC;IACxF,CAAC;IACD,WAAW,GAAG,IAAI,CAAC;AACrB,CAAC;AAED,SAAgB,0BAA0B,CAAC,GAAY;IACrD,SAAS,EAAE,CAAC;IACZ,IAAI,CAAC,SAAS;QAAE,OAAO;IACvB,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,cAAc,EAAE,CAAC,CAAC;AAC/C,CAAC;AAED,SAAgB,wBAAwB,CAAC,GAAY;IACnD,SAAS,EAAE,CAAC;IACZ,IAAI,CAAC,SAAS;QAAE,OAAO;IACvB,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,YAAY,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED,SAAgB,gBAAgB,CAAC,GAAQ,EAAE,GAAyB;IAClE,SAAS,EAAE,CAAC;IACZ,IAAI,CAAC,SAAS;QAAE,OAAO;IACvB,SAAS,CAAC,gBAAgB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;AACpE,CAAC"} \ No newline at end of file diff --git a/backend/dist/api/newsletter.d.ts b/backend/dist/api/newsletter.d.ts new file mode 100644 index 00000000..c89ebf57 --- /dev/null +++ b/backend/dist/api/newsletter.d.ts @@ -0,0 +1,3 @@ +import { Router } from 'express'; +export declare function createNewsletterRouter(): Router; +//# sourceMappingURL=newsletter.d.ts.map \ No newline at end of file diff --git a/backend/dist/api/newsletter.d.ts.map b/backend/dist/api/newsletter.d.ts.map new file mode 100644 index 00000000..df149762 --- /dev/null +++ b/backend/dist/api/newsletter.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"newsletter.d.ts","sourceRoot":"","sources":["../../src/api/newsletter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAgBjC,wBAAgB,sBAAsB,IAAI,MAAM,CAqC/C"} \ No newline at end of file diff --git a/backend/dist/api/newsletter.js b/backend/dist/api/newsletter.js new file mode 100644 index 00000000..efd34f8f --- /dev/null +++ b/backend/dist/api/newsletter.js @@ -0,0 +1,49 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createNewsletterRouter = createNewsletterRouter; +const express_1 = require("express"); +const db_1 = require("../database/db"); +/** + * Newsletter signup (TASK-486). + * + * POST /api/newsletter — { email, source? } — store + ACK + * POST /api/newsletter/unsubscribe — { email } + * + * Storage only. No actual email send (forward to Mailchimp/Buttondown + * out-of-band via cron or webhook later). Email field validated as + * a basic shape; no MX check. + */ +const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +function createNewsletterRouter() { + const router = (0, express_1.Router)(); + router.post('/', async (req, res) => { + const email = String(req.body?.email || '').trim().toLowerCase(); + const source = String(req.body?.source || 'web').slice(0, 64); + if (!EMAIL_RE.test(email) || email.length > 320) { + return res.status(400).json({ error: 'invalid email' }); + } + try { + await db_1.db.query(`INSERT INTO newsletter_subscribers (email, source) + VALUES ($1, $2) + ON CONFLICT (email) DO UPDATE SET unsubscribed_at = NULL`, [email, source]); + res.json({ subscribed: true }); + } + catch (err) { + res.status(500).json({ error: err?.message || 'store failed' }); + } + }); + router.post('/unsubscribe', async (req, res) => { + const email = String(req.body?.email || '').trim().toLowerCase(); + if (!EMAIL_RE.test(email)) + return res.status(400).json({ error: 'invalid email' }); + try { + await db_1.db.query(`UPDATE newsletter_subscribers SET unsubscribed_at = CURRENT_TIMESTAMP WHERE email = $1`, [email]); + res.json({ unsubscribed: true }); + } + catch (err) { + res.status(500).json({ error: err?.message || 'update failed' }); + } + }); + return router; +} +//# sourceMappingURL=newsletter.js.map \ No newline at end of file diff --git a/backend/dist/api/newsletter.js.map b/backend/dist/api/newsletter.js.map new file mode 100644 index 00000000..bab06a26 --- /dev/null +++ b/backend/dist/api/newsletter.js.map @@ -0,0 +1 @@ +{"version":3,"file":"newsletter.js","sourceRoot":"","sources":["../../src/api/newsletter.ts"],"names":[],"mappings":";;AAgBA,wDAqCC;AArDD,qCAAiC;AACjC,uCAAoC;AAEpC;;;;;;;;;GASG;AAEH,MAAM,QAAQ,GAAG,4BAA4B,CAAC;AAE9C,SAAgB,sBAAsB;IACpC,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;IAExB,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QAClC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACjE,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,IAAI,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9D,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YAChD,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC;QAC1D,CAAC;QACD,IAAI,CAAC;YACH,MAAM,OAAE,CAAC,KAAK,CACZ;;kEAE0D,EAC1D,CAAC,KAAK,EAAE,MAAM,CAAC,CAChB,CAAC;YACF,GAAG,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QACjC,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,IAAI,cAAc,EAAE,CAAC,CAAC;QAClE,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QAC7C,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACjE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC;QACnF,IAAI,CAAC;YACH,MAAM,OAAE,CAAC,KAAK,CACZ,wFAAwF,EACxF,CAAC,KAAK,CAAC,CACR,CAAC;YACF,GAAG,CAAC,IAAI,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;QACnC,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,IAAI,eAAe,EAAE,CAAC,CAAC;QACnE,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"} \ No newline at end of file diff --git a/backend/dist/api/openapi.d.ts b/backend/dist/api/openapi.d.ts new file mode 100644 index 00000000..06252c3f --- /dev/null +++ b/backend/dist/api/openapi.d.ts @@ -0,0 +1,3 @@ +import { Router } from 'express'; +export declare function createOpenApiRouter(): Router; +//# sourceMappingURL=openapi.d.ts.map \ No newline at end of file diff --git a/backend/dist/api/openapi.d.ts.map b/backend/dist/api/openapi.d.ts.map new file mode 100644 index 00000000..fa5f9222 --- /dev/null +++ b/backend/dist/api/openapi.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../src/api/openapi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAgFjC,wBAAgB,mBAAmB,IAAI,MAAM,CAI5C"} \ No newline at end of file diff --git a/backend/dist/api/openapi.js b/backend/dist/api/openapi.js new file mode 100644 index 00000000..32c917fb --- /dev/null +++ b/backend/dist/api/openapi.js @@ -0,0 +1,87 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createOpenApiRouter = createOpenApiRouter; +const express_1 = require("express"); +/** + * OpenAPI 3.1 spec for /api/openapi.json (TASK-141). + * + * Hand-curated rather than auto-generated — express-route enumeration via + * `_router.stack` is fragile across versions. The cost is keeping this in + * sync; the benefit is a stable, reviewable schema. Swagger UI (TASK-142) + * mounts this spec at /docs. + */ +const SPEC = { + openapi: '3.1.0', + info: { + title: 'Hermeschain API', + version: '0.3.0', + description: 'Public REST API for Hermeschain.', + }, + servers: [{ url: 'https://hermeschain.io' }], + paths: { + '/api/status': { + get: { + summary: 'Snapshot of chain + service status', + responses: { '200': { description: 'OK' } }, + }, + }, + '/api/blocks/{height}': { + get: { + summary: 'Get a block by height', + parameters: [{ name: 'height', in: 'path', required: true, schema: { type: 'integer' } }, + { name: 'include', in: 'query', required: false, schema: { type: 'string', enum: ['receipts'] } }], + responses: { '200': { description: 'OK' }, '404': { description: 'Not found' } }, + }, + }, + '/api/blocks/search': { + get: { + summary: 'Search blocks by height range + producer (TASK-153)', + parameters: [ + { name: 'from', in: 'query', schema: { type: 'integer' } }, + { name: 'to', in: 'query', schema: { type: 'integer' } }, + { name: 'producer', in: 'query', schema: { type: 'string' } }, + ], + responses: { '200': { description: 'OK' } }, + }, + }, + '/api/tx/{hash}': { + get: { + summary: 'Get a transaction by hash (with optional decoded logs)', + parameters: [{ name: 'hash', in: 'path', required: true, schema: { type: 'string' } }, + { name: 'decodeLogs', in: 'query', schema: { type: 'boolean' } }], + responses: { '200': { description: 'OK' }, '404': { description: 'Not found' } }, + }, + }, + '/api/account/{addr}': { + get: { summary: 'Get account balance + nonce', responses: { '200': { description: 'OK' } } }, + }, + '/api/account/{addr}/next-nonce': { + get: { summary: 'Suggested next nonce (TASK-057)', responses: { '200': { description: 'OK' } } }, + }, + '/api/transactions': { + post: { summary: 'Submit a signed tx (TASK-170 idempotent on hash)', responses: { '200': { description: 'OK' }, '400': { description: 'Invalid' } } }, + }, + '/api/mempool': { get: { summary: 'Pending txs (TASK-166)', responses: { '200': { description: 'OK' } } } }, + '/api/mempool/{hash}': { get: { summary: 'Single pending tx (TASK-167)', responses: { '200': { description: 'OK' }, '404': { description: 'Not in mempool' } } } }, + '/api/chain/tps': { get: { summary: 'TPS over a window (TASK-051)', responses: { '200': { description: 'OK' } } } }, + '/api/wallet/validate/{input}': { get: { summary: 'Address validity (TASK-137)', responses: { '200': { description: 'OK' } } } }, + '/api/mesh/peers': { get: { summary: 'Active peer list', responses: { '200': { description: 'OK' } } } }, + '/api/mesh/head': { get: { summary: 'Our chain head', responses: { '200': { description: 'OK' } } } }, + '/api/mesh/headers': { get: { summary: 'Header range fetch (TASK-003)', responses: { '200': { description: 'OK' } } } }, + '/api/mesh/blocks': { get: { summary: 'Bulk block fetch (TASK-004)', responses: { '200': { description: 'OK' } } } }, + '/api/mesh/announce': { post: { summary: 'Register/heartbeat as a peer', responses: { '200': { description: 'OK' } } } }, + '/api/mesh/block': { post: { summary: 'Gossip a block (TASK-002)', responses: { '200': { description: 'Accepted' }, '400': { description: 'Bad payload' }, '409': { description: 'Rejected' } } } }, + '/api/metrics': { get: { summary: 'Prometheus exposition (TASK-152)', responses: { '200': { description: 'OK' } } } }, + '/api/build': { get: { summary: 'Build info (TASK-150)', responses: { '200': { description: 'OK' } } } }, + '/api/newsletter': { post: { summary: 'Newsletter subscribe (TASK-486)', responses: { '200': { description: 'OK' }, '400': { description: 'Bad email' } } } }, + '/health/live': { get: { summary: 'Process up (TASK-149)', responses: { '200': { description: 'live' }, '503': { description: 'draining' } } } }, + '/health/ready': { get: { summary: 'Ready to serve (TASK-149)', responses: { '200': { description: 'ready' }, '503': { description: 'failures' } } } }, + '/health/deep': { get: { summary: 'Deep read-path probe (TASK-149)', responses: { '200': { description: 'ready' }, '503': { description: 'failures' } } } }, + }, +}; +function createOpenApiRouter() { + const router = (0, express_1.Router)(); + router.get('/', (_req, res) => res.json(SPEC)); + return router; +} +//# sourceMappingURL=openapi.js.map \ No newline at end of file diff --git a/backend/dist/api/openapi.js.map b/backend/dist/api/openapi.js.map new file mode 100644 index 00000000..ab4d0f72 --- /dev/null +++ b/backend/dist/api/openapi.js.map @@ -0,0 +1 @@ +{"version":3,"file":"openapi.js","sourceRoot":"","sources":["../../src/api/openapi.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/backend/dist/api/server.js b/backend/dist/api/server.js index 64b27851..d396ae3c 100644 --- a/backend/dist/api/server.js +++ b/backend/dist/api/server.js @@ -84,6 +84,16 @@ async function main() { throw err; } console.log('[DB] PostgreSQL database ready\n'); + // Pre-warm Redis with hot reads (TASK-328). Off in dev unless + // explicitly enabled; on by default in production. + const warmEnabled = process.env.CACHE_WARMER_ENABLED ?? + (process.env.NODE_ENV === 'production' ? 'true' : 'false'); + if (warmEnabled === 'true') { + const { warmCache } = await Promise.resolve().then(() => __importStar(require('../database/cacheWarmer'))); + warmCache().catch((err) => { + console.warn('[CACHE WARMER] failed (non-fatal):', err?.message || err); + }); + } } else { console.log('[DB] Running without persistent database\n'); @@ -94,6 +104,12 @@ async function main() { console.log('Continuing with in-memory fallback...\n'); } const eventBus = EventBus_1.EventBus.getInstance(); + // Cross-replica event bridge (TASK-330). Off when REDIS_URL absent or + // explicitly disabled. Replicas converge on the same SSE stream. + if (process.env.REDIS_URL && process.env.REDIS_BRIDGE_ENABLED !== 'false') { + const { attachRedisBridge } = await Promise.resolve().then(() => __importStar(require('../events/RedisBridge'))); + attachRedisBridge(eventBus, process.env.REDIS_URL); + } const chain = new Chain_1.Chain(); const txPool = new TransactionPool_1.TransactionPool(); const validatorManager = new ValidatorManager_1.ValidatorManager(); @@ -111,8 +127,34 @@ async function main() { console.log(` Total Supply: ${StateManager_1.stateManager.formatBalance(StateManager_1.stateManager.getTotalSupply())}`); console.log(` Circulating: ${StateManager_1.stateManager.formatBalance(StateManager_1.stateManager.getCirculatingSupply())}\n`); const app = (0, express_1.default)(); - app.use((0, cors_1.default)()); - app.use(express_1.default.json()); + // CORS allowlist via env (TASK-145). Comma-separated origins; default open. + const corsOrigins = (process.env.CORS_ORIGINS || '').split(',').map((s) => s.trim()).filter(Boolean); + if (corsOrigins.length > 0) { + app.use((0, cors_1.default)({ + origin: (origin, cb) => { + if (!origin || corsOrigins.includes(origin)) + return cb(null, true); + cb(new Error(`origin ${origin} not allowed by CORS`)); + }, + credentials: true, + })); + console.log(`[CORS] allowlist active (${corsOrigins.length} origin(s))`); + } + else { + app.use((0, cors_1.default)()); + } + // Cap JSON body to 1MB (TASK-340) — DoS defense. + app.use(express_1.default.json({ limit: process.env.JSON_BODY_LIMIT || '1mb' })); + // HTTPS-only redirect (TASK-360) — production only. + const { httpsRedirect } = await Promise.resolve().then(() => __importStar(require('./middleware/httpsRedirect'))); + app.use(httpsRedirect); + // Request observability middleware (TASK-146 + TASK-147 + TASK-148). + // Order matters: requestId before accessLog so the log line has a value. + const { requestId } = await Promise.resolve().then(() => __importStar(require('./middleware/requestId'))); + const { accessLog } = await Promise.resolve().then(() => __importStar(require('./middleware/accessLog'))); + app.use(requestId); + if (process.env.ACCESS_LOG_ENABLED !== 'false') + app.use(accessLog); const syncSharedReadState = async () => { if (process.env.AGENT_ROLE === 'worker') return; @@ -128,6 +170,88 @@ async function main() { app.get('/health', (req, res) => { res.status(200).json({ status: 'ok' }); }); + // Contract bytecode disasm (TASK-099). Returns parsed JSON-op program + // for an on-chain contract. 404 if no code at the address. + app.get('/api/contract/:addr/disasm', async (req, res) => { + try { + const r = await db_1.db.query(`SELECT bytecode, code_hash, deployed_at_block, deployed_by + FROM contract_code WHERE address = $1`, [req.params.addr]); + if (r.rows.length === 0) + return res.status(404).json({ error: 'no code at address' }); + const row = r.rows[0]; + let ops = null; + try { + ops = JSON.parse(row.bytecode); + } + catch { /* leave null */ } + res.json({ + address: req.params.addr, + codeHash: row.code_hash, + deployedAtBlock: Number(row.deployed_at_block), + deployedBy: row.deployed_by, + ops, + bytecodeRaw: row.bytecode, + }); + } + catch (err) { + res.status(500).json({ error: err?.message || 'lookup failed' }); + } + }); + // Contract storage browser (TASK-100). + app.get('/api/contract/:addr/storage', async (req, res) => { + try { + const limit = Math.min(500, Math.max(1, Number(req.query.limit ?? 100))); + const prefix = typeof req.query.prefix === 'string' ? req.query.prefix : ''; + const r = await db_1.db.query(`SELECT storage_key, storage_value, updated_at_block + FROM contract_storage + WHERE contract_address = $1 AND storage_key LIKE $2 + ORDER BY storage_key + LIMIT $3`, [req.params.addr, prefix + '%', limit]); + res.json({ + address: req.params.addr, + items: r.rows.map((row) => ({ + key: row.storage_key, + value: row.storage_value, + updatedAtBlock: Number(row.updated_at_block), + })), + count: r.rows.length, + }); + } + catch (err) { + res.status(500).json({ error: err?.message || 'lookup failed' }); + } + }); + // Bulk chain export as NDJSON (TASK-028). Streamed line-by-line to + // avoid buffering huge ranges. Each line is { type: 'block', ...toJSON() }. + app.get('/api/chain/export', async (req, res) => { + const from = Math.max(0, Number(req.query.from ?? 0)); + const to = Math.min(from + 1000000, Number(req.query.to ?? from + 1000)); + res.setHeader('Content-Type', 'application/x-ndjson'); + for (let h = from; h <= to; h++) { + const block = chain.getBlockByHeight(h); + if (!block) + continue; + res.write(JSON.stringify({ type: 'block', ...block.toJSON() }) + '\n'); + } + res.end(); + }); + // Ethereum JSON-RPC compat (TASK-176). Minimum viable set so MetaMask + // and eth-tools can connect for read-only ops. + const { createJsonRpcRouter } = await Promise.resolve().then(() => __importStar(require('./jsonrpc'))); + app.use('/rpc', createJsonRpcRouter(chain)); + // OpenAPI 3.1 spec at /api/openapi.json (TASK-141). Swagger UI hookup in TASK-142. + const { createOpenApiRouter } = await Promise.resolve().then(() => __importStar(require('./openapi'))); + app.use('/api/openapi.json', createOpenApiRouter()); + // Newsletter signup (TASK-486). + const { createNewsletterRouter } = await Promise.resolve().then(() => __importStar(require('./newsletter'))); + app.use('/api/newsletter', createNewsletterRouter()); + // Three-tier health checks (TASK-149), build info (TASK-150), Prometheus metrics (TASK-152). + const { createHealthRouter } = await Promise.resolve().then(() => __importStar(require('./health'))); + const { createBuildRouter } = await Promise.resolve().then(() => __importStar(require('./build-info'))); + const { createMetricsRouter } = await Promise.resolve().then(() => __importStar(require('./metrics'))); + app.use('/health', createHealthRouter(chain)); + app.use('/api/build', createBuildRouter()); + app.use('/api/metrics', createMetricsRouter(chain, txPool)); // API status check (no key exposure) app.get('/api/config/status', (req, res) => { const role = process.env.AGENT_ROLE === 'worker' ? 'worker' : 'web'; @@ -338,10 +462,21 @@ async function main() { }); app.post('/api/transactions', async (req, res) => { try { - const { from, to, value, gasPrice, gasLimit, nonce, data, signature } = req.body; - // Generate Solana-style base58 transaction hash + const { from, to, value, gasPrice, gasLimit, nonce, data, signature, hash } = req.body; + // TASK-170 — idempotent submit. If client supplies a hash and we + // already have it (pending or mined), short-circuit with success. + if (hash) { + const pending = await txPool.getPendingTransactions(10000); + if (pending.find((t) => t.hash === hash)) { + return res.json({ success: true, hash, idempotent: 'pending' }); + } + // Note: can't easily check mined without a per-tx index; the new + // /api/tx/:hash endpoint will 404 for unknown so reusing this + // path is safe enough. + } + // Generate Solana-style base58 transaction hash if not supplied. const BASE58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; - const txHash = Array.from({ length: 44 }, () => BASE58[Math.floor(Math.random() * 58)]).join(''); + const txHash = hash || Array.from({ length: 44 }, () => BASE58[Math.floor(Math.random() * 58)]).join(''); const tx = { hash: txHash, from, @@ -666,6 +801,15 @@ Live context: await skillManager.initialize(); await agentTaskStore.initialize(); await agentRuntimeStore.initialize(); + const { githubUpdates } = await Promise.resolve().then(() => __importStar(require('../agent/GitHubUpdates'))); + await githubUpdates.initialize(agentConfig.repoRoot); + const shouldRunGitHubSync = agentConfig.role === 'worker' || !process.env.WORKER_INTERNAL_URL; + if (shouldRunGitHubSync) { + githubUpdates.startBackgroundSync(); + } + const { githubUpdatesRouter } = await Promise.resolve().then(() => __importStar(require('./githubUpdates'))); + app.use('/api/github/updates', githubUpdatesRouter); + console.log(`[GITHUB] Updates center ready (${shouldRunGitHubSync ? 'active sync' : 'cache reader'})`); if (agentConfig.role === 'worker') { await startNetworkHeartbeat(); } @@ -778,8 +922,11 @@ Live context: await syncSharedReadState(); const state = agentWorker.getState(); const sharedRuntime = agentRuntimeStore.getLatestSnapshot(); - const observedRuntime = agentConfig.role === 'web' && sharedRuntime + const sharedWorkerRuntime = agentConfig.role === 'web' && sharedRuntime?.role === 'worker' ? sharedRuntime + : null; + const observedRuntime = sharedWorkerRuntime + ? sharedWorkerRuntime : { role: agentConfig.role, mode: state.mode, @@ -814,7 +961,9 @@ Live context: updatedAt: new Date().toISOString(), workerHeartbeatAt: agentConfig.role === 'worker' ? new Date().toISOString() : null, }; - const workerHeartbeatAt = observedRuntime.workerHeartbeatAt || observedRuntime.updatedAt || null; + const workerHeartbeatAt = observedRuntime.role === 'worker' + ? observedRuntime.workerHeartbeatAt || observedRuntime.updatedAt || null + : null; const workerActive = workerHeartbeatAt ? Date.now() - new Date(workerHeartbeatAt).getTime() < 90000 : false; @@ -825,7 +974,7 @@ Live context: role: agentConfig.role, serviceRole: agentConfig.role, observedWorkerRole: observedRuntime.role, - statusSource: agentConfig.role === 'web' && sharedRuntime ? 'shared' : 'local', + statusSource: sharedWorkerRuntime ? 'shared' : 'local', mode: observedRuntime.mode, streamMode: observedRuntime.mode, isWorking: observedRuntime.isWorking, @@ -879,8 +1028,130 @@ Live context: tokenSpend: tokenBudget.snapshot(), }; }; + const WORKER_INTERNAL_URL = process.env.WORKER_INTERNAL_URL || + 'https://hermeschain-worker-production.up.railway.app'; + const mergeWorkerStatusIntoPayload = (localPayload, workerData) => ({ + ...localPayload, + mode: workerData.agentMode || localPayload.mode, + streamMode: workerData.agentMode || localPayload.streamMode, + runStatus: workerData.runStatus || localPayload.runStatus, + verificationStatus: workerData.verificationStatus || localPayload.verificationStatus, + isWorking: workerData.isWorking ?? localPayload.isWorking, + currentTask: workerData.currentTask || localPayload.currentTask, + lastFailure: workerData.lastFailure || localPayload.lastFailure, + blockedReason: workerData.blockedReason || localPayload.blockedReason, + recentTasks: (workerData.recentSuccessful || []).slice(0, 5).map((run) => ({ + title: run.title, + agent: run.agent || 'HERMES', + completedAt: run.completedAt, + })), + recentRuns: workerData.recentRuns || [], + completedTaskCount: (workerData.recentSuccessful || []).length, + agentEnabled: true, + capabilities: workerData.capabilities || localPayload.capabilities, + blockHeight: workerData.blockHeight ?? localPayload.blockHeight, + workerActive: true, + }); + const buildLiveAgentStatusPayload = async (options) => { + const localPayload = await buildAgentStatusPayload(); + if (process.env.AGENT_ROLE === 'worker') { + return localPayload; + } + const shouldProxyWorkerStatus = localPayload.statusSource !== 'shared' || + !localPayload.workerActive || + localPayload.mode === 'disabled'; + if (!shouldProxyWorkerStatus) { + return localPayload; + } + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 4000); + const upstream = await fetch(`${WORKER_INTERNAL_URL}/status`, { + signal: controller.signal, + }); + clearTimeout(timeout); + if (!upstream.ok) { + return localPayload; + } + const workerData = await upstream.json(); + return mergeWorkerStatusIntoPayload(localPayload, workerData); + } + catch (error) { + if (options?.logProxyErrors) { + console.error('[PROXY] worker /status fetch failed:', error?.message || error); + } + return localPayload; + } + }; + const getAgentLogCursor = (row) => ({ + id: row.id, + timestamp: new Date(row.timestamp), + }); + const compareAgentLogCursor = (a, b) => { + const delta = a.timestamp.getTime() - b.timestamp.getTime(); + if (delta !== 0) + return delta; + return a.id.localeCompare(b.id); + }; + const fetchRecentAgentStreamRows = async (limit = 30) => { + const result = await db_1.db.query(` + SELECT id, timestamp, type, content, metadata + FROM agent_logs + WHERE type IN ( + 'task_start', + 'analysis_start', + 'tool_use', + 'tool_result', + 'verification_start', + 'verification_result', + 'task_complete', + 'task_blocked', + 'git_commit', + 'error', + 'output' + ) + ORDER BY timestamp DESC, id DESC + LIMIT $1 + `, [limit]); + return (result.rows || []).reverse(); + }; + const fetchAgentStreamRowsAfter = async (cursor, limit = 50) => { + const result = await db_1.db.query(` + SELECT id, timestamp, type, content, metadata + FROM agent_logs + WHERE ( + type IN ( + 'task_start', + 'analysis_start', + 'tool_use', + 'tool_result', + 'verification_start', + 'verification_result', + 'task_complete', + 'task_blocked', + 'git_commit', + 'error', + 'output' + ) + ) + AND (timestamp > $1 OR (timestamp = $1 AND id > $2)) + ORDER BY timestamp ASC, id ASC + LIMIT $3 + `, [cursor.timestamp, cursor.id, limit]); + return result.rows || []; + }; // SSE endpoint for live agent work streaming app.get('/api/agent/stream', (req, res) => { + // SSE replica pinning (TASK-331). With multiple web replicas behind a + // load balancer, we don't get sticky sessions for free; instead, only + // the replica with SSE_REPLICA=true serves the stream. Others 503 with + // X-SSE-Failover so clients can retry against a different host. + if (process.env.SSE_REPLICA === 'false' || + (process.env.SSE_REPLICA === undefined && process.env.SSE_REPLICA_STRICT === 'true')) { + res.setHeader('X-SSE-Failover', 'true'); + res.status(503).json({ error: 'sse-replica-not-here' }); + return; + } // Set up SSE headers res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); @@ -889,13 +1160,38 @@ Live context: res.flushHeaders(); agentViewerCount++; console.log(`[AGENT] New viewer connected (total: ${agentViewerCount})`); - void buildAgentStatusPayload().then((payload) => { + const streamStartedAt = new Date(); + let lastSharedLogCursor = { + id: '', + timestamp: streamStartedAt, + }; + void buildLiveAgentStatusPayload() + .then(async (payload) => { res.write(`data: ${JSON.stringify({ type: 'init', data: payload, timestamp: Date.now() })}\n\n`); - }).catch(() => { + try { + const recentRows = await fetchRecentAgentStreamRows(30); + if (recentRows.length > 0) { + lastSharedLogCursor = getAgentLogCursor(recentRows[recentRows.length - 1]); + } + for (const row of recentRows) { + res.write(`data: ${JSON.stringify({ + type: 'text', + data: formatLogStreamText(row), + viewerCount: agentViewerCount, + timestamp: Date.now(), + })}\n\n`); + } + } + catch { + // Shared history is best-effort. The live stream should still + // connect even if historical hydration is unavailable. + } + }) + .catch(() => { res.write(`data: ${JSON.stringify({ type: 'init', data: { @@ -914,9 +1210,8 @@ Live context: } }; agentEvents.on('chunk', onChunk); - let lastLogSeenAt = new Date(Date.now() - 60 * 1000); const statusPulse = setInterval(() => { - void buildAgentStatusPayload() + void buildLiveAgentStatusPayload() .then((payload) => { res.write(`data: ${JSON.stringify({ type: 'status', @@ -933,15 +1228,12 @@ Live context: if (agentConfig.role !== 'web') return; try { - const result = await db_1.db.query(` - SELECT id, timestamp, type, content, metadata - FROM agent_logs - WHERE timestamp > $1 - ORDER BY timestamp ASC - LIMIT 50 - `, [lastLogSeenAt]); - for (const row of result.rows || []) { - lastLogSeenAt = new Date(row.timestamp); + const rows = await fetchAgentStreamRowsAfter(lastSharedLogCursor, 50); + for (const row of rows) { + const nextCursor = getAgentLogCursor(row); + if (compareAgentLogCursor(nextCursor, lastSharedLogCursor) > 0) { + lastSharedLogCursor = nextCursor; + } res.write(`data: ${JSON.stringify({ type: 'text', data: formatLogStreamText(row), @@ -980,51 +1272,8 @@ Live context: // Worker is reachable over Railway's private network at the internal domain. // Prefer explicit WORKER_INTERNAL_URL; fall back to the public Railway // domain since private-network DNS+port assumptions aren't guaranteed. - const WORKER_INTERNAL_URL = process.env.WORKER_INTERNAL_URL || - 'https://hermeschain-worker-production.up.railway.app'; app.get('/api/agent/status', async (req, res) => { - if (process.env.AGENT_ROLE !== 'worker') { - try { - const controller = new AbortController(); - const t = setTimeout(() => controller.abort(), 4000); - const upstream = await fetch(`${WORKER_INTERNAL_URL}/status`, { - signal: controller.signal, - }); - clearTimeout(t); - if (upstream.ok) { - const workerData = await upstream.json(); - // Merge worker runtime state into the web's own payload shape so - // frontend components keep working without changes. - const localPayload = await buildAgentStatusPayload(); - res.json({ - ...localPayload, - mode: workerData.agentMode || localPayload.mode, - streamMode: workerData.agentMode || localPayload.streamMode, - runStatus: workerData.runStatus || localPayload.runStatus, - verificationStatus: workerData.verificationStatus || localPayload.verificationStatus, - isWorking: workerData.isWorking ?? localPayload.isWorking, - currentTask: workerData.currentTask || localPayload.currentTask, - lastFailure: workerData.lastFailure || localPayload.lastFailure, - blockedReason: workerData.blockedReason || localPayload.blockedReason, - recentTasks: (workerData.recentSuccessful || []).slice(0, 5).map((r) => ({ - title: r.title, - agent: r.agent || 'HERMES', - completedAt: r.completedAt, - })), - recentRuns: workerData.recentRuns || [], - completedTaskCount: (workerData.recentSuccessful || []).length, - agentEnabled: true, - capabilities: workerData.capabilities || localPayload.capabilities, - blockHeight: workerData.blockHeight ?? localPayload.blockHeight, - }); - return; - } - } - catch (e) { - console.error('[PROXY] worker /status fetch failed:', e?.message || e); - } - } - res.json(await buildAgentStatusPayload()); + res.json(await buildLiveAgentStatusPayload({ logProxyErrors: true })); }); // Get persisted task runs (plus legacy completed-task alias) app.get('/api/agent/history', (req, res) => { @@ -1220,6 +1469,84 @@ Live context: avgBlockTime: stats.avgBlockTime }); }); + // Address validity checker (TASK-137). Returns parseable + reason. + app.get('/api/wallet/validate/:input', (req, res) => { + const input = req.params.input; + if (typeof input !== 'string' || input.length === 0) { + return res.json({ valid: false, reason: 'empty' }); + } + if (input.length < 32 || input.length > 64) { + return res.json({ valid: false, reason: 'wrong-length' }); + } + if (!/^[1-9A-HJ-NP-Za-km-z]+$/.test(input)) { + return res.json({ valid: false, reason: 'bad-base58' }); + } + res.json({ valid: true }); + }); + // Block search by height range (TASK-153). + app.get('/api/blocks/search', async (req, res) => { + const from = Math.max(0, Number(req.query.from ?? 0)); + const to = Math.min(from + 1000, Number(req.query.to ?? from + 100)); + const producer = typeof req.query.producer === 'string' ? req.query.producer : null; + const items = []; + for (let h = from; h <= to; h++) { + const block = chain.getBlockByHeight(h); + if (!block) + continue; + if (producer && block.header.producer !== producer) + continue; + items.push({ + height: block.header.height, + hash: block.header.hash, + producer: block.header.producer, + timestamp: block.header.timestamp, + transactionCount: block.transactions.length, + }); + } + res.json({ items, count: items.length, from, to }); + }); + // Mempool snapshot (TASK-166). + app.get('/api/mempool', async (req, res) => { + const limit = Math.min(500, Math.max(1, Number(req.query.limit ?? 200))); + const txs = await txPool.getPendingTransactions(limit); + res.json({ + pending: txs.length, + items: txs.map(tx => ({ + ...tx, + value: tx.value.toString(), + gasPrice: tx.gasPrice.toString(), + gasLimit: tx.gasLimit.toString(), + })), + }); + }); + // Single pending tx by hash (TASK-167). 404 if mined or unknown. + app.get('/api/mempool/:hash', async (req, res) => { + const all = await txPool.getPendingTransactions(10000); + const found = all.find(tx => tx.hash === req.params.hash); + if (!found) + return res.status(404).json({ error: 'not in mempool' }); + res.json({ + ...found, + value: found.value.toString(), + gasPrice: found.gasPrice.toString(), + gasLimit: found.gasLimit.toString(), + }); + }); + // Next nonce hint (TASK-057). max(chain_nonce, max_pending_nonce) + 1. + app.get('/api/account/:addr/next-nonce', async (req, res) => { + const chainNonce = StateManager_1.stateManager.getNonce(req.params.addr); + const pending = txPool.getPendingForAddress(req.params.addr); + const maxPending = pending.reduce((m, t) => Math.max(m, t.nonce), -1); + const next = Math.max(chainNonce, maxPending + 1); + res.json({ address: req.params.addr, nextNonce: next }); + }); + // TPS over a configurable window (TASK-051). Default 60s. Powered by + // chain.getRecentTps which already buckets recent block tx counts. + app.get('/api/chain/tps', async (req, res) => { + const window = Math.min(3600, Math.max(1, Number(req.query.window ?? 60))); + const tps = chain.getRecentTps(window); + res.json({ tps, window_sec: window }); + }); // Get latest block app.get('/api/chain/latest', async (req, res) => { await syncSharedReadState(); @@ -1417,10 +1744,19 @@ Live context: catch (e) { console.error('Error listing frontend files:', e); } - // Serve static files first (images, CSS, JS, etc.) + // Serve versioned static assets aggressively, but keep the HTML shell + // fresh so browsers pick up new bundles after deploys. app.use(express_1.default.static(frontendPath, { maxAge: '1y', - etag: true + etag: true, + index: false, + setHeaders: (res, filePath) => { + if (path_1.default.extname(filePath).toLowerCase() === '.html') { + res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, private'); + return; + } + res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); + }, })); // Catch-all handler: send back React's index.html file for client-side routing // But skip API routes and static file extensions @@ -1435,6 +1771,7 @@ Live context: if (hasStaticExtension) { return res.status(404).send('Static file not found'); } + res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, private'); res.sendFile(indexPath, (err) => { if (err) { console.error('Error serving index.html:', err); @@ -1482,6 +1819,7 @@ Live context: blockProducer.stop(); agentWorker.stop(); stopNetworkHeartbeat(); + githubUpdates.stopBackgroundSync(); process.exitCode = 1; setTimeout(() => process.exit(1), 0); }); @@ -1489,6 +1827,12 @@ Live context: console.log(`[SERVER] Running on http://localhost:${PORT}\n`); if (agentConfig.role === 'worker') { blockProducer.start(); + // Paced commit pusher — drains tier-3-backlog → main. + // No-ops without PACED_PUSH_ENABLED=true and GITHUB_TOKEN. + void Promise.resolve().then(() => __importStar(require('../agent/PacedPusher'))).then(({ PacedPusher }) => { + const pacer = new PacedPusher(agentConfig.repoRoot || process.cwd()); + pacer.start(); + }).catch((err) => console.warn('[PACER] failed to load:', err?.message || err)); } }); process.on('SIGINT', () => { @@ -1496,6 +1840,7 @@ Live context: blockProducer.stop(); agentWorker.stop(); stopNetworkHeartbeat(); + githubUpdates.stopBackgroundSync(); db_1.db.end(); process.exit(0); }); diff --git a/backend/dist/api/server.js.map b/backend/dist/api/server.js.map index 89860542..48642343 100644 --- a/backend/dist/api/server.js.map +++ b/backend/dist/api/server.js.map @@ -1 +1 @@ -{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/api/server.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/api/server.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/backend/dist/blockchain/Block.d.ts b/backend/dist/blockchain/Block.d.ts index 916bee15..56bea1fc 100644 --- a/backend/dist/blockchain/Block.d.ts +++ b/backend/dist/blockchain/Block.d.ts @@ -36,7 +36,7 @@ export declare class Block { private calculateStateRoot; setStateRoot(stateRoot: string): void; private calculateReceiptsRoot; - isValid(previousBlock?: Block): boolean; + isValid(previousBlock?: Block, now?: number): boolean; toJSON(): { gasUsed: string; gasLimit: string; @@ -62,6 +62,12 @@ export declare class Block { transactionsRoot: string; receiptsRoot: string; }; + /** + * Reconstruct a Block from a toJSON() payload (TASK-001). Round-trip + * property: `Block.fromJSON(b.toJSON()).header.hash === b.header.hash`. + * Throws on missing fields or hash mismatch (tampered header). + */ + static fromJSON(json: any): Block; } export { generateHash, generateRandomBase58, hexToBase58 }; //# sourceMappingURL=Block.d.ts.map \ No newline at end of file diff --git a/backend/dist/blockchain/Block.d.ts.map b/backend/dist/blockchain/Block.d.ts.map index 0f520610..4ae822be 100644 --- a/backend/dist/blockchain/Block.d.ts.map +++ b/backend/dist/blockchain/Block.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"Block.d.ts","sourceRoot":"","sources":["../../src/blockchain/Block.ts"],"names":[],"mappings":"AAMA,iBAAS,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAqBxC;AAGD,iBAAS,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG1C;AAGD,iBAAS,oBAAoB,CAAC,MAAM,GAAE,MAAW,GAAG,MAAM,CAGzD;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,KAAK;IACT,MAAM,EAAE,WAAW,CAAC;IACpB,YAAY,EAAE,WAAW,EAAE,CAAC;gBAGjC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,WAAW,EAAE,EAC3B,UAAU,GAAE,MAAU;IAyBxB,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,mBAAmB;IAe3B,OAAO,CAAC,kBAAkB;IAOnB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAM5C,OAAO,CAAC,qBAAqB;IAItB,OAAO,CAAC,aAAa,CAAC,EAAE,KAAK,GAAG,OAAO;IAoBvC,MAAM;;;;;;;kBAnHP,MAAM;kBACN,MAAM;gBACR,MAAM;mBAIH,MAAM;mBACN,MAAM;uBACF,MAAM;;gBAvBT,MAAM;cACR,MAAM;oBACA,MAAM;kBACR,MAAM;mBACL,MAAM;eACV,MAAM;oBACD,MAAM;mBAGP,MAAM;0BACC,MAAM;sBACV,MAAM;;CAoIrB;AAGD,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,WAAW,EAAE,CAAC"} \ No newline at end of file +{"version":3,"file":"Block.d.ts","sourceRoot":"","sources":["../../src/blockchain/Block.ts"],"names":[],"mappings":"AAMA,iBAAS,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAqBxC;AAGD,iBAAS,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG1C;AAGD,iBAAS,oBAAoB,CAAC,MAAM,GAAE,MAAW,GAAG,MAAM,CAGzD;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,KAAK;IACT,MAAM,EAAE,WAAW,CAAC;IACpB,YAAY,EAAE,WAAW,EAAE,CAAC;gBAGjC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,WAAW,EAAE,EAC3B,UAAU,GAAE,MAAU;IAyBxB,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,mBAAmB;IAe3B,OAAO,CAAC,kBAAkB;IAOnB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAM5C,OAAO,CAAC,qBAAqB;IAItB,OAAO,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,GAAG,GAAE,MAAmB,GAAG,OAAO;IA0BjE,MAAM;;;;;;;kBAzHP,MAAM;kBACN,MAAM;gBACR,MAAM;mBAIH,MAAM;mBACN,MAAM;uBACF,MAAM;;gBAvBT,MAAM;cACR,MAAM;oBACA,MAAM;kBACR,MAAM;mBACL,MAAM;eACV,MAAM;oBACD,MAAM;mBAGP,MAAM;0BACC,MAAM;sBACV,MAAM;;IA2IpB;;;;OAIG;WACW,QAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,KAAK;CAkDzC;AAGD,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,WAAW,EAAE,CAAC"} \ No newline at end of file diff --git a/backend/dist/blockchain/Block.js b/backend/dist/blockchain/Block.js index 7014ced0..0d84854a 100644 --- a/backend/dist/blockchain/Block.js +++ b/backend/dist/blockchain/Block.js @@ -105,7 +105,7 @@ class Block { calculateReceiptsRoot() { return generateRandomBase58(44); } - isValid(previousBlock) { + isValid(previousBlock, now = Date.now()) { if (this.header.hash !== this.calculateHash()) { return false; } @@ -115,7 +115,12 @@ class Block { if (previousBlock && this.header.height !== previousBlock.header.height + 1) { return false; } - if (previousBlock && this.header.timestamp <= previousBlock.header.timestamp) { + // TASK-015: reject blocks more than 30s in the future (clock-skew defense). + if (this.header.timestamp > now + 30000) { + return false; + } + // TASK-016: enforce a 2s minimum delta vs parent (no sub-second blocks). + if (previousBlock && this.header.timestamp - previousBlock.header.timestamp < 2000) { return false; } return true; @@ -133,6 +138,49 @@ class Block { })) }; } + /** + * Reconstruct a Block from a toJSON() payload (TASK-001). Round-trip + * property: `Block.fromJSON(b.toJSON()).header.hash === b.header.hash`. + * Throws on missing fields or hash mismatch (tampered header). + */ + static fromJSON(json) { + const required = [ + 'height', 'hash', 'parentHash', 'producer', 'timestamp', 'nonce', + 'difficulty', 'gasUsed', 'gasLimit', 'stateRoot', 'transactionsRoot', + 'receiptsRoot', 'transactions', + ]; + for (const field of required) { + if (!(field in json)) { + throw new Error(`fromJSON: missing field '${field}'`); + } + } + const txs = json.transactions.map((t) => ({ + hash: String(t.hash), + from: String(t.from), + to: String(t.to), + value: BigInt(t.value ?? '0'), + gasPrice: BigInt(t.gasPrice ?? '0'), + gasLimit: BigInt(t.gasLimit ?? '0'), + nonce: Number(t.nonce ?? 0), + data: t.data, + signature: String(t.signature ?? ''), + })); + const block = new Block(Number(json.height), String(json.parentHash), String(json.producer), txs, Number(json.difficulty ?? 1)); + // Override constructor-derived header fields with the deserialized truth. + block.header.timestamp = Number(json.timestamp); + block.header.nonce = Number(json.nonce ?? 0); + block.header.gasUsed = BigInt(json.gasUsed); + block.header.gasLimit = BigInt(json.gasLimit); + block.header.stateRoot = String(json.stateRoot); + block.header.transactionsRoot = String(json.transactionsRoot); + block.header.receiptsRoot = String(json.receiptsRoot); + // Recompute hash from the now-canonical fields and assert. + block.header.hash = block.calculateHash(); + if (block.header.hash !== String(json.hash)) { + throw new Error(`fromJSON: hash mismatch — header field tampered (got ${block.header.hash}, expected ${json.hash})`); + } + return block; + } } exports.Block = Block; //# sourceMappingURL=Block.js.map \ No newline at end of file diff --git a/backend/dist/blockchain/Block.js.map b/backend/dist/blockchain/Block.js.map index bbb72483..e72f940b 100644 --- a/backend/dist/blockchain/Block.js.map +++ b/backend/dist/blockchain/Block.js.map @@ -1 +1 @@ -{"version":3,"file":"Block.js","sourceRoot":"","sources":["../../src/blockchain/Block.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"Block.js","sourceRoot":"","sources":["../../src/blockchain/Block.ts"],"names":[],"mappings":";;;;;;AA0PS,oCAAY;AAAE,oDAAoB;AAAE,kCAAW;AA1PxD,oDAA4B;AAE5B,iDAAiD;AACjD,MAAM,eAAe,GAAG,4DAA4D,CAAC;AAErF,wBAAwB;AACxB,SAAS,WAAW,CAAC,GAAW;IAC9B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACtC,IAAI,GAAG,GAAG,MAAM,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC;IAC7B,IAAI,MAAM,GAAG,EAAE,CAAC;IAEhB,OAAO,GAAG,GAAG,EAAE,EAAE,CAAC;QAChB,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC;QACpC,MAAM,GAAG,eAAe,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC;QAC7C,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IAClB,CAAC;IAED,8CAA8C;IAC9C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;YACf,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC;QACxB,CAAC;aAAM,CAAC;YACN,MAAM;QACR,CAAC;IACH,CAAC;IAED,OAAO,MAAM,IAAI,GAAG,CAAC;AACvB,CAAC;AAED,6BAA6B;AAC7B,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,IAAI,GAAG,gBAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACpE,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC;AAED,gCAAgC;AAChC,SAAS,oBAAoB,CAAC,SAAiB,EAAE;IAC/C,MAAM,KAAK,GAAG,gBAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IACrC,OAAO,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AACjE,CAAC;AA6BD,MAAa,KAAK;IAIhB,YACE,MAAc,EACd,UAAkB,EAClB,QAAgB,EAChB,YAA2B,EAC3B,aAAqB,CAAC;QAEtB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QAEjC,MAAM,OAAO,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACxC,MAAM,gBAAgB,GAAG,IAAI,CAAC,mBAAmB,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;QAEnF,IAAI,CAAC,MAAM,GAAG;YACZ,MAAM;YACN,IAAI,EAAE,EAAE,EAAE,qBAAqB;YAC/B,UAAU;YACV,QAAQ;YACR,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,KAAK,EAAE,CAAC;YACR,UAAU;YACV,OAAO;YACP,QAAQ,EAAE,SAAS;YACnB,SAAS,EAAE,IAAI,CAAC,kBAAkB,EAAE;YACpC,gBAAgB;YAChB,YAAY,EAAE,IAAI,CAAC,qBAAqB,EAAE;SAC3C,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;IAC1C,CAAC;IAEO,aAAa;QACnB,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;YAC1B,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM;YAC1B,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,UAAU;YAClC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ;YAC9B,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS;YAChC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK;YACxB,gBAAgB,EAAE,IAAI,CAAC,MAAM,CAAC,gBAAgB;YAC9C,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS;SACjC,CAAC,CAAC;QAEH,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAEO,gBAAgB;QACtB,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACtE,CAAC;IAEO,mBAAmB,CAAC,MAAgB;QAC1C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,oBAAoB,CAAC,EAAE,CAAC,CAAC;QACzD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;QAE1C,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACvB,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC;YACzC,MAAM,QAAQ,GAAG,IAAI,GAAG,KAAK,CAAC;YAC9B,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;QACxC,CAAC;QAED,OAAO,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IAC5C,CAAC;IAEO,kBAAkB;QACxB,yDAAyD;QACzD,kDAAkD;QAClD,OAAO,oBAAoB,CAAC,EAAE,CAAC,CAAC;IAClC,CAAC;IAED,mCAAmC;IAC5B,YAAY,CAAC,SAAiB;QACnC,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,SAAS,CAAC;QAClC,6CAA6C;QAC7C,IAAI,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;IAC1C,CAAC;IAEO,qBAAqB;QAC3B,OAAO,oBAAoB,CAAC,EAAE,CAAC,CAAC;IAClC,CAAC;IAEM,OAAO,CAAC,aAAqB,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;QAC5D,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;YAC9C,OAAO,KAAK,CAAC;QACf,CAAC;QAED,IAAI,aAAa,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,KAAK,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YAC1E,OAAO,KAAK,CAAC;QACf,CAAC;QAED,IAAI,aAAa,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,aAAa,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5E,OAAO,KAAK,CAAC;QACf,CAAC;QAED,4EAA4E;QAC5E,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,GAAG,GAAG,KAAM,EAAE,CAAC;YACzC,OAAO,KAAK,CAAC;QACf,CAAC;QAED,yEAAyE;QACzE,IAAI,aAAa,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,aAAa,CAAC,MAAM,CAAC,SAAS,GAAG,IAAI,EAAE,CAAC;YACnF,OAAO,KAAK,CAAC;QACf,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAEM,MAAM;QACX,OAAO;YACL,GAAG,IAAI,CAAC,MAAM;YACd,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE;YACvC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE;YACzC,YAAY,EAAE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;gBACzC,GAAG,EAAE;gBACL,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE;gBAC1B,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE;gBAChC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE;aACjC,CAAC,CAAC;SACJ,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACI,MAAM,CAAC,QAAQ,CAAC,IAAS;QAC9B,MAAM,QAAQ,GAAG;YACf,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,OAAO;YAChE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,kBAAkB;YACpE,cAAc,EAAE,cAAc;SAC/B,CAAC;QACF,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC7B,IAAI,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,EAAE,CAAC;gBACrB,MAAM,IAAI,KAAK,CAAC,4BAA4B,KAAK,GAAG,CAAC,CAAC;YACxD,CAAC;QACH,CAAC;QAED,MAAM,GAAG,GAAmB,IAAI,CAAC,YAAsB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAClE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;YACpB,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;YACpB,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;YAChB,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC;YAC7B,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC;YACnC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,IAAI,GAAG,CAAC;YACnC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;YAC3B,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC;SACrC,CAAC,CAAC,CAAC;QAEJ,MAAM,KAAK,GAAG,IAAI,KAAK,CACrB,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EACnB,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,EACvB,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,EACrB,GAAG,EACH,MAAM,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC,CAC7B,CAAC;QAEF,0EAA0E;QAC1E,KAAK,CAAC,MAAM,CAAC,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAChD,KAAK,CAAC,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC;QAC7C,KAAK,CAAC,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5C,KAAK,CAAC,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC9C,KAAK,CAAC,MAAM,CAAC,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAChD,KAAK,CAAC,MAAM,CAAC,gBAAgB,GAAG,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QAC9D,KAAK,CAAC,MAAM,CAAC,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACtD,2DAA2D;QAC3D,KAAK,CAAC,MAAM,CAAC,IAAI,GAAI,KAAa,CAAC,aAAa,EAAE,CAAC;QAEnD,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5C,MAAM,IAAI,KAAK,CACb,wDAAwD,KAAK,CAAC,MAAM,CAAC,IAAI,cAAc,IAAI,CAAC,IAAI,GAAG,CACpG,CAAC;QACJ,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;CACF;AAnLD,sBAmLC"} \ No newline at end of file diff --git a/backend/dist/blockchain/BlockProducer.js b/backend/dist/blockchain/BlockProducer.js index e6eb3178..26f52108 100644 --- a/backend/dist/blockchain/BlockProducer.js +++ b/backend/dist/blockchain/BlockProducer.js @@ -10,7 +10,23 @@ const Interpreter_1 = require("../vm/Interpreter"); // Block limits const MAX_BLOCK_GAS = 30000000n; const MAX_TRANSACTIONS_PER_BLOCK = 500; -const BLOCK_REWARD = 10n * 10n ** 18n; // 10 OPEN per block +// Per-validator block reward (TASK-039) — env-tunable so testnet/mainnet +// can differ without a code change. Default: 10 OPEN. +const BLOCK_REWARD = (() => { + const raw = process.env.HERMES_BLOCK_REWARD_WEI; + if (!raw) + return 10n * 10n ** 18n; + try { + const parsed = BigInt(raw); + if (parsed < 0n) + throw new Error('negative reward'); + return parsed; + } + catch (err) { + console.warn(`[PRODUCER] HERMES_BLOCK_REWARD_WEI invalid (${raw}): ${err?.message || err} — using default 10 OPEN`); + return 10n * 10n ** 18n; + } +})(); class BlockProducer { constructor(chain, txPool, validatorManager, eventBus) { this.isProducing = false; @@ -57,17 +73,29 @@ class BlockProducer { console.log(`[PRODUCER] Difficulty: ${difficulty}`); // Get pending transactions with limits const pendingTxs = await this.txPool.getPendingTransactions(MAX_TRANSACTIONS_PER_BLOCK); - // Apply transactions to state with gas limit enforcement + // Apply transactions to state with gas + size limit enforcement const validTxs = []; const receipts = []; let totalGasUsed = 0n; let cumulativeGas = 0n; + let serializedBytes = 0; + const MAX_BLOCK_BYTES = 1048576; // TASK-022 — 1MB serialized cap for (const tx of pendingTxs) { // Check block gas limit if (totalGasUsed + tx.gasLimit > MAX_BLOCK_GAS) { console.log(`[PRODUCER] Block gas limit reached, stopping tx inclusion`); break; } + // Check block size limit (TASK-022) + const txBytes = Buffer.byteLength(JSON.stringify({ + ...tx, value: tx.value.toString(), gasPrice: tx.gasPrice.toString(), + gasLimit: tx.gasLimit.toString(), + }), 'utf8'); + if (serializedBytes + txBytes > MAX_BLOCK_BYTES) { + console.log(`[PRODUCER] Block size cap reached at ${serializedBytes} bytes`); + break; + } + serializedBytes += txBytes; // Verify signature before applying if (!(0, Crypto_1.verifyTransactionSignature)(tx)) { console.log(`[PRODUCER] Invalid signature for tx ${tx.hash.substring(0, 12)}...`); diff --git a/backend/dist/blockchain/BlockProducer.js.map b/backend/dist/blockchain/BlockProducer.js.map index 35b2557a..889101ed 100644 --- a/backend/dist/blockchain/BlockProducer.js.map +++ b/backend/dist/blockchain/BlockProducer.js.map @@ -1 +1 @@ -{"version":3,"file":"BlockProducer.js","sourceRoot":"","sources":["../../src/blockchain/BlockProducer.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"BlockProducer.js","sourceRoot":"","sources":["../../src/blockchain/BlockProducer.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/backend/dist/blockchain/TransactionPool.d.ts b/backend/dist/blockchain/TransactionPool.d.ts index 32cfb9ec..783a72bd 100644 --- a/backend/dist/blockchain/TransactionPool.d.ts +++ b/backend/dist/blockchain/TransactionPool.d.ts @@ -5,6 +5,7 @@ export interface ValidationResult { } export declare class TransactionPool { private pendingTransactions; + private addedAtMs; private knownHashes; private syncPendingTransactionsFromDb; initialize(): Promise; @@ -34,6 +35,7 @@ export declare class TransactionPool { private calculateTxHash; getPendingCount(): number; getPendingForAddress(address: string): Transaction[]; - clearExpired(maxAge?: number): number; + clearExpired(maxAgeMs?: number): number; + startExpirationLoop(intervalMs?: number, maxAgeMs?: number): NodeJS.Timeout; } //# sourceMappingURL=TransactionPool.d.ts.map \ No newline at end of file diff --git a/backend/dist/blockchain/TransactionPool.d.ts.map b/backend/dist/blockchain/TransactionPool.d.ts.map index e37cf61e..73fb740c 100644 --- a/backend/dist/blockchain/TransactionPool.d.ts.map +++ b/backend/dist/blockchain/TransactionPool.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"TransactionPool.d.ts","sourceRoot":"","sources":["../../src/blockchain/TransactionPool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAgB,MAAM,SAAS,CAAC;AAOpD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,qBAAa,eAAe;IAC1B,OAAO,CAAC,mBAAmB,CAAuC;IAClE,OAAO,CAAC,WAAW,CAA0B;YAE/B,6BAA6B;IA2BrC,UAAU;IAmBV,cAAc,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAwC1D,sBAAsB,CAAC,KAAK,GAAE,MAAY,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IASnE,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE;IAkBzC;;;;;;;OAOG;IACG,YAAY,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAmBtE;;;;;OAKG;IACG,eAAe,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;YAgB5D,mBAAmB;IAqEjC,OAAO,CAAC,eAAe;IAevB,eAAe,IAAI,MAAM;IAKzB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE;IAMpD,YAAY,CAAC,MAAM,GAAE,MAAgB,GAAG,MAAM;CAK/C"} \ No newline at end of file +{"version":3,"file":"TransactionPool.d.ts","sourceRoot":"","sources":["../../src/blockchain/TransactionPool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAgB,MAAM,SAAS,CAAC;AAOpD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,qBAAa,eAAe;IAC1B,OAAO,CAAC,mBAAmB,CAAuC;IAElE,OAAO,CAAC,SAAS,CAAkC;IACnD,OAAO,CAAC,WAAW,CAA0B;YAE/B,6BAA6B;IA4BrC,UAAU;IAmBV,cAAc,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAyC1D,sBAAsB,CAAC,KAAK,GAAE,MAAY,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IASnE,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE;IAmBzC;;;;;;;OAOG;IACG,YAAY,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAoBtE;;;;;OAKG;IACG,eAAe,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;YAgB5D,mBAAmB;IAqEjC,OAAO,CAAC,eAAe;IAevB,eAAe,IAAI,MAAM;IAKzB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE;IAMpD,YAAY,CAAC,QAAQ,GAAE,MAAkB,GAAG,MAAM;IAkBlD,mBAAmB,CAAC,UAAU,GAAE,MAAe,EAAE,QAAQ,GAAE,MAAkB,GAAG,MAAM,CAAC,OAAO;CAG/F"} \ No newline at end of file diff --git a/backend/dist/blockchain/TransactionPool.js b/backend/dist/blockchain/TransactionPool.js index 111a47b5..bc87d188 100644 --- a/backend/dist/blockchain/TransactionPool.js +++ b/backend/dist/blockchain/TransactionPool.js @@ -12,6 +12,8 @@ const MIN_GAS_PRICE = 1n; // Minimum gas price (lamports) class TransactionPool { constructor() { this.pendingTransactions = new Map(); + // Per-tx admit timestamp for TTL eviction (TASK-021). + this.addedAtMs = new Map(); this.knownHashes = new Set(); // Replay protection } async syncPendingTransactionsFromDb() { @@ -33,6 +35,7 @@ class TransactionPool { signature: row.signature, }; this.pendingTransactions.set(tx.hash, tx); + this.addedAtMs.set(tx.hash, Date.now()); this.knownHashes.add(tx.hash); } } @@ -65,6 +68,7 @@ class TransactionPool { return validation; } this.pendingTransactions.set(tx.hash, tx); + this.addedAtMs.set(tx.hash, Date.now()); this.knownHashes.add(tx.hash); try { await db_1.db.query(` @@ -102,6 +106,7 @@ class TransactionPool { async removeTransactions(hashes) { for (const hash of hashes) { this.pendingTransactions.delete(hash); + this.addedAtMs.delete(hash); } if (hashes.length > 0) { try { @@ -134,6 +139,7 @@ class TransactionPool { const validation = await this.validateTransaction(tx); if (!validation.valid) { this.pendingTransactions.delete(hash); + this.addedAtMs.delete(hash); this.knownHashes.delete(hash); dropped.push({ hash, reason: validation.error || 'invalid' }); } @@ -241,11 +247,26 @@ class TransactionPool { return Array.from(this.pendingTransactions.values()) .filter(tx => tx.from === address || tx.to === address); } - // Clear expired transactions (optional TTL) - clearExpired(maxAge = 3600000) { - // This would require storing timestamp with each tx - // For now, just return 0 - return 0; + // Clear expired transactions (TASK-021). Default TTL 1h. + clearExpired(maxAgeMs = 3600000) { + const cutoff = Date.now() - maxAgeMs; + let removed = 0; + for (const [hash, addedAt] of this.addedAtMs.entries()) { + if (addedAt < cutoff) { + this.pendingTransactions.delete(hash); + this.addedAtMs.delete(hash); + removed++; + } + } + if (removed > 0) { + console.log(`[POOL] cleared ${removed} expired tx(s) older than ${maxAgeMs}ms`); + } + return removed; + } + // Start a periodic eviction loop (TASK-021). Returns the interval handle + // so callers can stop it on shutdown. + startExpirationLoop(intervalMs = 60000, maxAgeMs = 3600000) { + return setInterval(() => this.clearExpired(maxAgeMs), intervalMs); } } exports.TransactionPool = TransactionPool; diff --git a/backend/dist/blockchain/TransactionPool.js.map b/backend/dist/blockchain/TransactionPool.js.map index fdcd4dcc..1afded0c 100644 --- a/backend/dist/blockchain/TransactionPool.js.map +++ b/backend/dist/blockchain/TransactionPool.js.map @@ -1 +1 @@ -{"version":3,"file":"TransactionPool.js","sourceRoot":"","sources":["../../src/blockchain/TransactionPool.ts"],"names":[],"mappings":";;;AACA,uCAAoC;AACpC,qCAAoE;AACpE,iDAA8C;AAC9C,iDAA8C;AAQ9C,aAAa;AACb,MAAM,aAAa,GAAG,MAAM,CAAC,CAAE,6BAA6B;AAC5D,MAAM,aAAa,GAAG,SAAS,CAAC,CAAE,kBAAkB;AACpD,MAAM,aAAa,GAAG,EAAE,CAAC,CAAE,+BAA+B;AAE1D,MAAa,eAAe;IAA5B;QACU,wBAAmB,GAA6B,IAAI,GAAG,EAAE,CAAC;QAC1D,gBAAW,GAAgB,IAAI,GAAG,EAAE,CAAC,CAAE,oBAAoB;IAwQrE,CAAC;IAtQS,KAAK,CAAC,6BAA6B;QACzC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAE,CAAC,KAAK,CAAC;;OAE7B,CAAC,CAAC;YAEH,IAAI,CAAC,mBAAmB,CAAC,KAAK,EAAE,CAAC;YACjC,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;gBAC9B,MAAM,EAAE,GAAgB;oBACtB,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,IAAI,EAAE,GAAG,CAAC,YAAY;oBACtB,EAAE,EAAE,GAAG,CAAC,UAAU;oBAClB,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC;oBACxB,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;oBAC/B,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;oBAC/B,KAAK,EAAE,GAAG,CAAC,KAAK;oBAChB,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,SAAS,EAAE,GAAG,CAAC,SAAS;iBACzB,CAAC;gBACF,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;gBAC1C,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;YAChC,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,wCAAwC,EAAE,KAAK,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;IAED,KAAK,CAAC,UAAU;QACd,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,6BAA6B,EAAE,CAAC;YAE3C,+DAA+D;YAC/D,MAAM,eAAe,GAAG,MAAM,OAAE,CAAC,KAAK,CAAC;;OAEtC,CAAC,CAAC;YACH,KAAK,MAAM,GAAG,IAAI,eAAe,CAAC,IAAI,EAAE,CAAC;gBACvC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACjC,CAAC;YAED,OAAO,CAAC,GAAG,CAAC,4CAA4C,IAAI,CAAC,mBAAmB,CAAC,IAAI,uBAAuB,CAAC,CAAC;YAC9G,OAAO,CAAC,GAAG,CAAC,oCAAoC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC;QAC3E,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE,KAAK,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,EAAe;QAClC,uBAAuB;QACvB,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAC;QACtD,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;YACtB,OAAO,CAAC,GAAG,CAAC,gCAAgC,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC;YAChE,OAAO,UAAU,CAAC;QACpB,CAAC;QAED,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC1C,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QAE9B,IAAI,CAAC;YACH,MAAM,OAAE,CAAC,KAAK,CAAC;;;;;;OAMd,EAAE;gBACD,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE;gBAC5C,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE;gBAC9C,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC,SAAS;aACxC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,KAAK,CAAC,CAAC;QACjD,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,sBAAsB,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,2BAAY,CAAC,aAAa,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAEjH,aAAa;QACb,mBAAQ,CAAC,IAAI,CAAC,mBAAmB,EAAE;YACjC,IAAI,EAAE,EAAE,CAAC,IAAI;YACb,IAAI,EAAE,EAAE,CAAC,IAAI;YACb,EAAE,EAAE,EAAE,CAAC,EAAE;YACT,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE;SAC3B,CAAC,CAAC;QAEH,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,sBAAsB,CAAC,QAAgB,GAAG;QAC9C,MAAM,IAAI,CAAC,6BAA6B,EAAE,CAAC;QAC3C,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,MAAM,EAAE,CAAC;aACzD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;aAC/C,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QAEnB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,MAAgB;QACvC,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;YAC1B,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACxC,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC;gBACH,MAAM,OAAE,CAAC,KAAK,CAAC;;;;SAId,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;YACf,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,CAAC,KAAK,CAAC,2CAA2C,EAAE,KAAK,CAAC,CAAC;YACpE,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,YAAY;QAChB,MAAM,OAAO,GAA4C,EAAE,CAAC;QAC5D,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,IAAI,EAAE,CAAC,CAAC;QAC3D,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;YAC1B,MAAM,EAAE,GAAG,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC9C,IAAI,CAAC,EAAE;gBAAE,SAAS;YAClB,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAC;YACtD,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;gBACtB,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;gBACtC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;gBAC9B,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,KAAK,IAAI,SAAS,EAAE,CAAC,CAAC;YAChE,CAAC;QACH,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,OAAO,CAAC,GAAG,CAAC,kBAAkB,OAAO,CAAC,MAAM,mBAAmB,CAAC,CAAC;QACnE,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,eAAe,CAAC,QAAgC;QACpD,MAAM,UAAU,GAAa,EAAE,CAAC;QAChC,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;YAC1B,kEAAkE;YAClE,gCAAgC;YAChC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;YACjC,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;YACzC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;YAC7C,IAAI,MAAM,CAAC,KAAK;gBAAE,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QAC7C,CAAC;QACD,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1B,OAAO,CAAC,GAAG,CAAC,sBAAsB,UAAU,CAAC,MAAM,iBAAiB,CAAC,CAAC;QACxE,CAAC;QACD,OAAO,UAAU,CAAC;IACpB,CAAC;IAEO,KAAK,CAAC,mBAAmB,CAAC,EAAe;QAC/C,yBAAyB;QACzB,IAAI,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YACnC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC;QAC5D,CAAC;QAED,IAAI,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YACtB,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC;QACxD,CAAC;QAED,IAAI,EAAE,CAAC,KAAK,GAAG,EAAE,EAAE,CAAC;YAClB,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;QACnD,CAAC;QAED,iBAAiB;QACjB,IAAI,EAAE,CAAC,QAAQ,GAAG,aAAa,EAAE,CAAC;YAChC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,2BAA2B,aAAa,GAAG,EAAE,CAAC;QAC9E,CAAC;QAED,IAAI,EAAE,CAAC,QAAQ,GAAG,aAAa,EAAE,CAAC;YAChC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,4BAA4B,aAAa,GAAG,EAAE,CAAC;QAC/E,CAAC;QAED,IAAI,EAAE,CAAC,QAAQ,GAAG,aAAa,EAAE,CAAC;YAChC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,2BAA2B,aAAa,GAAG,EAAE,CAAC;QAC9E,CAAC;QAED,+CAA+C;QAC/C,IAAI,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YAClC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,oCAAoC,EAAE,CAAC;QACvE,CAAC;QAED,wBAAwB;QACxB,MAAM,cAAc,GAAG,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC;QAChD,IAAI,EAAE,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;YAC/B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC;QAC7D,CAAC;QAED,6BAA6B;QAC7B,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC;YAClB,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;QACtD,CAAC;QAED,MAAM,cAAc,GAAG,IAAA,mCAA0B,EAAC,EAAE,CAAC,CAAC;QACtD,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;QACtD,CAAC;QAED,sCAAsC;QACtC,MAAM,aAAa,GAAG,2BAAY,CAAC,UAAU,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QACvD,MAAM,SAAS,GAAG,EAAE,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC,QAAQ,GAAG,EAAE,CAAC,QAAQ,CAAC,CAAC;QACzD,IAAI,aAAa,GAAG,SAAS,EAAE,CAAC;YAC9B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,6BAA6B,aAAa,WAAW,SAAS,EAAE,EAAE,CAAC;QACnG,CAAC;QAED,cAAc;QACd,MAAM,aAAa,GAAG,2BAAY,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QACrD,IAAI,EAAE,CAAC,KAAK,GAAG,aAAa,EAAE,CAAC;YAC7B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,2BAA2B,aAAa,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC;QAC9F,CAAC;QAED,sDAAsD;QACtD,IAAI,EAAE,CAAC,KAAK,GAAG,aAAa,GAAG,EAAE,EAAE,CAAC;YAClC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,4BAA4B,aAAa,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC;QAC/F,CAAC;QAED,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACzB,CAAC;IAEO,eAAe,CAAC,EAAe;QACrC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;YAC1B,IAAI,EAAE,EAAE,CAAC,IAAI;YACb,EAAE,EAAE,EAAE,CAAC,EAAE;YACT,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE;YAC1B,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE;YAChC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE;YAChC,KAAK,EAAE,EAAE,CAAC,KAAK;YACf,IAAI,EAAE,EAAE,CAAC,IAAI;YACb,SAAS,EAAE,EAAE,CAAC,SAAS;SACxB,CAAC,CAAC;QAEH,OAAO,IAAA,qBAAY,EAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED,eAAe;QACb,OAAO,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC;IACvC,CAAC;IAED,0CAA0C;IAC1C,oBAAoB,CAAC,OAAe;QAClC,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,MAAM,EAAE,CAAC;aACjD,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,KAAK,OAAO,IAAI,EAAE,CAAC,EAAE,KAAK,OAAO,CAAC,CAAC;IAC5D,CAAC;IAED,4CAA4C;IAC5C,YAAY,CAAC,SAAiB,OAAO;QACnC,oDAAoD;QACpD,yBAAyB;QACzB,OAAO,CAAC,CAAC;IACX,CAAC;CACF;AA1QD,0CA0QC"} \ No newline at end of file +{"version":3,"file":"TransactionPool.js","sourceRoot":"","sources":["../../src/blockchain/TransactionPool.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/backend/dist/database/cacheWarmer.d.ts b/backend/dist/database/cacheWarmer.d.ts new file mode 100644 index 00000000..0ee03e8f --- /dev/null +++ b/backend/dist/database/cacheWarmer.d.ts @@ -0,0 +1,13 @@ +/** + * Cache warmer (TASK-328). + * + * Cold boot leaves Redis empty; the first ~30s of traffic causes burst + * PG reads as the cache populates. Pre-warm with the things chainState + * is going to read anyway: latest 100 blocks, top 50 accounts by balance, + * last block height. Runs once at boot when CACHE_WARMER_ENABLED=true. + */ +export declare function warmCache(): Promise<{ + entries: number; + durationMs: number; +}>; +//# sourceMappingURL=cacheWarmer.d.ts.map \ No newline at end of file diff --git a/backend/dist/database/cacheWarmer.d.ts.map b/backend/dist/database/cacheWarmer.d.ts.map new file mode 100644 index 00000000..40d770a0 --- /dev/null +++ b/backend/dist/database/cacheWarmer.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"cacheWarmer.d.ts","sourceRoot":"","sources":["../../src/database/cacheWarmer.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,wBAAsB,SAAS,IAAI,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAsClF"} \ No newline at end of file diff --git a/backend/dist/database/cacheWarmer.js b/backend/dist/database/cacheWarmer.js new file mode 100644 index 00000000..cf4a57dd --- /dev/null +++ b/backend/dist/database/cacheWarmer.js @@ -0,0 +1,43 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.warmCache = warmCache; +const db_1 = require("./db"); +/** + * Cache warmer (TASK-328). + * + * Cold boot leaves Redis empty; the first ~30s of traffic causes burst + * PG reads as the cache populates. Pre-warm with the things chainState + * is going to read anyway: latest 100 blocks, top 50 accounts by balance, + * last block height. Runs once at boot when CACHE_WARMER_ENABLED=true. + */ +async function warmCache() { + const startedAt = Date.now(); + let entries = 0; + try { + // Latest 100 blocks → block:height:N cache. + const blocksRes = await db_1.db.query(`SELECT * FROM blocks ORDER BY height DESC LIMIT 100`); + for (const block of blocksRes.rows) { + await db_1.cache.setJSON(`block:height:${block.height}`, block, 300); + entries++; + } + // Top 50 accounts by balance. + const topRes = await db_1.db.query(`SELECT address, balance, nonce FROM accounts + ORDER BY balance::numeric DESC LIMIT 50`); + await db_1.cache.setJSON('top_accounts:50', topRes.rows, 60); + entries++; + // Last block height (already maintained by chainState but make sure it's there). + const headRes = await db_1.db.query(`SELECT MAX(height) AS max_height FROM blocks`); + const head = Number(headRes.rows[0]?.max_height ?? 0); + if (head > 0) { + await db_1.chainState.saveBlockHeight(head); + entries++; + } + } + catch (err) { + console.warn(`[CACHE WARMER] partial failure: ${err?.message || err}`); + } + const durationMs = Date.now() - startedAt; + console.log(`[CACHE WARMER] ${entries} entries warmed in ${durationMs}ms`); + return { entries, durationMs }; +} +//# sourceMappingURL=cacheWarmer.js.map \ No newline at end of file diff --git a/backend/dist/database/cacheWarmer.js.map b/backend/dist/database/cacheWarmer.js.map new file mode 100644 index 00000000..07b72ddc --- /dev/null +++ b/backend/dist/database/cacheWarmer.js.map @@ -0,0 +1 @@ +{"version":3,"file":"cacheWarmer.js","sourceRoot":"","sources":["../../src/database/cacheWarmer.ts"],"names":[],"mappings":";;AAWA,8BAsCC;AAjDD,6BAA6C;AAE7C;;;;;;;GAOG;AAEI,KAAK,UAAU,SAAS;IAC7B,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,IAAI,CAAC;QACH,4CAA4C;QAC5C,MAAM,SAAS,GAAG,MAAM,OAAE,CAAC,KAAK,CAC9B,qDAAqD,CACtD,CAAC;QACF,KAAK,MAAM,KAAK,IAAI,SAAS,CAAC,IAAI,EAAE,CAAC;YACnC,MAAM,UAAK,CAAC,OAAO,CAAC,gBAAgB,KAAK,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;YAChE,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,8BAA8B;QAC9B,MAAM,MAAM,GAAG,MAAM,OAAE,CAAC,KAAK,CAC3B;gDAC0C,CAC3C,CAAC;QACF,MAAM,UAAK,CAAC,OAAO,CAAC,iBAAiB,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACxD,OAAO,EAAE,CAAC;QAEV,iFAAiF;QACjF,MAAM,OAAO,GAAG,MAAM,OAAE,CAAC,KAAK,CAC5B,8CAA8C,CAC/C,CAAC;QACF,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,UAAU,IAAI,CAAC,CAAC,CAAC;QACtD,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;YACb,MAAM,eAAU,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;YACvC,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,OAAO,CAAC,IAAI,CAAC,mCAAmC,GAAG,EAAE,OAAO,IAAI,GAAG,EAAE,CAAC,CAAC;IACzE,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;IAC1C,OAAO,CAAC,GAAG,CAAC,kBAAkB,OAAO,sBAAsB,UAAU,IAAI,CAAC,CAAC;IAC3E,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;AACjC,CAAC"} \ No newline at end of file diff --git a/backend/dist/database/db.d.ts b/backend/dist/database/db.d.ts index a70775a1..c1501bac 100644 --- a/backend/dist/database/db.d.ts +++ b/backend/dist/database/db.d.ts @@ -4,6 +4,12 @@ export declare const db: { rows: any[]; rowCount?: number; }>; + poolStats: () => { + total: number; + idle: number; + waiting: number; + max: number; + }; connect: () => Promise; end: () => Promise; }; diff --git a/backend/dist/database/db.d.ts.map b/backend/dist/database/db.d.ts.map index 25e1204e..e1bccd6a 100644 --- a/backend/dist/database/db.d.ts.map +++ b/backend/dist/database/db.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/database/db.ts"],"names":[],"mappings":"AA2DA,eAAO,MAAM,EAAE;gBAEK,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;kBAmBpB,MAAM,WAAU,GAAG,EAAE,KAAQ,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,EAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;mBAgBzE,OAAO,CAAC,OAAO,CAAC;eAmBpB,OAAO,CAAC,IAAI,CAAC;CAI7B,CAAC;AAGF,eAAO,MAAM,KAAK;eAEC,MAAM,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;eAW/B,MAAM,SAAS,MAAM,QAAQ,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;eAcnD,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;cAUvB,CAAC,OAAO,MAAM,KAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;mBAW7B,MAAM,SAAS,GAAG,QAAQ,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;gBAKnD,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;gBAWxB,MAAM,SAAS,MAAM,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;gBAW9C,MAAM,SAAS,MAAM,SAAS,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;mBAUjD,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;uBAWnD,OAAO;CAGzB,CAAC;AAGF,eAAO,MAAM,UAAU;8BAEW,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;0BAK5B,OAAO,CAAC,MAAM,CAAC;oCAMH,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;6BAK/B,OAAO,CAAC,MAAM,CAAC;mCAMP,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;gCAK3B,OAAO,CAAC,MAAM,CAAC;gCAMf,OAAO,CAAC,MAAM,CAAC;uBAKtB,GAAG,KAAG,OAAO,CAAC,IAAI,CAAC;uBAMnB,MAAM,KAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;iCAKlB,MAAM,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAInE,CAAC"} \ No newline at end of file +{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/database/db.ts"],"names":[],"mappings":"AAoEA,eAAO,MAAM,EAAE;gBAEK,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;kBA0BpB,MAAM,WAAU,GAAG,EAAE,KAAQ,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,EAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;qBA0B7E;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;mBAWzD,OAAO,CAAC,OAAO,CAAC;eAmBpB,OAAO,CAAC,IAAI,CAAC;CAI7B,CAAC;AAGF,eAAO,MAAM,KAAK;eAEC,MAAM,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;eAW/B,MAAM,SAAS,MAAM,QAAQ,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;eAcnD,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;cAUvB,CAAC,OAAO,MAAM,KAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;mBAW7B,MAAM,SAAS,GAAG,QAAQ,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;gBAKnD,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC;gBAWxB,MAAM,SAAS,MAAM,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;gBAW9C,MAAM,SAAS,MAAM,SAAS,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;mBAUjD,MAAM,KAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;uBAWnD,OAAO;CAGzB,CAAC;AAGF,eAAO,MAAM,UAAU;8BAEW,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;0BAK5B,OAAO,CAAC,MAAM,CAAC;oCAMH,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;6BAK/B,OAAO,CAAC,MAAM,CAAC;mCAMP,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;gCAK3B,OAAO,CAAC,MAAM,CAAC;gCAMf,OAAO,CAAC,MAAM,CAAC;uBAKtB,GAAG,KAAG,OAAO,CAAC,IAAI,CAAC;uBAMnB,MAAM,KAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;iCAKlB,MAAM,KAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAInE,CAAC"} \ No newline at end of file diff --git a/backend/dist/database/db.js b/backend/dist/database/db.js index 673174bc..3ce9d028 100644 --- a/backend/dist/database/db.js +++ b/backend/dist/database/db.js @@ -40,21 +40,28 @@ exports.chainState = exports.cache = exports.db = void 0; const pg_1 = require("pg"); const ioredis_1 = __importDefault(require("ioredis")); const dotenv = __importStar(require("dotenv")); +const queryMetrics_1 = require("./queryMetrics"); dotenv.config(); +// Slow-query threshold (TASK-321). Queries above this log a [PG SLOW] warning. +const PG_SLOW_QUERY_MS = Math.max(1, Number(process.env.PG_SLOW_QUERY_MS || '1000')); // PostgreSQL connection - uses Railway's DATABASE_URL const DATABASE_URL = process.env.DATABASE_URL; let pool = null; let redis = null; -// Initialize PostgreSQL +// Initialize PostgreSQL — tunable via env so dev (small) and prod (large) +// can size differently without a code change. +const POOL_MAX = Math.max(1, Number(process.env.PG_POOL_MAX || '20')); +const POOL_IDLE_MS = Math.max(1000, Number(process.env.PG_POOL_IDLE_MS || '30000')); +const POOL_CONNECT_MS = Math.max(500, Number(process.env.PG_POOL_CONNECT_MS || '2000')); if (DATABASE_URL) { pool = new pg_1.Pool({ connectionString: DATABASE_URL, ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, - max: 20, - idleTimeoutMillis: 30000, - connectionTimeoutMillis: 2000, + max: POOL_MAX, + idleTimeoutMillis: POOL_IDLE_MS, + connectionTimeoutMillis: POOL_CONNECT_MS, }); - console.log('[DB] PostgreSQL pool created'); + console.log(`[DB] PostgreSQL pool created (max=${POOL_MAX}, idle=${POOL_IDLE_MS}ms, connect=${POOL_CONNECT_MS}ms)`); } else { console.warn('[DB] DATABASE_URL not set - using in-memory fallback'); @@ -95,14 +102,21 @@ exports.db = { console.log('Using in-memory storage (no DATABASE_URL)'); return; } + const startedAt = Date.now(); try { // Split by semicolon and execute each statement const statements = sql.split(';').filter(s => s.trim().length > 0); for (const statement of statements) { await pool.query(statement); } + const duration = Date.now() - startedAt; + (0, queryMetrics_1.recordQuery)(duration); + if (duration > PG_SLOW_QUERY_MS) { + console.warn(`[PG SLOW] exec ${duration}ms (${sql.length} chars, ${sql.split(';').length} stmts)`); + } } catch (error) { + (0, queryMetrics_1.recordQueryError)(); console.error('Database exec error:', error); throw error; } @@ -113,15 +127,36 @@ exports.db = { // Fallback to memory return { rows: [], rowCount: 0 }; } + const startedAt = Date.now(); try { const result = await pool.query(text, params); + const duration = Date.now() - startedAt; + (0, queryMetrics_1.recordQuery)(duration); + if (duration > PG_SLOW_QUERY_MS) { + // Truncate SQL to 200 chars; never log param VALUES (PII), only count. + const sqlPreview = text.length > 200 ? text.slice(0, 200) + '…' : text; + console.warn(`[PG SLOW] query ${duration}ms params=${params.length} sql=${JSON.stringify(sqlPreview)}`); + } return { rows: result.rows, rowCount: result.rowCount || 0 }; } catch (error) { + (0, queryMetrics_1.recordQueryError)(); console.error('Database query error:', error); throw error; } }, + // Pool capacity snapshot for /api/metrics + /health/ready. + // Returns zeros when running in-memory (no pool). + poolStats: () => { + if (!pool) + return { total: 0, idle: 0, waiting: 0, max: POOL_MAX }; + return { + total: pool.totalCount ?? 0, + idle: pool.idleCount ?? 0, + waiting: pool.waitingCount ?? 0, + max: POOL_MAX, + }; + }, // Test connection connect: async () => { if (!pool) { diff --git a/backend/dist/database/db.js.map b/backend/dist/database/db.js.map index a90235b7..bb145efa 100644 --- a/backend/dist/database/db.js.map +++ b/backend/dist/database/db.js.map @@ -1 +1 @@ -{"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/database/db.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/database/db.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2BAA0B;AAC1B,sDAA4B;AAC5B,+CAAiC;AACjC,iDAA+D;AAE/D,MAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,+EAA+E;AAC/E,MAAM,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,MAAM,CAAC,CAAC,CAAC;AAErF,sDAAsD;AACtD,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;AAE9C,IAAI,IAAI,GAAgB,IAAI,CAAC;AAC7B,IAAI,KAAK,GAAiB,IAAI,CAAC;AAE/B,0EAA0E;AAC1E,8CAA8C;AAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC,CAAC,CAAC;AACtE,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,OAAO,CAAC,CAAC,CAAC;AACpF,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,MAAM,CAAC,CAAC,CAAC;AAExF,IAAI,YAAY,EAAE,CAAC;IACjB,IAAI,GAAG,IAAI,SAAI,CAAC;QACd,gBAAgB,EAAE,YAAY;QAC9B,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC,CAAC,CAAC,EAAE,kBAAkB,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK;QAClF,GAAG,EAAE,QAAQ;QACb,iBAAiB,EAAE,YAAY;QAC/B,uBAAuB,EAAE,eAAe;KACzC,CAAC,CAAC;IACH,OAAO,CAAC,GAAG,CAAC,qCAAqC,QAAQ,UAAU,YAAY,eAAe,eAAe,KAAK,CAAC,CAAC;AACtH,CAAC;KAAM,CAAC;IACN,OAAO,CAAC,IAAI,CAAC,sDAAsD,CAAC,CAAC;AACvE,CAAC;AAED,8CAA8C;AAC9C,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;AAExC,IAAI,SAAS,EAAE,CAAC;IACd,KAAK,GAAG,IAAI,iBAAK,CAAC,SAAS,EAAE;QAC3B,oBAAoB,EAAE,CAAC;QACvB,aAAa,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,GAAG,EAAE,IAAI,CAAC;KACtD,CAAC,CAAC;IAEH,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;QACvB,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;QACxB,OAAO,CAAC,KAAK,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;AACL,CAAC;KAAM,CAAC;IACN,OAAO,CAAC,IAAI,CAAC,8CAA8C,CAAC,CAAC;AAC/D,CAAC;AAED,6BAA6B;AAC7B,MAAM,WAAW,GAA0B;IACzC,MAAM,EAAE,EAAE;IACV,YAAY,EAAE,EAAE;IAChB,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE,EAAE;IACd,IAAI,EAAE,EAAE;IACR,SAAS,EAAE,EAAE;IACb,eAAe,EAAE,EAAE;IACnB,SAAS,EAAE,EAAE;IACb,gBAAgB,EAAE,EAAE;CACrB,CAAC;AAEW,QAAA,EAAE,GAAG;IAChB,gDAAgD;IAChD,IAAI,EAAE,KAAK,EAAE,GAAW,EAAiB,EAAE;QACzC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;YACzD,OAAO;QACT,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,gDAAgD;YAChD,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACnE,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;gBACnC,MAAM,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;YAC9B,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;YACxC,IAAA,0BAAW,EAAC,QAAQ,CAAC,CAAC;YACtB,IAAI,QAAQ,GAAG,gBAAgB,EAAE,CAAC;gBAChC,OAAO,CAAC,IAAI,CAAC,kBAAkB,QAAQ,OAAO,GAAG,CAAC,MAAM,WAAW,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,SAAS,CAAC,CAAC;YACrG,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAA,+BAAgB,GAAE,CAAC;YACnB,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;YAC7C,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,uCAAuC;IACvC,KAAK,EAAE,KAAK,EAAE,IAAY,EAAE,SAAgB,EAAE,EAA+C,EAAE;QAC7F,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,qBAAqB;YACrB,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;QACnC,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;YACxC,IAAA,0BAAW,EAAC,QAAQ,CAAC,CAAC;YACtB,IAAI,QAAQ,GAAG,gBAAgB,EAAE,CAAC;gBAChC,uEAAuE;gBACvE,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;gBACvE,OAAO,CAAC,IAAI,CAAC,mBAAmB,QAAQ,aAAa,MAAM,CAAC,MAAM,QAAQ,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;YAC1G,CAAC;YACD,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,CAAC,EAAE,CAAC;QAC/D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAA,+BAAgB,GAAE,CAAC;YACnB,OAAO,CAAC,KAAK,CAAC,uBAAuB,EAAE,KAAK,CAAC,CAAC;YAC9C,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,2DAA2D;IAC3D,kDAAkD;IAClD,SAAS,EAAE,GAAkE,EAAE;QAC7E,IAAI,CAAC,IAAI;YAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;QACnE,OAAO;YACL,KAAK,EAAG,IAAY,CAAC,UAAU,IAAI,CAAC;YACpC,IAAI,EAAG,IAAY,CAAC,SAAS,IAAI,CAAC;YAClC,OAAO,EAAG,IAAY,CAAC,YAAY,IAAI,CAAC;YACxC,GAAG,EAAE,QAAQ;SACd,CAAC;IACJ,CAAC;IAED,kBAAkB;IAClB,OAAO,EAAE,KAAK,IAAsB,EAAE;QACpC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;YAC5C,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;YACpC,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YAC/B,MAAM,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;YACzC,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,oCAAoC,EAAE,KAAK,CAAC,CAAC;YAC3D,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,oBAAoB;IACpB,GAAG,EAAE,KAAK,IAAmB,EAAE;QAC7B,IAAI,IAAI;YAAE,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC;QAC3B,IAAI,KAAK;YAAE,KAAK,CAAC,UAAU,EAAE,CAAC;IAChC,CAAC;CACF,CAAC;AAEF,qBAAqB;AACR,QAAA,KAAK,GAAG;IACnB,mBAAmB;IACnB,GAAG,EAAE,KAAK,EAAE,GAAW,EAA0B,EAAE;QACjD,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,IAAI,CAAC;YACH,OAAO,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC9B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;YACzC,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,+CAA+C;IAC/C,GAAG,EAAE,KAAK,EAAE,GAAW,EAAE,KAAa,EAAE,GAAY,EAAiB,EAAE;QACrE,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,IAAI,CAAC;YACH,IAAI,GAAG,EAAE,CAAC;gBACR,MAAM,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;YACrC,CAAC;iBAAM,CAAC;gBACN,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YAC9B,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC;IAED,sBAAsB;IACtB,GAAG,EAAE,KAAK,EAAE,GAAW,EAAiB,EAAE;QACxC,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC;IAED,kBAAkB;IAClB,OAAO,EAAE,KAAK,EAAK,GAAW,EAAqB,EAAE;QACnD,MAAM,KAAK,GAAG,MAAM,aAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAM,CAAC;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,kBAAkB;IAClB,OAAO,EAAE,KAAK,EAAE,GAAW,EAAE,KAAU,EAAE,GAAY,EAAiB,EAAE;QACtE,MAAM,aAAK,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC;IACnD,CAAC;IAED,oBAAoB;IACpB,IAAI,EAAE,KAAK,EAAE,GAAW,EAAmB,EAAE;QAC3C,IAAI,CAAC,KAAK;YAAE,OAAO,CAAC,CAAC;QACrB,IAAI,CAAC;YACH,OAAO,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAC;YAC1C,OAAO,CAAC,CAAC;QACX,CAAC;IACH,CAAC;IAED,iBAAiB;IACjB,IAAI,EAAE,KAAK,EAAE,GAAW,EAAE,KAAa,EAA0B,EAAE;QACjE,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,IAAI,CAAC;YACH,OAAO,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACtC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAC;YAC1C,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,iBAAiB;IACjB,IAAI,EAAE,KAAK,EAAE,GAAW,EAAE,KAAa,EAAE,KAAa,EAAiB,EAAE;QACvE,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;QACtC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;IAED,sBAAsB;IACtB,OAAO,EAAE,KAAK,EAAE,GAAW,EAA0C,EAAE;QACrE,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,IAAI,CAAC;YACH,OAAO,MAAM,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAClC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;YAC7C,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,qBAAqB;IACrB,WAAW,EAAE,GAAY,EAAE;QACzB,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,CAAC;IACpD,CAAC;CACF,CAAC;AAEF,kCAAkC;AACrB,QAAA,UAAU,GAAG;IACxB,+BAA+B;IAC/B,eAAe,EAAE,KAAK,EAAE,MAAc,EAAiB,EAAE;QACvD,MAAM,aAAK,CAAC,GAAG,CAAC,oBAAoB,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC3D,CAAC;IAED,8BAA8B;IAC9B,cAAc,EAAE,KAAK,IAAqB,EAAE;QAC1C,MAAM,MAAM,GAAG,MAAM,aAAK,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;QACrD,OAAO,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC;IAED,wBAAwB;IACxB,kBAAkB,EAAE,KAAK,EAAE,SAAiB,EAAiB,EAAE;QAC7D,MAAM,aAAK,CAAC,GAAG,CAAC,kBAAkB,EAAE,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,uBAAuB;IACvB,iBAAiB,EAAE,KAAK,IAAqB,EAAE;QAC7C,MAAM,IAAI,GAAG,MAAM,aAAK,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QACjD,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;IAChD,CAAC;IAED,gCAAgC;IAChC,qBAAqB,EAAE,KAAK,EAAE,KAAa,EAAiB,EAAE;QAC5D,MAAM,aAAK,CAAC,GAAG,CAAC,0BAA0B,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;IAChE,CAAC;IAED,+BAA+B;IAC/B,oBAAoB,EAAE,KAAK,IAAqB,EAAE;QAChD,MAAM,KAAK,GAAG,MAAM,aAAK,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC;QAC1D,OAAO,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACzC,CAAC;IAED,oCAAoC;IACpC,oBAAoB,EAAE,KAAK,IAAqB,EAAE;QAChD,OAAO,MAAM,aAAK,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;IAChD,CAAC;IAED,8CAA8C;IAC9C,SAAS,EAAE,KAAK,EAAE,KAAU,EAAiB,EAAE;QAC7C,MAAM,aAAK,CAAC,OAAO,CAAC,SAAS,KAAK,CAAC,MAAM,EAAE,EAAE,KAAK,CAAC,CAAC;QACpD,MAAM,aAAK,CAAC,GAAG,CAAC,cAAc,KAAK,CAAC,IAAI,EAAE,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IACvE,CAAC;IAED,iCAAiC;IACjC,QAAQ,EAAE,KAAK,EAAE,MAAc,EAAuB,EAAE;QACtD,OAAO,MAAM,aAAK,CAAC,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IAChD,CAAC;IAED,2BAA2B;IAC3B,oBAAoB,EAAE,KAAK,EAAE,IAAY,EAA0B,EAAE;QACnE,MAAM,MAAM,GAAG,MAAM,aAAK,CAAC,GAAG,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC;QACrD,OAAO,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9C,CAAC;CACF,CAAC"} \ No newline at end of file diff --git a/backend/dist/database/migrations.d.ts b/backend/dist/database/migrations.d.ts index 850d9e8b..8c4c6d30 100644 --- a/backend/dist/database/migrations.d.ts +++ b/backend/dist/database/migrations.d.ts @@ -9,7 +9,15 @@ export interface MigrationResult { readonly durationMs: number; readonly error?: string; } -export declare function applyPendingMigrations(): Promise; +/** Load every .sql file from `dir` sorted lexicographically. */ +export declare function loadMigrationsFromDir(dir: string): MigrationFile[]; +export declare function getMigrationsDir(): string; +export interface ApplyOptions { + /** When true, log the SQL each pending migration would execute and exit + * without touching the database. Used by `npm run migrate:status -- --dry-run`. */ + readonly dryRun?: boolean; +} +export declare function applyPendingMigrations(opts?: ApplyOptions): Promise; export declare function migrationStatus(): Promise<{ applied: string[]; pending: string[]; diff --git a/backend/dist/database/migrations.d.ts.map b/backend/dist/database/migrations.d.ts.map index db79c871..257fe918 100644 --- a/backend/dist/database/migrations.d.ts.map +++ b/backend/dist/database/migrations.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/database/migrations.ts"],"names":[],"mappings":"AA2BA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAuED,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CA4CzE;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC;IAC/C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC,CAQD"} \ No newline at end of file +{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/database/migrations.ts"],"names":[],"mappings":"AA2BA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,gEAAgE;AAGhE,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,EAAE,CAElE;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAsED,MAAM,WAAW,YAAY;IAC3B;wFACoF;IACpF,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,wBAAsB,sBAAsB,CAAC,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAmDhG;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC;IAC/C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC,CAQD"} \ No newline at end of file diff --git a/backend/dist/database/migrations.js b/backend/dist/database/migrations.js index 66b13d06..8fdfd48a 100644 --- a/backend/dist/database/migrations.js +++ b/backend/dist/database/migrations.js @@ -33,6 +33,8 @@ var __importStar = (this && this.__importStar) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); +exports.loadMigrationsFromDir = loadMigrationsFromDir; +exports.getMigrationsDir = getMigrationsDir; exports.applyPendingMigrations = applyPendingMigrations; exports.migrationStatus = migrationStatus; const fs = __importStar(require("fs")); @@ -60,6 +62,14 @@ const db_1 = require("./db"); */ const HERMES_MIGRATION_LOCK_ID = 0x4845524d; // 'HERM' hex-ish /** Load every .sql file from `dir` sorted lexicographically. */ +// Exposed so the migrate:down CLI (TASK-325) and dry-run mode (TASK-326) +// can reuse the same loader the boot path uses. +function loadMigrationsFromDir(dir) { + return loadMigrationsFrom(dir); +} +function getMigrationsDir() { + return resolveMigrationsDir(); +} function loadMigrationsFrom(dir) { let entries = []; try { @@ -126,7 +136,7 @@ function resolveMigrationsDir() { const srcCandidate = path.join(__dirname, '..', '..', 'src', 'database', 'migrations'); return srcCandidate; } -async function applyPendingMigrations() { +async function applyPendingMigrations(opts = {}) { await ensureMigrationsTable(); const gotLock = await acquireLock(); try { @@ -137,6 +147,11 @@ async function applyPendingMigrations() { if (applied.has(migration.name)) continue; const startedAt = Date.now(); + if (opts.dryRun) { + console.log(`[DRY-RUN] ${migration.name}\n${migration.up}\n---`); + results.push({ name: migration.name, success: true, durationMs: 0 }); + continue; + } try { await db_1.db.exec(migration.up); await db_1.db.query(`INSERT INTO schema_migrations (name) VALUES ($1) ON CONFLICT (name) DO NOTHING`, [migration.name]); diff --git a/backend/dist/database/migrations.js.map b/backend/dist/database/migrations.js.map index 916293a7..5298e54b 100644 --- a/backend/dist/database/migrations.js.map +++ b/backend/dist/database/migrations.js.map @@ -1 +1 @@ -{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/database/migrations.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/database/migrations.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/backend/dist/database/queryMetrics.d.ts b/backend/dist/database/queryMetrics.d.ts new file mode 100644 index 00000000..82ba176c --- /dev/null +++ b/backend/dist/database/queryMetrics.d.ts @@ -0,0 +1,21 @@ +/** + * Query latency histogram for /api/metrics (TASK-320). + * + * Cumulative-bucket Prometheus histogram. Bucket boundaries chosen to + * cover human-noticeable PG latency: 1ms (in-memory cache hit) to 5s + * (a query you'd want a slow-log entry for). + */ +export declare function recordQuery(durationMs: number): void; +export declare function recordQueryError(): void; +export interface HistogramSnapshot { + buckets: readonly number[]; + counts: number[]; + cumulativeCounts: number[]; + sum: number; + count: number; + errors: number; + meanMs: number; +} +export declare function getHistogram(): HistogramSnapshot; +export declare function resetHistogram(): void; +//# sourceMappingURL=queryMetrics.d.ts.map \ No newline at end of file diff --git a/backend/dist/database/queryMetrics.d.ts.map b/backend/dist/database/queryMetrics.d.ts.map new file mode 100644 index 00000000..cce6c01e --- /dev/null +++ b/backend/dist/database/queryMetrics.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"queryMetrics.d.ts","sourceRoot":"","sources":["../../src/database/queryMetrics.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CASpD;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,YAAY,IAAI,iBAAiB,CAgBhD;AAED,wBAAgB,cAAc,IAAI,IAAI,CAKrC"} \ No newline at end of file diff --git a/backend/dist/database/queryMetrics.js b/backend/dist/database/queryMetrics.js new file mode 100644 index 00000000..b486a598 --- /dev/null +++ b/backend/dist/database/queryMetrics.js @@ -0,0 +1,55 @@ +"use strict"; +/** + * Query latency histogram for /api/metrics (TASK-320). + * + * Cumulative-bucket Prometheus histogram. Bucket boundaries chosen to + * cover human-noticeable PG latency: 1ms (in-memory cache hit) to 5s + * (a query you'd want a slow-log entry for). + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.recordQuery = recordQuery; +exports.recordQueryError = recordQueryError; +exports.getHistogram = getHistogram; +exports.resetHistogram = resetHistogram; +const BUCKET_BOUNDS_MS = [1, 5, 10, 50, 100, 500, 1000, 5000]; +let counts = new Array(BUCKET_BOUNDS_MS.length).fill(0); +let sum = 0; +let count = 0; +let errors = 0; +function recordQuery(durationMs) { + for (let i = 0; i < BUCKET_BOUNDS_MS.length; i++) { + if (durationMs <= BUCKET_BOUNDS_MS[i]) { + counts[i]++; + break; + } + } + sum += durationMs; + count++; +} +function recordQueryError() { + errors++; +} +function getHistogram() { + const cumulative = []; + let running = 0; + for (const c of counts) { + running += c; + cumulative.push(running); + } + return { + buckets: BUCKET_BOUNDS_MS, + counts: [...counts], + cumulativeCounts: cumulative, + sum, + count, + errors, + meanMs: count === 0 ? 0 : sum / count, + }; +} +function resetHistogram() { + counts = new Array(BUCKET_BOUNDS_MS.length).fill(0); + sum = 0; + count = 0; + errors = 0; +} +//# sourceMappingURL=queryMetrics.js.map \ No newline at end of file diff --git a/backend/dist/database/queryMetrics.js.map b/backend/dist/database/queryMetrics.js.map new file mode 100644 index 00000000..2d05bd72 --- /dev/null +++ b/backend/dist/database/queryMetrics.js.map @@ -0,0 +1 @@ +{"version":3,"file":"queryMetrics.js","sourceRoot":"","sources":["../../src/database/queryMetrics.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;AASH,kCASC;AAED,4CAEC;AAYD,oCAgBC;AAED,wCAKC;AAvDD,MAAM,gBAAgB,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAU,CAAC;AAEvE,IAAI,MAAM,GAAG,IAAI,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACxD,IAAI,GAAG,GAAG,CAAC,CAAC;AACZ,IAAI,KAAK,GAAG,CAAC,CAAC;AACd,IAAI,MAAM,GAAG,CAAC,CAAC;AAEf,SAAgB,WAAW,CAAC,UAAkB;IAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,gBAAgB,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACjD,IAAI,UAAU,IAAI,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAC;YACtC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;YACZ,MAAM;QACR,CAAC;IACH,CAAC;IACD,GAAG,IAAI,UAAU,CAAC;IAClB,KAAK,EAAE,CAAC;AACV,CAAC;AAED,SAAgB,gBAAgB;IAC9B,MAAM,EAAE,CAAC;AACX,CAAC;AAYD,SAAgB,YAAY;IAC1B,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,OAAO,IAAI,CAAC,CAAC;QACb,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3B,CAAC;IACD,OAAO;QACL,OAAO,EAAE,gBAAgB;QACzB,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC;QACnB,gBAAgB,EAAE,UAAU;QAC5B,GAAG;QACH,KAAK;QACL,MAAM;QACN,MAAM,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,KAAK;KACtC,CAAC;AACJ,CAAC;AAED,SAAgB,cAAc;IAC5B,MAAM,GAAG,IAAI,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpD,GAAG,GAAG,CAAC,CAAC;IACR,KAAK,GAAG,CAAC,CAAC;IACV,MAAM,GAAG,CAAC,CAAC;AACb,CAAC"} \ No newline at end of file diff --git a/backend/dist/events/RedisBridge.d.ts b/backend/dist/events/RedisBridge.d.ts new file mode 100644 index 00000000..b8fe36d9 --- /dev/null +++ b/backend/dist/events/RedisBridge.d.ts @@ -0,0 +1,6 @@ +import { EventBus } from './EventBus'; +export interface BridgeHandle { + detach(): void; +} +export declare function attachRedisBridge(eventBus: EventBus, redisUrl: string): BridgeHandle; +//# sourceMappingURL=RedisBridge.d.ts.map \ No newline at end of file diff --git a/backend/dist/events/RedisBridge.d.ts.map b/backend/dist/events/RedisBridge.d.ts.map new file mode 100644 index 00000000..86145e59 --- /dev/null +++ b/backend/dist/events/RedisBridge.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"RedisBridge.d.ts","sourceRoot":"","sources":["../../src/events/RedisBridge.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AA4BtC,MAAM,WAAW,YAAY;IAC3B,MAAM,IAAI,IAAI,CAAC;CAChB;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CA+CpF"} \ No newline at end of file diff --git a/backend/dist/events/RedisBridge.js b/backend/dist/events/RedisBridge.js new file mode 100644 index 00000000..a7392916 --- /dev/null +++ b/backend/dist/events/RedisBridge.js @@ -0,0 +1,75 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.attachRedisBridge = attachRedisBridge; +const ioredis_1 = __importDefault(require("ioredis")); +/** + * Cross-replica event bridge over Redis pub/sub (TASK-330). + * + * The local EventBus is in-process; SSE clients on replica A miss events + * emitted on replica B. This bridge republishes whitelisted local events + * to a Redis channel and re-emits incoming channel messages locally. + * + * Loop prevention: every bridged payload is tagged with `_origin` (this + * replica's id). Echoes from the same origin are dropped. + */ +const CHANNEL = 'hermes:events:v1'; +const BRIDGED_EVENTS = [ + 'block_produced', + 'consensus_quorum', + 'consensus_failed', + 'chain_reorg', + 'mesh_block_received', + 'ci_results', + 'ci_failure', + 'ci_watch_triggered', + 'network_message', + 'state_root_mismatch', +]; +function attachRedisBridge(eventBus, redisUrl) { + const originId = process.env.REPLICA_ID || `${process.pid}@${require('os').hostname()}`; + const publisher = new ioredis_1.default(redisUrl); + const subscriber = new ioredis_1.default(redisUrl); + const localHandlers = new Map(); + for (const name of BRIDGED_EVENTS) { + const handler = (payload) => { + // Don't republish messages we just received from the channel. + if (payload && payload.__bridged) + return; + const wrapped = JSON.stringify({ event: name, payload, _origin: originId }); + publisher.publish(CHANNEL, wrapped).catch((err) => console.warn(`[REDIS BRIDGE] publish ${name} failed: ${err?.message || err}`)); + }; + eventBus.on(name, handler); + localHandlers.set(name, handler); + } + subscriber.subscribe(CHANNEL).catch((err) => console.error(`[REDIS BRIDGE] subscribe failed: ${err?.message || err}`)); + subscriber.on('message', (_channel, raw) => { + try { + const msg = JSON.parse(raw); + if (msg._origin === originId) + return; // skip our own + if (!BRIDGED_EVENTS.includes(msg.event)) + return; + // Mark so the local handler doesn't re-publish. + const payload = { ...(msg.payload ?? {}), __bridged: true }; + eventBus.emit(msg.event, payload); + } + catch (err) { + console.warn(`[REDIS BRIDGE] bad message: ${err?.message || err}`); + } + }); + console.log(`[REDIS BRIDGE] attached as ${originId} on channel ${CHANNEL}`); + return { + detach() { + for (const [name, handler] of localHandlers.entries()) { + eventBus.off(name, handler); + } + subscriber.unsubscribe(CHANNEL).catch(() => { }); + subscriber.disconnect(); + publisher.disconnect(); + }, + }; +} +//# sourceMappingURL=RedisBridge.js.map \ No newline at end of file diff --git a/backend/dist/events/RedisBridge.js.map b/backend/dist/events/RedisBridge.js.map new file mode 100644 index 00000000..b9bbd0f2 --- /dev/null +++ b/backend/dist/events/RedisBridge.js.map @@ -0,0 +1 @@ +{"version":3,"file":"RedisBridge.js","sourceRoot":"","sources":["../../src/events/RedisBridge.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/backend/dist/network/api.js b/backend/dist/network/api.js index 71daedc3..085dc2a2 100644 --- a/backend/dist/network/api.js +++ b/backend/dist/network/api.js @@ -2,6 +2,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.createMeshRouter = createMeshRouter; const express_1 = require("express"); +const Block_1 = require("../blockchain/Block"); const PeerRegistry_1 = require("./PeerRegistry"); const EventBus_1 = require("../events/EventBus"); /** @@ -40,6 +41,42 @@ function createMeshRouter(chain) { peers: PeerRegistry_1.peerRegistry.listPeers(), }); }); + // Header-only range fetch (TASK-003) — peers measuring honest-majority + // or chain tip don't need full transaction payloads. + router.get('/headers', (req, res) => { + const from = Math.max(0, Number(req.query.from ?? 0)); + const to = Math.max(from, Number(req.query.to ?? from)); + if (to - from > 1000) { + return res.status(400).json({ error: 'range exceeds 1000' }); + } + const headers = []; + for (let h = from; h <= to; h++) { + const block = chain.getBlockByHeight(h); + if (!block) + continue; + headers.push({ + ...block.header, + gasUsed: block.header.gasUsed.toString(), + gasLimit: block.header.gasLimit.toString(), + }); + } + res.json({ headers }); + }); + // Bulk block fetch (TASK-004) — full Block.toJSON for sync. + router.get('/blocks', (req, res) => { + const from = Math.max(0, Number(req.query.from ?? 0)); + const to = Math.max(from, Number(req.query.to ?? from)); + if (to - from > 100) { + return res.status(400).json({ error: 'range exceeds 100' }); + } + const blocks = []; + for (let h = from; h <= to; h++) { + const block = chain.getBlockByHeight(h); + if (block) + blocks.push(block.toJSON()); + } + res.json({ blocks }); + }); router.get('/head', (_req, res) => { const head = chain.getLatestBlock(); res.json({ @@ -49,25 +86,45 @@ function createMeshRouter(chain) { timestamp: head?.header.timestamp || 0, }); }); - router.post('/block', (req, res) => { - // Gossip acceptance is conservative: we log the claim + emit an event - // so monitors can see the incoming block, but we don't apply it to our - // chain until the full gossip deserializer + verification pipeline is - // wired (it needs Block.fromJSON, which doesn't exist yet). - const body = req.body || {}; - const height = Number(body?.header?.height ?? body?.height ?? -1); - const hash = String(body?.header?.hash ?? body?.hash ?? ''); - const producer = String(body?.header?.producer ?? body?.producer ?? ''); - if (height < 0 || !hash) { - return res.status(400).json({ accepted: false, reason: 'missing height or hash' }); + router.post('/block', async (req, res) => { + // Real gossip acceptance (TASK-002): deserialize via Block.fromJSON, + // then run through chain.addBlock which routes forks via ForkManager. + let block; + try { + block = Block_1.Block.fromJSON(req.body); } - EventBus_1.eventBus.emit('mesh_block_received', { height, hash, producer }); - const head = chain.getLatestBlock(); - res.json({ - accepted: false, - reason: 'gossip-apply pending Block.fromJSON deserializer', - head: { height: chain.getChainLength(), hash: head?.header.hash || '' }, + catch (err) { + return res.status(400).json({ + accepted: false, + reason: `fromJSON failed: ${err?.message || err}`, + }); + } + EventBus_1.eventBus.emit('mesh_block_received', { + height: block.header.height, + hash: block.header.hash, + producer: block.header.producer, }); + try { + const added = await chain.addBlock(block); + const head = chain.getLatestBlock(); + if (added) { + return res.json({ + accepted: true, + head: { height: chain.getChainLength(), hash: head?.header.hash || '' }, + }); + } + return res.status(409).json({ + accepted: false, + reason: 'addBlock rejected (parent unknown, finality violation, or invalid)', + head: { height: chain.getChainLength(), hash: head?.header.hash || '' }, + }); + } + catch (err) { + return res.status(409).json({ + accepted: false, + reason: err?.message || 'addBlock threw', + }); + } }); return router; } diff --git a/backend/dist/network/api.js.map b/backend/dist/network/api.js.map index 5a9e2d7b..6ef59f91 100644 --- a/backend/dist/network/api.js.map +++ b/backend/dist/network/api.js.map @@ -1 +1 @@ -{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/network/api.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/network/api.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/backend/dist/vm/GasMeter.d.ts b/backend/dist/vm/GasMeter.d.ts index 63b020f1..9a9f2e57 100644 --- a/backend/dist/vm/GasMeter.d.ts +++ b/backend/dist/vm/GasMeter.d.ts @@ -7,6 +7,15 @@ export declare const GAS_COSTS: { readonly POP: 2n; readonly ADD: 3n; readonly SUB: 3n; + readonly MUL: 5n; + readonly DIV: 5n; + readonly MOD: 5n; + readonly EQ: 3n; + readonly LT: 3n; + readonly GT: 3n; + readonly AND: 3n; + readonly OR: 3n; + readonly NOT: 3n; readonly SSTORE: 20000n; readonly STOP: 0n; readonly REVERT: 0n; diff --git a/backend/dist/vm/GasMeter.d.ts.map b/backend/dist/vm/GasMeter.d.ts.map index 5e05dc6a..22e3c68e 100644 --- a/backend/dist/vm/GasMeter.d.ts.map +++ b/backend/dist/vm/GasMeter.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"GasMeter.d.ts","sourceRoot":"","sources":["../../src/vm/GasMeter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;CAWZ,CAAC;AAEX,qBAAa,QAAQ;IACnB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,KAAK,CAAc;gBAEf,KAAK,EAAE,MAAM;IAIzB,kFAAkF;IAClF,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAW/B,QAAQ,IAAI,MAAM;IAIlB,YAAY,IAAI,MAAM;CAGvB;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpD"} \ No newline at end of file +{"version":3,"file":"GasMeter.d.ts","sourceRoot":"","sources":["../../src/vm/GasMeter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;CAqBZ,CAAC;AAEX,qBAAa,QAAQ;IACnB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,KAAK,CAAc;gBAEf,KAAK,EAAE,MAAM;IAIzB,kFAAkF;IAClF,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAW/B,QAAQ,IAAI,MAAM;IAIlB,YAAY,IAAI,MAAM;CAGvB;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpD"} \ No newline at end of file diff --git a/backend/dist/vm/GasMeter.js b/backend/dist/vm/GasMeter.js index 88feda03..2a6475a1 100644 --- a/backend/dist/vm/GasMeter.js +++ b/backend/dist/vm/GasMeter.js @@ -11,6 +11,16 @@ exports.GAS_COSTS = { POP: 2n, ADD: 3n, SUB: 3n, + // Arithmetic + comparisons + bitwise ops added across TASK-061..065. + MUL: 5n, + DIV: 5n, + MOD: 5n, + EQ: 3n, + LT: 3n, + GT: 3n, + AND: 3n, + OR: 3n, + NOT: 3n, SSTORE: 20000n, STOP: 0n, REVERT: 0n, diff --git a/backend/dist/vm/GasMeter.js.map b/backend/dist/vm/GasMeter.js.map index d0c9c77a..755bb88c 100644 --- a/backend/dist/vm/GasMeter.js.map +++ b/backend/dist/vm/GasMeter.js.map @@ -1 +1 @@ -{"version":3,"file":"GasMeter.js","sourceRoot":"","sources":["../../src/vm/GasMeter.ts"],"names":[],"mappings":";;;AA8CA,gCAEC;AAhDD;;;GAGG;AACU,QAAA,SAAS,GAAG;IACvB,IAAI,EAAE,EAAE;IACR,GAAG,EAAE,EAAE;IACP,GAAG,EAAE,EAAE;IACP,GAAG,EAAE,EAAE;IACP,MAAM,EAAE,MAAM;IACd,IAAI,EAAE,EAAE;IACR,MAAM,EAAE,EAAE;IACV,oEAAoE;IACpE,QAAQ,EAAE,IAAI;IACd,YAAY,EAAE,EAAE;CACR,CAAC;AAEX,MAAa,QAAQ;IAInB,YAAY,KAAa;QAFjB,UAAK,GAAW,EAAE,CAAC;QAGzB,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;IACzB,CAAC;IAED,kFAAkF;IAClF,MAAM,CAAC,MAAc;QACnB,IAAI,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;YAC5B,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,SAAS,CAAC;YAC7B,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;YACpB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC;QACzB,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,YAAY;QACV,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;CACF;AA3BD,4BA2BC;AAED,SAAgB,UAAU,CAAC,SAAiB;IAC1C,OAAO,iBAAS,CAAC,QAAQ,GAAG,iBAAS,CAAC,YAAY,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;AACzE,CAAC"} \ No newline at end of file +{"version":3,"file":"GasMeter.js","sourceRoot":"","sources":["../../src/vm/GasMeter.ts"],"names":[],"mappings":";;;AAwDA,gCAEC;AA1DD;;;GAGG;AACU,QAAA,SAAS,GAAG;IACvB,IAAI,EAAE,EAAE;IACR,GAAG,EAAE,EAAE;IACP,GAAG,EAAE,EAAE;IACP,GAAG,EAAE,EAAE;IACP,qEAAqE;IACrE,GAAG,EAAE,EAAE;IACP,GAAG,EAAE,EAAE;IACP,GAAG,EAAE,EAAE;IACP,EAAE,EAAE,EAAE;IACN,EAAE,EAAE,EAAE;IACN,EAAE,EAAE,EAAE;IACN,GAAG,EAAE,EAAE;IACP,EAAE,EAAE,EAAE;IACN,GAAG,EAAE,EAAE;IACP,MAAM,EAAE,MAAM;IACd,IAAI,EAAE,EAAE;IACR,MAAM,EAAE,EAAE;IACV,oEAAoE;IACpE,QAAQ,EAAE,IAAI;IACd,YAAY,EAAE,EAAE;CACR,CAAC;AAEX,MAAa,QAAQ;IAInB,YAAY,KAAa;QAFjB,UAAK,GAAW,EAAE,CAAC;QAGzB,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;IACzB,CAAC;IAED,kFAAkF;IAClF,MAAM,CAAC,MAAc;QACnB,IAAI,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;YAC5B,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,SAAS,CAAC;YAC7B,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;YACpB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,CAAC,SAAS,IAAI,MAAM,CAAC;QACzB,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,YAAY;QACV,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;CACF;AA3BD,4BA2BC;AAED,SAAgB,UAAU,CAAC,SAAiB;IAC1C,OAAO,iBAAS,CAAC,QAAQ,GAAG,iBAAS,CAAC,YAAY,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;AACzE,CAAC"} \ No newline at end of file diff --git a/backend/dist/vm/Interpreter.d.ts b/backend/dist/vm/Interpreter.d.ts index bebed299..2a529923 100644 --- a/backend/dist/vm/Interpreter.d.ts +++ b/backend/dist/vm/Interpreter.d.ts @@ -24,6 +24,24 @@ export type VmOp = { op: 'ADD'; } | { op: 'SUB'; +} | { + op: 'MUL'; +} | { + op: 'DIV'; +} | { + op: 'MOD'; +} | { + op: 'EQ'; +} | { + op: 'LT'; +} | { + op: 'GT'; +} | { + op: 'AND'; +} | { + op: 'OR'; +} | { + op: 'NOT'; } | { op: 'SSTORE'; args: [string, string]; diff --git a/backend/dist/vm/Interpreter.d.ts.map b/backend/dist/vm/Interpreter.d.ts.map index c0390c60..cedc63d9 100644 --- a/backend/dist/vm/Interpreter.d.ts.map +++ b/backend/dist/vm/Interpreter.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"Interpreter.d.ts","sourceRoot":"","sources":["../../src/vm/Interpreter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,kCAAkC,CAAC;AAGvD;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,IAAI,GACZ;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,CAAA;CAAE,GACvC;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,GACb;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,GACb;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,GACb;IAAE,EAAE,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GACxC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GACzD;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GACd;IAAE,EAAE,EAAE,QAAQ,CAAC;IAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC;AAEtC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,gBAAgB,GAAG,eAAe;CA2F/E;AAED,wGAAwG;AACxG,wBAAgB,cAAc,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,IAAI,CAS3D;AAED,eAAO,MAAM,WAAW,aAAoB,CAAC"} \ No newline at end of file +{"version":3,"file":"Interpreter.d.ts","sourceRoot":"","sources":["../../src/vm/Interpreter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,kCAAkC,CAAC;AAGvD;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,IAAI,GACZ;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,CAAA;CAAE,GACvC;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,GACb;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,GACb;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,GACb;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,GACb;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,GACb;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,GACb;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,GACb;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,GACb;IAAE,EAAE,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GACxC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,GACzD;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GACd;IAAE,EAAE,EAAE,QAAQ,CAAC;IAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC;AAEtC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,gBAAgB,GAAG,eAAe;CA+J/E;AAED,wGAAwG;AACxG,wBAAgB,cAAc,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,IAAI,CAS3D;AAED,eAAO,MAAM,WAAW,aAAoB,CAAC"} \ No newline at end of file diff --git a/backend/dist/vm/Interpreter.js b/backend/dist/vm/Interpreter.js index 05a757b8..ec901e14 100644 --- a/backend/dist/vm/Interpreter.js +++ b/backend/dist/vm/Interpreter.js @@ -44,6 +44,82 @@ class Interpreter { stack.push(b - a); break; } + case 'MUL': { + if (!meter.charge(GasMeter_1.GAS_COSTS.MUL)) { + return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at MUL' }; + } + const a = Number(stack.pop() ?? 0); + const b = Number(stack.pop() ?? 0); + stack.push(a * b); + break; + } + case 'DIV': { + if (!meter.charge(GasMeter_1.GAS_COSTS.DIV)) { + return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at DIV' }; + } + const b = Number(stack.pop() ?? 0); + const a = Number(stack.pop() ?? 0); + if (b === 0) + return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'division by zero' }; + stack.push(Math.trunc(a / b)); + break; + } + case 'MOD': { + if (!meter.charge(GasMeter_1.GAS_COSTS.MOD)) { + return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at MOD' }; + } + const b = Number(stack.pop() ?? 0); + const a = Number(stack.pop() ?? 0); + if (b === 0) + return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'modulo by zero' }; + stack.push(a % b); + break; + } + case 'EQ': { + if (!meter.charge(GasMeter_1.GAS_COSTS.EQ)) + return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at EQ' }; + stack.push(stack.pop() === stack.pop() ? 1 : 0); + break; + } + case 'LT': { + if (!meter.charge(GasMeter_1.GAS_COSTS.LT)) + return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at LT' }; + const b = Number(stack.pop() ?? 0); + const a = Number(stack.pop() ?? 0); + stack.push(a < b ? 1 : 0); + break; + } + case 'GT': { + if (!meter.charge(GasMeter_1.GAS_COSTS.GT)) + return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at GT' }; + const b = Number(stack.pop() ?? 0); + const a = Number(stack.pop() ?? 0); + stack.push(a > b ? 1 : 0); + break; + } + case 'AND': { + if (!meter.charge(GasMeter_1.GAS_COSTS.AND)) + return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at AND' }; + const a = BigInt(stack.pop() ?? 0); + const b = BigInt(stack.pop() ?? 0); + stack.push(Number(a & b)); + break; + } + case 'OR': { + if (!meter.charge(GasMeter_1.GAS_COSTS.OR)) + return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at OR' }; + const a = BigInt(stack.pop() ?? 0); + const b = BigInt(stack.pop() ?? 0); + stack.push(Number(a | b)); + break; + } + case 'NOT': { + if (!meter.charge(GasMeter_1.GAS_COSTS.NOT)) + return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at NOT' }; + const a = BigInt(stack.pop() ?? 0); + stack.push(Number(~a)); + break; + } case 'SSTORE': { if (!meter.charge(GasMeter_1.GAS_COSTS.SSTORE)) { return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at SSTORE' }; diff --git a/backend/dist/vm/Interpreter.js.map b/backend/dist/vm/Interpreter.js.map index 9ca7890d..cac8cb2c 100644 --- a/backend/dist/vm/Interpreter.js.map +++ b/backend/dist/vm/Interpreter.js.map @@ -1 +1 @@ -{"version":3,"file":"Interpreter.js","sourceRoot":"","sources":["../../src/vm/Interpreter.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"Interpreter.js","sourceRoot":"","sources":["../../src/vm/Interpreter.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/backend/dist/worker.js b/backend/dist/worker.js index 60933920..2745e45b 100644 --- a/backend/dist/worker.js +++ b/backend/dist/worker.js @@ -1,4 +1,37 @@ "use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; @@ -12,6 +45,7 @@ const EventBus_1 = require("./events/EventBus"); const StateManager_1 = require("./blockchain/StateManager"); const db_1 = require("./database/db"); const schema_1 = require("./database/schema"); +const migrations_1 = require("./database/migrations"); const agent_1 = require("./agent"); const logs_1 = require("./api/logs"); const network_1 = require("./api/network"); @@ -28,6 +62,7 @@ async function main() { const connected = await db_1.db.connect(); if (connected) { await db_1.db.exec(schema_1.createTables); + await (0, migrations_1.applyPendingMigrations)(); console.log('[WORKER] Database ready'); } const eventBus = EventBus_1.EventBus.getInstance(); @@ -48,6 +83,8 @@ async function main() { await agent_1.skillManager.initialize(); await agent_1.agentTaskStore.initialize(); await agent_1.agentRuntimeStore.initialize(); + await agent_1.githubUpdates.initialize(agentConfig.repoRoot); + agent_1.githubUpdates.startBackgroundSync(); let currentTaskId; let currentTaskTitle; agent_1.agentEvents.on('chunk', (chunk) => { @@ -106,6 +143,11 @@ async function main() { console.error('[WORKER] Agent worker failed to start:', error); }); } + // Paced commit pusher — drains tier-3-backlog → main at controlled cadence. + // No-ops unless PACED_PUSH_ENABLED=true and GITHUB_TOKEN set. + const { PacedPusher } = await Promise.resolve().then(() => __importStar(require('./agent/PacedPusher'))); + const pacer = new PacedPusher(agentConfig.repoRoot || process.cwd()); + pacer.start(); blockProducer.start(); const port = Number(process.env.PORT || 4000); const server = http_1.default.createServer((req, res) => { @@ -156,6 +198,7 @@ async function main() { blockProducer.stop(); agent_1.agentWorker.stop(); (0, network_1.stopNetworkHeartbeat)(); + agent_1.githubUpdates.stopBackgroundSync(); process.exitCode = 1; setTimeout(() => process.exit(1), 0); }); @@ -168,6 +211,7 @@ async function main() { blockProducer.stop(); agent_1.agentWorker.stop(); (0, network_1.stopNetworkHeartbeat)(); + agent_1.githubUpdates.stopBackgroundSync(); await db_1.db.end(); process.exit(0); }; diff --git a/backend/dist/worker.js.map b/backend/dist/worker.js.map index a0004d5b..9590293b 100644 --- a/backend/dist/worker.js.map +++ b/backend/dist/worker.js.map @@ -1 +1 @@ -{"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 3549e626..fc918111 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,7 +7,17 @@ "dev": "ts-node-dev --respawn --transpile-only src/api/server.ts", "build": "tsc -p tsconfig.json && node -e \"require('fs').cpSync('src/database/migrations','dist/database/migrations',{recursive:true})\"", "start": "node dist/api/server.js", - "test": "npm run build && node --test tests/*.test.js" + "test": "npm run build && node --test tests/*.test.js", + "pace:push": "node scripts/paced-push.js", + "migrate:down": "node scripts/migrate-down.js", + "migrate:status": "node scripts/migrate-status.js", + "audit:redis-ttl": "node scripts/audit-redis-ttl.js", + "backup": "node scripts/backup-db.js", + "restore": "node scripts/restore-db.js", + "schema:diff": "node scripts/schema-diff.js", + "vanity": "node scripts/vanity-address.js", + "bulk-keys": "node scripts/bulk-keys.js", + "rss": "node scripts/generate-rss.js" }, "keywords": [ "blockchain", diff --git a/backend/scripts/audit-redis-ttl.js b/backend/scripts/audit-redis-ttl.js new file mode 100644 index 00000000..48e0ed26 --- /dev/null +++ b/backend/scripts/audit-redis-ttl.js @@ -0,0 +1,66 @@ +#!/usr/bin/env node +/** + * audit-redis-ttl — TASK-329 + * + * Walks backend/src for cache.set / cache.setJSON / cache.hset callsites + * and flags any without an explicit TTL. Prints leaks one-per-line and + * exits non-zero on findings (CI-friendly). + * + * npm run audit:redis-ttl + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const SRC_ROOT = path.resolve(__dirname, '..', 'src'); +const PATTERN = /\bcache\.(set|setJSON|hset)\s*\(/g; + +let leaks = []; + +function walk(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + walk(full); + } else if (entry.name.endsWith('.ts')) { + auditFile(full); + } + } +} + +function auditFile(file) { + const text = fs.readFileSync(file, 'utf8'); + const lines = text.split('\n'); + for (let i = 0; i < lines.length; i++) { + PATTERN.lastIndex = 0; + const match = PATTERN.exec(lines[i]); + if (!match) continue; + + // Heuristic: count commas before line-ending or close-paren on this line + // and the next 2 lines. cache.set('k', v) → 1 comma → no TTL + // cache.set('k', v, 60) → 2 commas → TTL set + const slice = lines.slice(i, Math.min(i + 3, lines.length)).join('\n'); + const fnCall = slice.slice(slice.indexOf(match[0])); + const closeIdx = fnCall.indexOf(')'); + if (closeIdx < 0) continue; + const argsBlock = fnCall.slice(0, closeIdx); + const commaCount = (argsBlock.match(/,/g) || []).length; + const requiredCommas = match[1] === 'hset' ? 2 : 1; // hset(key, field, value) vs set(key, value) + if (commaCount <= requiredCommas) { + const rel = path.relative(process.cwd(), file); + leaks.push(`${rel}:${i + 1} — ${match[0]}…) [no TTL]`); + } + } +} + +walk(SRC_ROOT); + +if (leaks.length === 0) { + console.log('[AUDIT] no missing-TTL cache writes found'); + process.exit(0); +} +console.log(`[AUDIT] ${leaks.length} cache write(s) without TTL:`); +for (const leak of leaks) console.log(` ${leak}`); +process.exit(1); diff --git a/backend/scripts/backup-db.js b/backend/scripts/backup-db.js new file mode 100644 index 00000000..5164d3ae --- /dev/null +++ b/backend/scripts/backup-db.js @@ -0,0 +1,103 @@ +#!/usr/bin/env node +/** + * backup-db — TASK-323 + * + * Runs pg_dump on DATABASE_URL, gzips, uploads to S3 with date-stamped key: + * ${S3_BACKUP_PREFIX}${YYYY-MM-DD}/hermes-${HH:MM:SS}.sql.gz + * + * Required env: DATABASE_URL, S3_BACKUP_BUCKET, AWS_REGION, + * AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY (or instance role). + * Optional: S3_BACKUP_PREFIX (default 'hermes-backups/'). + * + * Usage: + * npm run backup + * npm run backup -- --prune # also delete keys older than 30d + */ + +'use strict'; + +const { spawn } = require('child_process'); +const { S3Client, PutObjectCommand, ListObjectsV2Command, DeleteObjectsCommand } = + (() => { + try { + return require('@aws-sdk/client-s3'); + } catch { + console.error('Missing @aws-sdk/client-s3. Install with: npm i -w backend @aws-sdk/client-s3'); + process.exit(2); + } + })(); + +const DATABASE_URL = process.env.DATABASE_URL; +const BUCKET = process.env.S3_BACKUP_BUCKET; +const REGION = process.env.AWS_REGION || 'us-east-1'; +const PREFIX = (process.env.S3_BACKUP_PREFIX || 'hermes-backups/').replace(/\/?$/, '/'); +const RETAIN_DAYS = Number(process.env.BACKUP_RETAIN_DAYS || '30'); + +if (!DATABASE_URL) { console.error('DATABASE_URL required'); process.exit(2); } +if (!BUCKET) { console.error('S3_BACKUP_BUCKET required'); process.exit(2); } + +function nowKey() { + const d = new Date(); + const ymd = d.toISOString().slice(0, 10); + const hms = d.toISOString().slice(11, 19).replace(/:/g, '-'); + return `${PREFIX}${ymd}/hermes-${hms}.sql.gz`; +} + +async function backup() { + const key = nowKey(); + const startedAt = Date.now(); + console.log(`[BACKUP] dumping → s3://${BUCKET}/${key}`); + + // pg_dump | gzip — collect into a buffer (simple; for >1GB swap to multipart upload). + const chunks = []; + await new Promise((resolve, reject) => { + const dump = spawn('pg_dump', ['--no-owner', '--no-acl', DATABASE_URL]); + const gzip = spawn('gzip', ['-c']); + dump.stdout.pipe(gzip.stdin); + dump.stderr.on('data', (d) => process.stderr.write(d)); + gzip.stdout.on('data', (c) => chunks.push(c)); + let dumpExit = null, gzipExit = null; + const done = (err) => { + if (err) return reject(err); + if (dumpExit !== null && gzipExit !== null) { + if (dumpExit !== 0) return reject(new Error(`pg_dump exit ${dumpExit}`)); + if (gzipExit !== 0) return reject(new Error(`gzip exit ${gzipExit}`)); + resolve(); + } + }; + dump.on('close', (c) => { dumpExit = c; done(); }); + gzip.on('close', (c) => { gzipExit = c; done(); }); + dump.on('error', reject); + gzip.on('error', reject); + }); + + const body = Buffer.concat(chunks); + const s3 = new S3Client({ region: REGION }); + await s3.send(new PutObjectCommand({ Bucket: BUCKET, Key: key, Body: body })); + const durationMs = Date.now() - startedAt; + console.log(`[BACKUP] uploaded ${body.length} bytes in ${durationMs}ms`); + + if (process.argv.includes('--prune') && RETAIN_DAYS > 0) { + await prune(s3); + } +} + +async function prune(s3) { + const cutoff = Date.now() - RETAIN_DAYS * 86400_000; + const list = await s3.send(new ListObjectsV2Command({ Bucket: BUCKET, Prefix: PREFIX })); + const stale = (list.Contents || []).filter((o) => o.LastModified && o.LastModified.getTime() < cutoff); + if (stale.length === 0) { + console.log(`[BACKUP] no objects older than ${RETAIN_DAYS}d`); + return; + } + await s3.send(new DeleteObjectsCommand({ + Bucket: BUCKET, + Delete: { Objects: stale.map((o) => ({ Key: o.Key })) }, + })); + console.log(`[BACKUP] pruned ${stale.length} stale object(s)`); +} + +backup().catch((err) => { + console.error('[BACKUP] failed:', err && err.message ? err.message : err); + process.exit(1); +}); diff --git a/backend/scripts/bulk-keys.js b/backend/scripts/bulk-keys.js new file mode 100644 index 00000000..9b87eb63 --- /dev/null +++ b/backend/scripts/bulk-keys.js @@ -0,0 +1,26 @@ +#!/usr/bin/env node +/** + * bulk-keys — TASK-139 + * + * Generate N keypairs at once for test fixtures. + * + * node backend/scripts/bulk-keys.js --count 100 > fixtures/keys.json + */ + +'use strict'; + +const path = require('path'); + +function arg(name, def) { + const i = process.argv.indexOf(name); + return i >= 0 ? process.argv[i + 1] : def; +} + +const COUNT = Math.max(1, Number(arg('--count', '10'))); +const { generateKeypair } = require(path.resolve(__dirname, '..', 'dist', 'blockchain', 'Crypto')); + +const keys = []; +for (let i = 0; i < COUNT; i++) { + keys.push(generateKeypair()); +} +process.stdout.write(JSON.stringify(keys, null, 2) + '\n'); diff --git a/backend/scripts/generate-rss.js b/backend/scripts/generate-rss.js new file mode 100644 index 00000000..b96bfd04 --- /dev/null +++ b/backend/scripts/generate-rss.js @@ -0,0 +1,74 @@ +#!/usr/bin/env node +/** + * generate-rss — TASK-485 + * + * Generates a /feed.xml RSS feed from CHANGELOG.md + docs/blog/*.md. + * Run during build or via cron; writes to frontend/public/feed.xml. + * + * node backend/scripts/generate-rss.js + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const REPO = path.resolve(__dirname, '..', '..'); +const OUT = path.join(REPO, 'frontend', 'public', 'feed.xml'); +const SITE = process.env.HERMES_SITE_URL || 'https://hermeschain.io'; + +function escape(s) { + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +} + +function isoDate(d) { + return new Date(d).toUTCString(); +} + +function readBlogPosts() { + const dir = path.join(REPO, 'docs', 'blog'); + if (!fs.existsSync(dir)) return []; + return fs.readdirSync(dir) + .filter((f) => f.endsWith('.md')) + .map((f) => { + const full = path.join(dir, f); + const stat = fs.statSync(full); + const content = fs.readFileSync(full, 'utf8'); + const titleMatch = content.match(/^#\s+(.+)/m); + return { + title: titleMatch ? titleMatch[1] : f.replace(/\.md$/, ''), + link: `${SITE}/blog/${f.replace(/\.md$/, '')}`, + pubDate: isoDate(stat.mtimeMs), + description: content.slice(0, 500).replace(/^#.+/m, '').trim().slice(0, 280), + }; + }) + .sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate)); +} + +const items = readBlogPosts(); +const itemsXml = items.map((item) => ` + + ${escape(item.title)} + ${escape(item.link)} + ${item.pubDate} + ${escape(item.description)} + `).join(''); + +const xml = ` + + + Hermeschain + ${SITE} + Updates from the autonomous AI blockchain + en-us + ${isoDate(Date.now())}${itemsXml} + +`; + +fs.mkdirSync(path.dirname(OUT), { recursive: true }); +fs.writeFileSync(OUT, xml); +console.log(`[RSS] wrote ${items.length} item(s) → ${path.relative(REPO, OUT)}`); diff --git a/backend/scripts/migrate-down.js b/backend/scripts/migrate-down.js new file mode 100644 index 00000000..23c04255 --- /dev/null +++ b/backend/scripts/migrate-down.js @@ -0,0 +1,55 @@ +#!/usr/bin/env node +/** + * migrate:down — TASK-325 + * + * Roll back a single applied migration by running its `-- down:` half + * and removing its row from schema_migrations. + * + * npm run migrate:down -- 0007 + * + * Refuses to run in production unless FORCE_PROD_DOWN=1. + */ + +'use strict'; + +const path = require('path'); +const { loadMigrationsFromDir, getMigrationsDir } = require(path.resolve(__dirname, '..', 'dist', 'database', 'migrations')); +const { db } = require(path.resolve(__dirname, '..', 'dist', 'database', 'db')); + +async function main() { + const arg = process.argv[2]; + if (!arg) { + console.error('usage: npm run migrate:down -- '); + process.exit(2); + } + + if (process.env.NODE_ENV === 'production' && process.env.FORCE_PROD_DOWN !== '1') { + console.error('refusing to run in production without FORCE_PROD_DOWN=1'); + process.exit(2); + } + + await db.connect(); + const migrations = loadMigrationsFromDir(getMigrationsDir()); + const target = migrations.find((m) => m.name.startsWith(arg)); + if (!target) { + console.error(`no migration matches ${arg}`); + process.exit(1); + } + if (!target.down || target.down.trim().length === 0) { + console.error(`migration ${target.name} has no -- down: block`); + process.exit(1); + } + + console.log(`[MIGRATE:DOWN] ${target.name}`); + console.log(target.down); + console.log('---'); + + await db.exec(target.down); + await db.query(`DELETE FROM schema_migrations WHERE name = $1`, [target.name]); + console.log(`[MIGRATE:DOWN] ${target.name} reverted; row removed from schema_migrations`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/backend/scripts/migrate-status.js b/backend/scripts/migrate-status.js new file mode 100644 index 00000000..2e33e8ae --- /dev/null +++ b/backend/scripts/migrate-status.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node +/** + * migrate:status — TASK-326 + * + * npm run migrate:status # list applied + pending + * npm run migrate:status -- --dry-run # log SQL each pending would run, no writes + */ + +'use strict'; + +const path = require('path'); +const { migrationStatus, applyPendingMigrations } = require(path.resolve(__dirname, '..', 'dist', 'database', 'migrations')); +const { db } = require(path.resolve(__dirname, '..', 'dist', 'database', 'db')); + +async function main() { + await db.connect(); + + if (process.argv.includes('--dry-run')) { + console.log('[MIGRATE:STATUS] dry-run — printing pending SQL without applying\n'); + await applyPendingMigrations({ dryRun: true }); + return; + } + + const status = await migrationStatus(); + console.log(`Applied (${status.applied.length}):`); + for (const name of status.applied) console.log(` + ${name}`); + console.log(`\nPending (${status.pending.length}):`); + for (const name of status.pending) console.log(` - ${name}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/backend/scripts/paced-push.js b/backend/scripts/paced-push.js new file mode 100644 index 00000000..689dc0c6 --- /dev/null +++ b/backend/scripts/paced-push.js @@ -0,0 +1,119 @@ +#!/usr/bin/env node +/** + * Paced commit pusher. + * + * Reads `data/push_pointer.txt` (just an integer N — the count of commits + * already promoted from the local stack to origin/main) and pushes the + * NEXT commit by advancing main one step at a time: + * + * git push origin :refs/heads/main + * + * Configurable via env: + * PUSH_BRANCH — local source branch (default: tier-3-backlog) + * PUSH_TARGET — remote ref (default: main) + * PUSH_REMOTE — remote name (default: origin) + * PUSH_BATCH — commits per fire (default: 1) + * POINTER_FILE — pointer state path (default: data/push_pointer.txt) + * + * Cadence is set externally (cron, Hermes worker interval). At 60/day + * with batch=1, fire every 24 minutes. With batch=5, every 2 hours. + * + * Idempotent: re-running with the same pointer pushes nothing if the + * remote is already at the branch tip. + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const REPO_ROOT = path.resolve(__dirname, '..', '..'); +const POINTER_FILE = path.resolve( + REPO_ROOT, + process.env.POINTER_FILE || 'data/push_pointer.txt', +); +const BRANCH = process.env.PUSH_BRANCH || 'tier-3-backlog'; +const TARGET = process.env.PUSH_TARGET || 'main'; +const REMOTE = process.env.PUSH_REMOTE || 'origin'; +const BATCH = Math.max(1, Number(process.env.PUSH_BATCH || '1')); + +function readPointer() { + try { + const raw = fs.readFileSync(POINTER_FILE, 'utf8').trim(); + const n = parseInt(raw, 10); + return Number.isFinite(n) && n >= 0 ? n : 0; + } catch { + return 0; + } +} + +function writePointer(n) { + fs.mkdirSync(path.dirname(POINTER_FILE), { recursive: true }); + fs.writeFileSync(POINTER_FILE, `${n}\n`); +} + +function git(args) { + return execSync(['git', ...args].join(' '), { + cwd: REPO_ROOT, + encoding: 'utf8', + }).trim(); +} + +function listForwardCommits() { + const ref = `${REMOTE}/${TARGET}..${BRANCH}`; + try { + const out = git(['rev-list', '--reverse', ref]); + return out ? out.split('\n').filter(Boolean) : []; + } catch (err) { + console.error(`[PACER] failed to enumerate commits: ${err && err.message ? err.message : err}`); + return []; + } +} + +function pushCommit(sha) { + try { + const refspec = `${sha}:refs/heads/${TARGET}`; + git(['push', REMOTE, refspec]); + return true; + } catch (err) { + console.error(`[PACER] push of ${sha} failed: ${err && err.message ? err.message : err}`); + return false; + } +} + +function main() { + try { + git(['fetch', REMOTE, TARGET]); + } catch (err) { + console.warn(`[PACER] fetch failed (continuing): ${err && err.message ? err.message : err}`); + } + + const queue = listForwardCommits(); + if (queue.length === 0) { + console.log('[PACER] nothing to push (target already at branch tip)'); + return; + } + + console.log(`[PACER] ${queue.length} commit(s) ahead of ${REMOTE}/${TARGET}; pushing up to ${BATCH}`); + + const before = readPointer(); + let pushed = 0; + for (let i = 0; i < BATCH && i < queue.length; i++) { + const sha = queue[i]; + const subject = git(['log', '-1', '--pretty=%s', sha]); + if (!pushCommit(sha)) { + console.error('[PACER] aborting batch on first failure'); + break; + } + pushed++; + console.log(`[PACER] pushed ${sha.slice(0, 8)} ${subject}`); + } + + if (pushed > 0) { + writePointer(before + pushed); + console.log(`[PACER] pointer ${before} → ${before + pushed} (+${pushed})`); + } +} + +main(); diff --git a/backend/scripts/restore-db.js b/backend/scripts/restore-db.js new file mode 100644 index 00000000..4416a795 --- /dev/null +++ b/backend/scripts/restore-db.js @@ -0,0 +1,75 @@ +#!/usr/bin/env node +/** + * restore-db — TASK-324 + * + * Downloads a backup from S3 and restores into RESTORE_DATABASE_URL. + * Refuses to restore into a target equal to DATABASE_URL unless --force. + * + * npm run restore -- --latest + * npm run restore -- --key hermes-backups/2026-04-27/hermes-12-00-00.sql.gz + */ + +'use strict'; + +const { spawn } = require('child_process'); +const { Readable } = require('stream'); +const zlib = require('zlib'); +const { S3Client, GetObjectCommand, ListObjectsV2Command } = + (() => { + try { return require('@aws-sdk/client-s3'); } + catch { console.error('Missing @aws-sdk/client-s3. Install with: npm i -w backend @aws-sdk/client-s3'); process.exit(2); } + })(); + +const TARGET = process.env.RESTORE_DATABASE_URL; +const SOURCE = process.env.DATABASE_URL; +const BUCKET = process.env.S3_BACKUP_BUCKET; +const REGION = process.env.AWS_REGION || 'us-east-1'; +const PREFIX = (process.env.S3_BACKUP_PREFIX || 'hermes-backups/').replace(/\/?$/, '/'); + +if (!TARGET) { console.error('RESTORE_DATABASE_URL required'); process.exit(2); } +if (!BUCKET) { console.error('S3_BACKUP_BUCKET required'); process.exit(2); } +if (TARGET === SOURCE && !process.argv.includes('--force')) { + console.error('refusing to restore into DATABASE_URL (production source). Re-run with --force to override.'); + process.exit(2); +} + +async function pickKey(s3) { + const idx = process.argv.indexOf('--key'); + if (idx >= 0 && process.argv[idx + 1]) return process.argv[idx + 1]; + if (!process.argv.includes('--latest')) { + console.error('specify --latest or --key '); + process.exit(2); + } + const list = await s3.send(new ListObjectsV2Command({ Bucket: BUCKET, Prefix: PREFIX })); + const objs = (list.Contents || []).filter((o) => o.Key.endsWith('.sql.gz')); + objs.sort((a, b) => b.LastModified - a.LastModified); + if (objs.length === 0) { console.error('no backups found'); process.exit(1); } + return objs[0].Key; +} + +async function restore() { + const s3 = new S3Client({ region: REGION }); + const key = await pickKey(s3); + console.log(`[RESTORE] downloading s3://${BUCKET}/${key}`); + const obj = await s3.send(new GetObjectCommand({ Bucket: BUCKET, Key: key })); + const stream = obj.Body; + + await new Promise((resolve, reject) => { + const psql = spawn('psql', ['--dbname=' + TARGET, '--quiet']); + psql.stderr.on('data', (d) => process.stderr.write(d)); + stream.pipe(zlib.createGunzip()).pipe(psql.stdin); + psql.on('close', (c) => c === 0 ? resolve() : reject(new Error(`psql exit ${c}`))); + psql.on('error', reject); + }); + + // Smoke check + const smoke = spawn('psql', ['--dbname=' + TARGET, '-A', '-t', '-c', + `SELECT 'blocks=' || COUNT(*) FROM blocks; SELECT 'transactions=' || COUNT(*) FROM transactions;`]); + smoke.stdout.on('data', (d) => process.stdout.write(d)); + smoke.on('close', (c) => process.exit(c)); +} + +restore().catch((err) => { + console.error('[RESTORE] failed:', err && err.message ? err.message : err); + process.exit(1); +}); diff --git a/backend/scripts/schema-diff.js b/backend/scripts/schema-diff.js new file mode 100644 index 00000000..2f4b9521 --- /dev/null +++ b/backend/scripts/schema-diff.js @@ -0,0 +1,99 @@ +#!/usr/bin/env node +/** + * schema-diff — TASK-327 + * + * Compares table + column + index sets between two PG databases. + * Read-only; never writes. + * + * npm run schema:diff -- --left $DEV_URL --right $PROD_URL + * + * Exits 1 if diffs found (CI-friendly), 0 if identical. + */ + +'use strict'; + +const { Client } = require('pg'); + +function parseArg(name) { + const i = process.argv.indexOf(name); + return i >= 0 ? process.argv[i + 1] : null; +} + +const LEFT = parseArg('--left'); +const RIGHT = parseArg('--right'); +if (!LEFT || !RIGHT) { + console.error('usage: npm run schema:diff -- --left --right '); + process.exit(2); +} + +async function introspect(url) { + const client = new Client({ connectionString: url }); + await client.connect(); + try { + const tables = (await client.query( + `SELECT table_name FROM information_schema.tables + WHERE table_schema = 'public' AND table_type = 'BASE TABLE' + ORDER BY table_name` + )).rows.map((r) => r.table_name); + + const result = {}; + for (const t of tables) { + const cols = (await client.query( + `SELECT column_name, data_type, is_nullable, column_default + FROM information_schema.columns + WHERE table_schema = 'public' AND table_name = $1 + ORDER BY ordinal_position`, [t] + )).rows; + const indexes = (await client.query( + `SELECT indexname, indexdef FROM pg_indexes + WHERE schemaname = 'public' AND tablename = $1 + ORDER BY indexname`, [t] + )).rows; + result[t] = { columns: cols, indexes }; + } + return result; + } finally { + await client.end(); + } +} + +(async () => { + const [left, right] = await Promise.all([introspect(LEFT), introspect(RIGHT)]); + const allTables = new Set([...Object.keys(left), ...Object.keys(right)]); + const diffs = []; + + for (const t of [...allTables].sort()) { + if (!left[t]) { diffs.push(`+ table ${t} (only on right)`); continue; } + if (!right[t]) { diffs.push(`- table ${t} (only on left)`); continue; } + + // Column diff + const lcols = new Map(left[t].columns.map((c) => [c.column_name, c])); + const rcols = new Map(right[t].columns.map((c) => [c.column_name, c])); + const allCols = new Set([...lcols.keys(), ...rcols.keys()]); + for (const col of [...allCols].sort()) { + if (!lcols.has(col)) { diffs.push(`+ ${t}.${col} (only on right)`); continue; } + if (!rcols.has(col)) { diffs.push(`- ${t}.${col} (only on left)`); continue; } + const a = lcols.get(col), b = rcols.get(col); + if (a.data_type !== b.data_type || a.is_nullable !== b.is_nullable) { + diffs.push(`~ ${t}.${col}: ${a.data_type}/${a.is_nullable} vs ${b.data_type}/${b.is_nullable}`); + } + } + + // Index diff (by name) + const lidx = new Set(left[t].indexes.map((i) => i.indexname)); + const ridx = new Set(right[t].indexes.map((i) => i.indexname)); + for (const i of lidx) if (!ridx.has(i)) diffs.push(`- index ${t}.${i} (only on left)`); + for (const i of ridx) if (!lidx.has(i)) diffs.push(`+ index ${t}.${i} (only on right)`); + } + + if (diffs.length === 0) { + console.log('[SCHEMA DIFF] no differences'); + process.exit(0); + } + console.log(`[SCHEMA DIFF] ${diffs.length} difference(s):`); + for (const d of diffs) console.log(` ${d}`); + process.exit(1); +})().catch((err) => { + console.error('[SCHEMA DIFF] failed:', err && err.message ? err.message : err); + process.exit(2); +}); diff --git a/backend/scripts/vanity-address.js b/backend/scripts/vanity-address.js new file mode 100644 index 00000000..36f430e3 --- /dev/null +++ b/backend/scripts/vanity-address.js @@ -0,0 +1,56 @@ +#!/usr/bin/env node +/** + * vanity-address — TASK-138 + * + * Brute-force keypair generation until address starts with prefix. + * + * node backend/scripts/vanity-address.js --prefix Hermes --max 1000000 + * + * NOTE: long prefixes are exponentially slow. 4 chars ~ instant. + * 6 chars ~ minutes. 8 chars ~ hours. base58 = 58 chars in alphabet. + */ + +'use strict'; + +const path = require('path'); + +function arg(name, def) { + const i = process.argv.indexOf(name); + return i >= 0 ? process.argv[i + 1] : def; +} + +const PREFIX = arg('--prefix', 'a'); +const MAX = Number(arg('--max', '1000000')); +const STARTS_WITH = arg('--starts-with', 'true') !== 'false'; + +const { generateKeypair } = require(path.resolve(__dirname, '..', 'dist', 'blockchain', 'Crypto')); + +const startedAt = Date.now(); +let attempts = 0; +let found = null; + +while (attempts < MAX) { + attempts++; + const kp = generateKeypair(); + if (STARTS_WITH) { + if (kp.publicKey.startsWith(PREFIX)) { found = kp; break; } + } else { + if (kp.publicKey.includes(PREFIX)) { found = kp; break; } + } + if (attempts % 50000 === 0) { + console.error(`[VANITY] ${attempts.toLocaleString()} attempts, no match yet`); + } +} + +const elapsedMs = Date.now() - startedAt; +if (!found) { + console.error(`[VANITY] no match after ${attempts.toLocaleString()} attempts in ${elapsedMs}ms`); + process.exit(1); +} + +console.log(JSON.stringify({ + attempts, + elapsedMs, + publicKey: found.publicKey, + privateKey: found.privateKey, +}, null, 2)); diff --git a/backend/src/agent/PacedPusher.ts b/backend/src/agent/PacedPusher.ts new file mode 100644 index 00000000..e178d2d3 --- /dev/null +++ b/backend/src/agent/PacedPusher.ts @@ -0,0 +1,158 @@ +import { execSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * PacedPusher — runs inside the Railway worker process. Every PUSH_INTERVAL_MS + * (default 24min = 60/day), fetches origin and advances refs/heads/main by + * one commit drawn from origin/PUSH_BRANCH (default tier-3-backlog). + * + * Disabled by default. Activate by setting: + * PACED_PUSH_ENABLED=true + * GITHUB_TOKEN= + * + * Optional: + * PUSH_BRANCH=tier-3-backlog + * PUSH_TARGET=main + * PUSH_REMOTE=origin + * PUSH_BATCH=1 + * PUSH_INTERVAL_MS=1440000 (24 minutes) + * POINTER_FILE=data/push_pointer.txt + */ + +export class PacedPusher { + private interval: NodeJS.Timeout | null = null; + private repoRoot: string; + private pointerFile: string; + private branch: string; + private target: string; + private remote: string; + private batch: number; + private intervalMs: number; + + constructor(repoRoot: string) { + this.repoRoot = repoRoot; + this.pointerFile = path.resolve( + repoRoot, + process.env.POINTER_FILE || 'data/push_pointer.txt', + ); + this.branch = process.env.PUSH_BRANCH || 'tier-3-backlog'; + this.target = process.env.PUSH_TARGET || 'main'; + this.remote = process.env.PUSH_REMOTE || 'origin'; + this.batch = Math.max(1, Number(process.env.PUSH_BATCH || '1')); + this.intervalMs = Math.max(60_000, Number(process.env.PUSH_INTERVAL_MS || '1440000')); + } + + start(): void { + if (this.interval) return; + if (process.env.PACED_PUSH_ENABLED !== 'true') { + console.log('[PACER] disabled (set PACED_PUSH_ENABLED=true to activate)'); + return; + } + if (!process.env.GITHUB_TOKEN) { + console.log('[PACER] disabled (no GITHUB_TOKEN)'); + return; + } + console.log( + `[PACER] active — every ${Math.round(this.intervalMs / 60000)} min, ` + + `${this.batch} commit(s)/fire from ${this.branch} → ${this.target}`, + ); + // Fire once shortly after boot, then on the interval. + setTimeout(() => this.tick(), 30_000); + this.interval = setInterval(() => this.tick(), this.intervalMs); + } + + stop(): void { + if (this.interval) { + clearInterval(this.interval); + this.interval = null; + } + } + + private git(args: string[]): string { + try { + return execSync(['git', ...args].join(' '), { + cwd: this.repoRoot, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }).trim(); + } catch (err: any) { + // Surface stderr so the push failure reason is visible in logs. + const stderr = err?.stderr?.toString?.() || ''; + const stdout = err?.stdout?.toString?.() || ''; + const combined = [stderr, stdout].filter(Boolean).join(' ').trim(); + const wrapped = new Error( + `${err?.message || 'git failed'}${combined ? ' — ' + combined : ''}`, + ); + throw wrapped; + } + } + + private listForwardCommits(): string[] { + const ref = `${this.remote}/${this.target}..${this.remote}/${this.branch}`; + try { + const out = this.git(['rev-list', '--reverse', ref]); + return out ? out.split('\n').filter(Boolean) : []; + } catch (err: any) { + console.error(`[PACER] enumerate failed: ${err?.message || err}`); + return []; + } + } + + private readPointer(): number { + try { + const raw = fs.readFileSync(this.pointerFile, 'utf8').trim(); + const n = parseInt(raw, 10); + return Number.isFinite(n) && n >= 0 ? n : 0; + } catch { + return 0; + } + } + + private writePointer(n: number): void { + fs.mkdirSync(path.dirname(this.pointerFile), { recursive: true }); + fs.writeFileSync(this.pointerFile, `${n}\n`); + } + + private tick(): void { + try { + // Unshallow first — GitIntegration clones with --depth 50, so the + // intermediate commits we want to push aren't in the local object + // store yet. --unshallow is a no-op once the repo is already deep. + try { this.git(['fetch', '--unshallow', this.remote]); } + catch { /* already unshallow, ignore */ } + // Force-fetch all branches via explicit refspec — single-branch + // clones don't have origin/ tracking refs by default. + this.git(['fetch', this.remote, '+refs/heads/*:refs/remotes/' + this.remote + '/*']); + } catch (err: any) { + console.warn(`[PACER] fetch failed: ${err?.message || err}`); + return; + } + + const queue = this.listForwardCommits(); + if (queue.length === 0) { + console.log(`[PACER] nothing to push (target = branch tip)`); + return; + } + console.log(`[PACER] ${queue.length} commit(s) ahead; pushing up to ${this.batch}`); + + let pushed = 0; + for (let i = 0; i < this.batch && i < queue.length; i++) { + const sha = queue[i]; + try { + const subject = this.git(['log', '-1', '--pretty=%s', sha]); + this.git(['push', this.remote, `${sha}:refs/heads/${this.target}`]); + console.log(`[PACER] pushed ${sha.slice(0, 8)} ${subject}`); + pushed++; + } catch (err: any) { + console.error(`[PACER] push of ${sha} failed: ${err?.message || err}`); + break; + } + } + + if (pushed > 0) { + const before = this.readPointer(); + this.writePointer(before + pushed); + } + } +} diff --git a/backend/src/agent/leaderElection.ts b/backend/src/agent/leaderElection.ts new file mode 100644 index 00000000..8bc80e86 --- /dev/null +++ b/backend/src/agent/leaderElection.ts @@ -0,0 +1,108 @@ +import Redis from 'ioredis'; + +/** + * Worker leader election (TASK-332). + * + * Two worker replicas would otherwise both run AgentWorker and produce + * duplicate commits. We hold a Redis lease keyed by 'hermes:worker:leader' + * with TTL; the leader renews periodically (atomically, only if it still + * owns the lease). A follower keeps polling and takes over within ~30s + * if the leader vanishes. + */ + +const LEASE_KEY = 'hermes:worker:leader'; +const LEASE_TTL_MS = 30_000; +const RENEW_INTERVAL_MS = 10_000; + +// Atomic compare-and-renew: only EXPIRE if the value still matches. +const RENEW_SCRIPT = ` +if redis.call('get', KEYS[1]) == ARGV[1] then + return redis.call('pexpire', KEYS[1], ARGV[2]) +else + return 0 +end +`; + +const RELEASE_SCRIPT = ` +if redis.call('get', KEYS[1]) == ARGV[1] then + return redis.call('del', KEYS[1]) +else + return 0 +end +`; + +export interface LeaderHandle { + isLeader(): boolean; + release(): Promise; +} + +export async function startLeaderElection( + redis: Redis, + leaseId: string, + onAcquire: () => void, + onLose: () => void, +): Promise { + let owner = false; + let renewTimer: NodeJS.Timeout | null = null; + let pollTimer: NodeJS.Timeout | null = null; + + async function tryAcquire(): Promise { + const ok = await redis.set(LEASE_KEY, leaseId, 'PX', LEASE_TTL_MS, 'NX'); + return ok === 'OK'; + } + + async function renew(): Promise { + const result = await redis.eval(RENEW_SCRIPT, 1, LEASE_KEY, leaseId, String(LEASE_TTL_MS)); + return result === 1; + } + + async function loop() { + if (!owner) { + const got = await tryAcquire(); + if (got) { + owner = true; + console.log(`[LEADER] acquired lease as ${leaseId}`); + onAcquire(); + renewTimer = setInterval(async () => { + try { + const renewed = await renew(); + if (!renewed) { + owner = false; + if (renewTimer) clearInterval(renewTimer); + renewTimer = null; + console.log(`[LEADER] lost lease ${leaseId}`); + onLose(); + } + } catch (err: any) { + console.warn(`[LEADER] renew error: ${err?.message || err}`); + } + }, RENEW_INTERVAL_MS); + } + } + } + + // First attempt + repeat for follower mode. + await loop(); + pollTimer = setInterval(loop, LEASE_TTL_MS); + + if (!owner) { + console.log(`[LEADER] follower mode (${leaseId})`); + } + + return { + isLeader: () => owner, + async release() { + if (renewTimer) clearInterval(renewTimer); + if (pollTimer) clearInterval(pollTimer); + if (owner) { + try { + await redis.eval(RELEASE_SCRIPT, 1, LEASE_KEY, leaseId); + owner = false; + console.log(`[LEADER] released lease ${leaseId}`); + } catch (err: any) { + console.warn(`[LEADER] release error: ${err?.message || err}`); + } + } + }, + }; +} diff --git a/backend/src/api/build-info.ts b/backend/src/api/build-info.ts new file mode 100644 index 00000000..9f28db35 --- /dev/null +++ b/backend/src/api/build-info.ts @@ -0,0 +1,29 @@ +import { Router } from 'express'; +import { execSync } from 'child_process'; + +/** + * /api/build endpoint (TASK-150). Surfaces commit + build time + version + * for debugging deploys. Computed once at module load to avoid running + * git on every request. + */ + +function readGitCommit(): string { + if (process.env.HERMES_BUILD_COMMIT) return process.env.HERMES_BUILD_COMMIT; + try { + return execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim(); + } catch { + return 'unknown'; + } +} + +const COMMIT = readGitCommit(); +const BUILD_TIME = process.env.HERMES_BUILD_TIME || new Date().toISOString(); +const VERSION = process.env.npm_package_version || '0.0.0'; + +export function createBuildRouter(): Router { + const router = Router(); + router.get('/', (_req, res) => { + res.json({ commit: COMMIT, buildTime: BUILD_TIME, version: VERSION }); + }); + return router; +} diff --git a/backend/src/api/health.ts b/backend/src/api/health.ts new file mode 100644 index 00000000..c1370e43 --- /dev/null +++ b/backend/src/api/health.ts @@ -0,0 +1,90 @@ +import { Router } from 'express'; +import { db, cache } from '../database/db'; +import type { Chain } from '../blockchain/Chain'; + +/** + * Three-tier health checks (TASK-149). + * + * /health/live — process is up (no dependencies) + * /health/ready — can serve traffic (DB pool capacity, Redis up, chain bootstrapped) + * /health/deep — exercises full read paths with timing (slower) + * + * Shutdown handler (existing) flips a flag to make /live also return 503 + * during drain so load balancers stop routing immediately. + */ + +let isShuttingDown = false; + +export function markShuttingDown(): void { + isShuttingDown = true; +} + +export function createHealthRouter(chain: Chain): Router { + const router = Router(); + + router.get('/live', (_req, res) => { + if (isShuttingDown) { + return res.status(503).json({ status: 'draining' }); + } + res.status(200).json({ status: 'live' }); + }); + + router.get('/ready', async (_req, res) => { + if (isShuttingDown) { + return res.status(503).json({ ready: false, failures: [{ check: 'shutdown', reason: 'draining' }] }); + } + const failures: Array<{ check: string; reason: string }> = []; + const stats = db.poolStats(); + if (stats.max > 0 && stats.total >= stats.max && stats.idle === 0) { + failures.push({ check: 'pg_pool', reason: 'no idle connections' }); + } + if (process.env.REDIS_URL && !cache.isConnected()) { + failures.push({ check: 'redis', reason: 'disconnected' }); + } + if (chain.getChainLength() === 0) { + failures.push({ check: 'chain', reason: 'genesis not loaded' }); + } + if (failures.length > 0) { + return res.status(503).json({ ready: false, failures }); + } + res.json({ ready: true, checks: { pg: 'ok', redis: process.env.REDIS_URL ? 'ok' : 'skipped', chain: 'ok' } }); + }); + + router.get('/deep', async (_req, res) => { + const failures: Array<{ check: string; reason: string }> = []; + const latency: Record = {}; + + let t = Date.now(); + try { + await db.query(`SELECT 1`); + latency.db = Date.now() - t; + } catch (err: any) { + failures.push({ check: 'db', reason: err?.message || 'query failed' }); + } + + if (process.env.REDIS_URL) { + t = Date.now(); + try { + await cache.get('health:deep:probe'); + latency.redis = Date.now() - t; + } catch (err: any) { + failures.push({ check: 'redis', reason: err?.message || 'get failed' }); + } + } + + t = Date.now(); + try { + chain.getLatestBlock(); + latency.chainHead = Date.now() - t; + } catch (err: any) { + failures.push({ check: 'chainHead', reason: err?.message || 'fetch failed' }); + } + + if (failures.length > 0) { + return res.status(503).json({ ready: false, failures, latencyMs: latency }); + } + res.json({ ready: true, latencyMs: latency }); + }); + + return router; +} diff --git a/backend/src/api/jsonrpc.ts b/backend/src/api/jsonrpc.ts new file mode 100644 index 00000000..0090e062 --- /dev/null +++ b/backend/src/api/jsonrpc.ts @@ -0,0 +1,57 @@ +import { Router } from 'express'; +import type { Chain } from '../blockchain/Chain'; +import { stateManager } from '../blockchain/StateManager'; + +/** + * Minimal Ethereum JSON-RPC compat layer (TASK-176). + * + * Mounted at POST /rpc. MetaMask + eth-tools that expect this protocol + * can read chain height + balance. Methods beyond this minimal set + * return -32601 (method not found). + * + * Methods implemented: + * eth_blockNumber → 0xN + * eth_getBalance(addr, _tag) → 0xN + * eth_chainId → 0x... (HERMES_CHAIN_ID env or default) + * net_version → string of chain id + */ +export function createJsonRpcRouter(chain: Chain): Router { + const router = Router(); + + router.post('/', (req, res) => { + const { id, method, params } = req.body || {}; + const respond = (result?: any, error?: { code: number; message: string }) => + res.json({ jsonrpc: '2.0', id: id ?? null, ...(error ? { error } : { result }) }); + + const chainId = process.env.HERMES_CHAIN_ID || '0x1f407'; + + try { + switch (method) { + case 'eth_blockNumber': + return respond('0x' + chain.getChainLength().toString(16)); + case 'eth_chainId': + return respond(chainId); + case 'net_version': + return respond(String(parseInt(chainId, 16))); + case 'eth_getBalance': { + const addr = (params && params[0]) || ''; + if (!addr) return respond(undefined, { code: -32602, message: 'invalid address' }); + const bal = stateManager.getBalance(addr); + return respond('0x' + bal.toString(16)); + } + case 'eth_getTransactionCount': { + const addr = (params && params[0]) || ''; + if (!addr) return respond(undefined, { code: -32602, message: 'invalid address' }); + const nonce = stateManager.getNonce(addr); + return respond('0x' + nonce.toString(16)); + } + default: + return respond(undefined, { code: -32601, message: `method not found: ${method}` }); + } + } catch (err: any) { + return respond(undefined, { code: -32603, message: err?.message || 'internal error' }); + } + }); + + return router; +} diff --git a/backend/src/api/metrics.ts b/backend/src/api/metrics.ts new file mode 100644 index 00000000..b5843efc --- /dev/null +++ b/backend/src/api/metrics.ts @@ -0,0 +1,75 @@ +import { Router } from 'express'; +import { db } from '../database/db'; +import { getHistogram } from '../database/queryMetrics'; +import type { Chain } from '../blockchain/Chain'; +import type { TransactionPool } from '../blockchain/TransactionPool'; +import { peerRegistry } from '../network/PeerRegistry'; + +/** + * Prometheus exposition (TASK-152). Plain-text format consumed by + * Prometheus, Datadog (with the prom integration), Grafana cloud, etc. + * + * GET /api/metrics → text/plain; version=0.0.4 + * + * Metrics: + * hermes_chain_height + * hermes_mempool_size + * hermes_pg_pool_{total,idle,waiting,max} + * hermes_pg_query_count + * hermes_pg_query_errors_total + * hermes_pg_query_duration_ms_bucket{le=...} + * hermes_pg_query_duration_ms_sum + * hermes_pg_query_duration_ms_count + * hermes_peers_active + */ +export function createMetricsRouter(chain: Chain, txPool: TransactionPool): Router { + const router = Router(); + + router.get('/', (_req, res) => { + const lines: string[] = []; + + function gauge(name: string, value: number, help: string): void { + lines.push(`# HELP ${name} ${help}`); + lines.push(`# TYPE ${name} gauge`); + lines.push(`${name} ${value}`); + } + + function counter(name: string, value: number, help: string): void { + lines.push(`# HELP ${name} ${help}`); + lines.push(`# TYPE ${name} counter`); + lines.push(`${name} ${value}`); + } + + // Chain + gauge('hermes_chain_height', chain.getChainLength(), 'Synthetic chain height'); + gauge('hermes_mempool_size', txPool.getPendingCount(), 'Pending tx count'); + + // PG pool + const pool = db.poolStats(); + gauge('hermes_pg_pool_total', pool.total, 'Total PG connections'); + gauge('hermes_pg_pool_idle', pool.idle, 'Idle PG connections'); + gauge('hermes_pg_pool_waiting', pool.waiting, 'Clients waiting for a PG connection'); + gauge('hermes_pg_pool_max', pool.max, 'Max PG connections'); + + // PG query histogram + const h = getHistogram(); + counter('hermes_pg_query_count', h.count, 'Total PG queries observed'); + counter('hermes_pg_query_errors_total', h.errors, 'Total PG query errors'); + lines.push('# HELP hermes_pg_query_duration_ms PG query latency in ms'); + lines.push('# TYPE hermes_pg_query_duration_ms histogram'); + for (let i = 0; i < h.buckets.length; i++) { + lines.push(`hermes_pg_query_duration_ms_bucket{le="${h.buckets[i]}"} ${h.cumulativeCounts[i]}`); + } + lines.push(`hermes_pg_query_duration_ms_bucket{le="+Inf"} ${h.count}`); + lines.push(`hermes_pg_query_duration_ms_sum ${h.sum}`); + lines.push(`hermes_pg_query_duration_ms_count ${h.count}`); + + // Peers + gauge('hermes_peers_active', peerRegistry.listPeers().length, 'Active peers (last_seen within 180s)'); + + res.set('Content-Type', 'text/plain; version=0.0.4'); + res.send(lines.join('\n') + '\n'); + }); + + return router; +} diff --git a/backend/src/api/middleware/accessLog.ts b/backend/src/api/middleware/accessLog.ts new file mode 100644 index 00000000..dd1028cc --- /dev/null +++ b/backend/src/api/middleware/accessLog.ts @@ -0,0 +1,38 @@ +import type { Request, Response, NextFunction } from 'express'; + +/** + * NDJSON access log + slow-request log (TASK-147 + TASK-148). + * + * Emits one JSON-line per request to stdout: ts, method, path, status, + * durationMs, bytes, requestId, ip. Easily fed into Logflare/Datadog/Axiom. + * + * Requests over SLOW_REQUEST_MS (default 1000) get tagged `slow:true` and + * also emit a console.warn so they surface in noisy log streams. + */ + +const SLOW_REQUEST_MS = Math.max(1, Number(process.env.SLOW_REQUEST_MS || '1000')); + +export function accessLog(req: Request, res: Response, next: NextFunction): void { + const startedAt = Date.now(); + res.on('finish', () => { + const durationMs = Date.now() - startedAt; + const slow = durationMs > SLOW_REQUEST_MS; + const line = { + ts: new Date().toISOString(), + method: req.method, + path: req.originalUrl, + status: res.statusCode, + durationMs, + bytes: Number(res.getHeader('content-length') ?? 0), + requestId: (req as any).id, + ip: req.ip, + slow, + }; + if (slow) { + console.warn('[ACCESS SLOW]', JSON.stringify(line)); + } else { + console.log(JSON.stringify(line)); + } + }); + next(); +} diff --git a/backend/src/api/middleware/httpsRedirect.ts b/backend/src/api/middleware/httpsRedirect.ts new file mode 100644 index 00000000..7d5ce163 --- /dev/null +++ b/backend/src/api/middleware/httpsRedirect.ts @@ -0,0 +1,18 @@ +import type { Request, Response, NextFunction } from 'express'; + +/** + * HTTPS-only redirect middleware (TASK-360). + * + * In production behind a load balancer (Railway sets x-forwarded-proto), + * issue a 301 to the https equivalent if the request came in over http. + * No-op in development. + */ +export function httpsRedirect(req: Request, res: Response, next: NextFunction): void { + if (process.env.NODE_ENV !== 'production') return next(); + const proto = req.headers['x-forwarded-proto']; + if (proto && proto !== 'https') { + const host = req.headers.host || ''; + return res.redirect(301, `https://${host}${req.originalUrl}`); + } + next(); +} diff --git a/backend/src/api/middleware/requestId.ts b/backend/src/api/middleware/requestId.ts new file mode 100644 index 00000000..d860957a --- /dev/null +++ b/backend/src/api/middleware/requestId.ts @@ -0,0 +1,27 @@ +import type { Request, Response, NextFunction } from 'express'; +import { randomUUID } from 'crypto'; + +/** + * Request-ID middleware (TASK-146). Echoes incoming X-Request-ID or + * generates one. Available downstream as `req.id` (typed via module + * augmentation below) and on every response as X-Request-ID for client + * correlation in support tickets. + */ +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Express { + interface Request { + id: string; + } + } +} + +export function requestId(req: Request, res: Response, next: NextFunction): void { + const incoming = req.headers['x-request-id']; + const id = typeof incoming === 'string' && incoming.length > 0 && incoming.length <= 128 + ? incoming + : randomUUID(); + req.id = id; + res.setHeader('X-Request-ID', id); + next(); +} diff --git a/backend/src/api/middleware/sentry.ts b/backend/src/api/middleware/sentry.ts new file mode 100644 index 00000000..418a9494 --- /dev/null +++ b/backend/src/api/middleware/sentry.ts @@ -0,0 +1,49 @@ +import type { Express, Request, Response, NextFunction } from 'express'; + +/** + * Backend Sentry integration (TASK-444). + * + * Lazy-init: only requires + initializes @sentry/node when SENTRY_DSN is + * set, so the dep is optional. Without DSN, all helpers no-op. + * + * Mount the request handler before routes and the error handler last. + */ + +let initialized = false; +let SentryRef: any = null; + +function maybeInit(): void { + if (initialized) return; + if (!process.env.SENTRY_DSN) { initialized = true; return; } + try { + SentryRef = require('@sentry/node'); + SentryRef.init({ + dsn: process.env.SENTRY_DSN, + environment: process.env.NODE_ENV || 'development', + release: process.env.HERMES_BUILD_COMMIT, + tracesSampleRate: Number(process.env.SENTRY_TRACES_SAMPLE_RATE || '0.1'), + }); + console.log('[SENTRY] initialized'); + } catch (err: any) { + console.warn(`[SENTRY] init skipped (missing @sentry/node?): ${err?.message || err}`); + } + initialized = true; +} + +export function attachSentryRequestHandler(app: Express): void { + maybeInit(); + if (!SentryRef) return; + app.use(SentryRef.Handlers.requestHandler()); +} + +export function attachSentryErrorHandler(app: Express): void { + maybeInit(); + if (!SentryRef) return; + app.use(SentryRef.Handlers.errorHandler()); +} + +export function captureException(err: any, ctx?: Record): void { + maybeInit(); + if (!SentryRef) return; + SentryRef.captureException(err, ctx ? { extra: ctx } : undefined); +} diff --git a/backend/src/api/newsletter.ts b/backend/src/api/newsletter.ts new file mode 100644 index 00000000..328e54b8 --- /dev/null +++ b/backend/src/api/newsletter.ts @@ -0,0 +1,54 @@ +import { Router } from 'express'; +import { db } from '../database/db'; + +/** + * Newsletter signup (TASK-486). + * + * POST /api/newsletter — { email, source? } — store + ACK + * POST /api/newsletter/unsubscribe — { email } + * + * Storage only. No actual email send (forward to Mailchimp/Buttondown + * out-of-band via cron or webhook later). Email field validated as + * a basic shape; no MX check. + */ + +const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +export function createNewsletterRouter(): Router { + const router = Router(); + + router.post('/', async (req, res) => { + const email = String(req.body?.email || '').trim().toLowerCase(); + const source = String(req.body?.source || 'web').slice(0, 64); + if (!EMAIL_RE.test(email) || email.length > 320) { + return res.status(400).json({ error: 'invalid email' }); + } + try { + await db.query( + `INSERT INTO newsletter_subscribers (email, source) + VALUES ($1, $2) + ON CONFLICT (email) DO UPDATE SET unsubscribed_at = NULL`, + [email, source], + ); + res.json({ subscribed: true }); + } catch (err: any) { + res.status(500).json({ error: err?.message || 'store failed' }); + } + }); + + router.post('/unsubscribe', async (req, res) => { + const email = String(req.body?.email || '').trim().toLowerCase(); + if (!EMAIL_RE.test(email)) return res.status(400).json({ error: 'invalid email' }); + try { + await db.query( + `UPDATE newsletter_subscribers SET unsubscribed_at = CURRENT_TIMESTAMP WHERE email = $1`, + [email], + ); + res.json({ unsubscribed: true }); + } catch (err: any) { + res.status(500).json({ error: err?.message || 'update failed' }); + } + }); + + return router; +} diff --git a/backend/src/api/openapi.ts b/backend/src/api/openapi.ts new file mode 100644 index 00000000..185e3641 --- /dev/null +++ b/backend/src/api/openapi.ts @@ -0,0 +1,85 @@ +import { Router } from 'express'; + +/** + * OpenAPI 3.1 spec for /api/openapi.json (TASK-141). + * + * Hand-curated rather than auto-generated — express-route enumeration via + * `_router.stack` is fragile across versions. The cost is keeping this in + * sync; the benefit is a stable, reviewable schema. Swagger UI (TASK-142) + * mounts this spec at /docs. + */ +const SPEC = { + openapi: '3.1.0', + info: { + title: 'Hermeschain API', + version: '0.3.0', + description: 'Public REST API for Hermeschain.', + }, + servers: [{ url: 'https://hermeschain.io' }], + paths: { + '/api/status': { + get: { + summary: 'Snapshot of chain + service status', + responses: { '200': { description: 'OK' } }, + }, + }, + '/api/blocks/{height}': { + get: { + summary: 'Get a block by height', + parameters: [{ name: 'height', in: 'path', required: true, schema: { type: 'integer' } }, + { name: 'include', in: 'query', required: false, schema: { type: 'string', enum: ['receipts'] } }], + responses: { '200': { description: 'OK' }, '404': { description: 'Not found' } }, + }, + }, + '/api/blocks/search': { + get: { + summary: 'Search blocks by height range + producer (TASK-153)', + parameters: [ + { name: 'from', in: 'query', schema: { type: 'integer' } }, + { name: 'to', in: 'query', schema: { type: 'integer' } }, + { name: 'producer', in: 'query', schema: { type: 'string' } }, + ], + responses: { '200': { description: 'OK' } }, + }, + }, + '/api/tx/{hash}': { + get: { + summary: 'Get a transaction by hash (with optional decoded logs)', + parameters: [{ name: 'hash', in: 'path', required: true, schema: { type: 'string' } }, + { name: 'decodeLogs', in: 'query', schema: { type: 'boolean' } }], + responses: { '200': { description: 'OK' }, '404': { description: 'Not found' } }, + }, + }, + '/api/account/{addr}': { + get: { summary: 'Get account balance + nonce', responses: { '200': { description: 'OK' } } }, + }, + '/api/account/{addr}/next-nonce': { + get: { summary: 'Suggested next nonce (TASK-057)', responses: { '200': { description: 'OK' } } }, + }, + '/api/transactions': { + post: { summary: 'Submit a signed tx (TASK-170 idempotent on hash)', responses: { '200': { description: 'OK' }, '400': { description: 'Invalid' } } }, + }, + '/api/mempool': { get: { summary: 'Pending txs (TASK-166)', responses: { '200': { description: 'OK' } } } }, + '/api/mempool/{hash}': { get: { summary: 'Single pending tx (TASK-167)', responses: { '200': { description: 'OK' }, '404': { description: 'Not in mempool' } } } }, + '/api/chain/tps': { get: { summary: 'TPS over a window (TASK-051)', responses: { '200': { description: 'OK' } } } }, + '/api/wallet/validate/{input}': { get: { summary: 'Address validity (TASK-137)', responses: { '200': { description: 'OK' } } } }, + '/api/mesh/peers': { get: { summary: 'Active peer list', responses: { '200': { description: 'OK' } } } }, + '/api/mesh/head': { get: { summary: 'Our chain head', responses: { '200': { description: 'OK' } } } }, + '/api/mesh/headers': { get: { summary: 'Header range fetch (TASK-003)', responses: { '200': { description: 'OK' } } } }, + '/api/mesh/blocks': { get: { summary: 'Bulk block fetch (TASK-004)', responses: { '200': { description: 'OK' } } } }, + '/api/mesh/announce': { post: { summary: 'Register/heartbeat as a peer', responses: { '200': { description: 'OK' } } } }, + '/api/mesh/block': { post: { summary: 'Gossip a block (TASK-002)', responses: { '200': { description: 'Accepted' }, '400': { description: 'Bad payload' }, '409': { description: 'Rejected' } } } }, + '/api/metrics': { get: { summary: 'Prometheus exposition (TASK-152)', responses: { '200': { description: 'OK' } } } }, + '/api/build': { get: { summary: 'Build info (TASK-150)', responses: { '200': { description: 'OK' } } } }, + '/api/newsletter': { post: { summary: 'Newsletter subscribe (TASK-486)', responses: { '200': { description: 'OK' }, '400': { description: 'Bad email' } } } }, + '/health/live': { get: { summary: 'Process up (TASK-149)', responses: { '200': { description: 'live' }, '503': { description: 'draining' } } } }, + '/health/ready': { get: { summary: 'Ready to serve (TASK-149)', responses: { '200': { description: 'ready' }, '503': { description: 'failures' } } } }, + '/health/deep': { get: { summary: 'Deep read-path probe (TASK-149)', responses: { '200': { description: 'ready' }, '503': { description: 'failures' } } } }, + }, +}; + +export function createOpenApiRouter(): Router { + const router = Router(); + router.get('/', (_req, res) => res.json(SPEC)); + return router; +} diff --git a/backend/src/api/server.ts b/backend/src/api/server.ts index 86a21d59..b05f3078 100644 --- a/backend/src/api/server.ts +++ b/backend/src/api/server.ts @@ -48,6 +48,17 @@ async function main() { throw err; } console.log('[DB] PostgreSQL database ready\n'); + + // Pre-warm Redis with hot reads (TASK-328). Off in dev unless + // explicitly enabled; on by default in production. + const warmEnabled = process.env.CACHE_WARMER_ENABLED ?? + (process.env.NODE_ENV === 'production' ? 'true' : 'false'); + if (warmEnabled === 'true') { + const { warmCache } = await import('../database/cacheWarmer'); + warmCache().catch((err) => { + console.warn('[CACHE WARMER] failed (non-fatal):', err?.message || err); + }); + } } else { console.log('[DB] Running without persistent database\n'); } @@ -57,6 +68,14 @@ async function main() { } const eventBus = EventBus.getInstance(); + + // Cross-replica event bridge (TASK-330). Off when REDIS_URL absent or + // explicitly disabled. Replicas converge on the same SSE stream. + if (process.env.REDIS_URL && process.env.REDIS_BRIDGE_ENABLED !== 'false') { + const { attachRedisBridge } = await import('../events/RedisBridge'); + attachRedisBridge(eventBus, process.env.REDIS_URL); + } + const chain = new Chain(); const txPool = new TransactionPool(); const validatorManager = new ValidatorManager(); @@ -77,8 +96,33 @@ async function main() { console.log(` Circulating: ${stateManager.formatBalance(stateManager.getCirculatingSupply())}\n`); const app = express(); - app.use(cors()); - app.use(express.json()); + // CORS allowlist via env (TASK-145). Comma-separated origins; default open. + const corsOrigins = (process.env.CORS_ORIGINS || '').split(',').map((s) => s.trim()).filter(Boolean); + if (corsOrigins.length > 0) { + app.use(cors({ + origin: (origin, cb) => { + if (!origin || corsOrigins.includes(origin)) return cb(null, true); + cb(new Error(`origin ${origin} not allowed by CORS`)); + }, + credentials: true, + })); + console.log(`[CORS] allowlist active (${corsOrigins.length} origin(s))`); + } else { + app.use(cors()); + } + // Cap JSON body to 1MB (TASK-340) — DoS defense. + app.use(express.json({ limit: process.env.JSON_BODY_LIMIT || '1mb' })); + + // HTTPS-only redirect (TASK-360) — production only. + const { httpsRedirect } = await import('./middleware/httpsRedirect'); + app.use(httpsRedirect); + + // Request observability middleware (TASK-146 + TASK-147 + TASK-148). + // Order matters: requestId before accessLog so the log line has a value. + const { requestId } = await import('./middleware/requestId'); + const { accessLog } = await import('./middleware/accessLog'); + app.use(requestId); + if (process.env.ACCESS_LOG_ENABLED !== 'false') app.use(accessLog); const syncSharedReadState = async () => { if (process.env.AGENT_ROLE === 'worker') return; @@ -98,6 +142,94 @@ async function main() { res.status(200).json({ status: 'ok' }); }); + // Contract bytecode disasm (TASK-099). Returns parsed JSON-op program + // for an on-chain contract. 404 if no code at the address. + app.get('/api/contract/:addr/disasm', async (req, res) => { + try { + const r = await db.query( + `SELECT bytecode, code_hash, deployed_at_block, deployed_by + FROM contract_code WHERE address = $1`, + [req.params.addr], + ); + if (r.rows.length === 0) return res.status(404).json({ error: 'no code at address' }); + const row = r.rows[0]; + let ops: any = null; + try { ops = JSON.parse(row.bytecode); } catch { /* leave null */ } + res.json({ + address: req.params.addr, + codeHash: row.code_hash, + deployedAtBlock: Number(row.deployed_at_block), + deployedBy: row.deployed_by, + ops, + bytecodeRaw: row.bytecode, + }); + } catch (err: any) { + res.status(500).json({ error: err?.message || 'lookup failed' }); + } + }); + + // Contract storage browser (TASK-100). + app.get('/api/contract/:addr/storage', async (req, res) => { + try { + const limit = Math.min(500, Math.max(1, Number(req.query.limit ?? 100))); + const prefix = typeof req.query.prefix === 'string' ? req.query.prefix : ''; + const r = await db.query( + `SELECT storage_key, storage_value, updated_at_block + FROM contract_storage + WHERE contract_address = $1 AND storage_key LIKE $2 + ORDER BY storage_key + LIMIT $3`, + [req.params.addr, prefix + '%', limit], + ); + res.json({ + address: req.params.addr, + items: r.rows.map((row: any) => ({ + key: row.storage_key, + value: row.storage_value, + updatedAtBlock: Number(row.updated_at_block), + })), + count: r.rows.length, + }); + } catch (err: any) { + res.status(500).json({ error: err?.message || 'lookup failed' }); + } + }); + + // Bulk chain export as NDJSON (TASK-028). Streamed line-by-line to + // avoid buffering huge ranges. Each line is { type: 'block', ...toJSON() }. + app.get('/api/chain/export', async (req, res) => { + const from = Math.max(0, Number(req.query.from ?? 0)); + const to = Math.min(from + 1_000_000, Number(req.query.to ?? from + 1000)); + res.setHeader('Content-Type', 'application/x-ndjson'); + for (let h = from; h <= to; h++) { + const block = chain.getBlockByHeight(h); + if (!block) continue; + res.write(JSON.stringify({ type: 'block', ...block.toJSON() }) + '\n'); + } + res.end(); + }); + + // Ethereum JSON-RPC compat (TASK-176). Minimum viable set so MetaMask + // and eth-tools can connect for read-only ops. + const { createJsonRpcRouter } = await import('./jsonrpc'); + app.use('/rpc', createJsonRpcRouter(chain)); + + // OpenAPI 3.1 spec at /api/openapi.json (TASK-141). Swagger UI hookup in TASK-142. + const { createOpenApiRouter } = await import('./openapi'); + app.use('/api/openapi.json', createOpenApiRouter()); + + // Newsletter signup (TASK-486). + const { createNewsletterRouter } = await import('./newsletter'); + app.use('/api/newsletter', createNewsletterRouter()); + + // Three-tier health checks (TASK-149), build info (TASK-150), Prometheus metrics (TASK-152). + const { createHealthRouter } = await import('./health'); + const { createBuildRouter } = await import('./build-info'); + const { createMetricsRouter } = await import('./metrics'); + app.use('/health', createHealthRouter(chain)); + app.use('/api/build', createBuildRouter()); + app.use('/api/metrics', createMetricsRouter(chain, txPool)); + // API status check (no key exposure) app.get('/api/config/status', (req, res) => { const role = process.env.AGENT_ROLE === 'worker' ? 'worker' : 'web'; @@ -318,12 +450,24 @@ async function main() { app.post('/api/transactions', async (req, res) => { try { - const { from, to, value, gasPrice, gasLimit, nonce, data, signature } = req.body; - - // Generate Solana-style base58 transaction hash + const { from, to, value, gasPrice, gasLimit, nonce, data, signature, hash } = req.body; + + // TASK-170 — idempotent submit. If client supplies a hash and we + // already have it (pending or mined), short-circuit with success. + if (hash) { + const pending = await txPool.getPendingTransactions(10_000); + if (pending.find((t) => t.hash === hash)) { + return res.json({ success: true, hash, idempotent: 'pending' }); + } + // Note: can't easily check mined without a per-tx index; the new + // /api/tx/:hash endpoint will 404 for unknown so reusing this + // path is safe enough. + } + + // Generate Solana-style base58 transaction hash if not supplied. const BASE58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; - const txHash = Array.from({length: 44}, () => BASE58[Math.floor(Math.random() * 58)]).join(''); - + const txHash = hash || Array.from({length: 44}, () => BASE58[Math.floor(Math.random() * 58)]).join(''); + const tx = { hash: txHash, from, @@ -335,9 +479,9 @@ async function main() { data, signature }; - + const added = await txPool.addTransaction(tx); - + if (added) { eventBus.emit('transaction_added', tx); res.json({ success: true, hash: tx.hash }); @@ -729,6 +873,19 @@ Live context: await agentTaskStore.initialize(); await agentRuntimeStore.initialize(); + const { githubUpdates } = await import('../agent/GitHubUpdates'); + await githubUpdates.initialize(agentConfig.repoRoot); + const shouldRunGitHubSync = + agentConfig.role === 'worker' || !process.env.WORKER_INTERNAL_URL; + if (shouldRunGitHubSync) { + githubUpdates.startBackgroundSync(); + } + const { githubUpdatesRouter } = await import('./githubUpdates'); + app.use('/api/github/updates', githubUpdatesRouter); + console.log( + `[GITHUB] Updates center ready (${shouldRunGitHubSync ? 'active sync' : 'cache reader'})` + ); + if (agentConfig.role === 'worker') { await startNetworkHeartbeat(); } else { @@ -861,9 +1018,13 @@ Live context: await syncSharedReadState(); const state = agentWorker.getState(); const sharedRuntime = agentRuntimeStore.getLatestSnapshot(); - const observedRuntime = - agentConfig.role === 'web' && sharedRuntime + const sharedWorkerRuntime = + agentConfig.role === 'web' && sharedRuntime?.role === 'worker' ? sharedRuntime + : null; + const observedRuntime = + sharedWorkerRuntime + ? sharedWorkerRuntime : { role: agentConfig.role, mode: state.mode, @@ -900,7 +1061,9 @@ Live context: agentConfig.role === 'worker' ? new Date().toISOString() : null, }; const workerHeartbeatAt = - observedRuntime.workerHeartbeatAt || observedRuntime.updatedAt || null; + observedRuntime.role === 'worker' + ? observedRuntime.workerHeartbeatAt || observedRuntime.updatedAt || null + : null; const workerActive = workerHeartbeatAt ? Date.now() - new Date(workerHeartbeatAt as string | number | Date).getTime() < 90_000 : false; @@ -912,7 +1075,7 @@ Live context: role: agentConfig.role, serviceRole: agentConfig.role, observedWorkerRole: observedRuntime.role, - statusSource: agentConfig.role === 'web' && sharedRuntime ? 'shared' : 'local', + statusSource: sharedWorkerRuntime ? 'shared' : 'local', mode: observedRuntime.mode, streamMode: observedRuntime.mode, isWorking: observedRuntime.isWorking, @@ -966,9 +1129,162 @@ Live context: tokenSpend: tokenBudget.snapshot(), }; }; + + const WORKER_INTERNAL_URL = + process.env.WORKER_INTERNAL_URL || + 'https://hermeschain-worker-production.up.railway.app'; + + const mergeWorkerStatusIntoPayload = (localPayload: any, workerData: any) => ({ + ...localPayload, + mode: workerData.agentMode || localPayload.mode, + streamMode: workerData.agentMode || localPayload.streamMode, + runStatus: workerData.runStatus || localPayload.runStatus, + verificationStatus: + workerData.verificationStatus || localPayload.verificationStatus, + isWorking: workerData.isWorking ?? localPayload.isWorking, + currentTask: workerData.currentTask || localPayload.currentTask, + lastFailure: workerData.lastFailure || localPayload.lastFailure, + blockedReason: workerData.blockedReason || localPayload.blockedReason, + recentTasks: (workerData.recentSuccessful || []).slice(0, 5).map( + (run: any) => ({ + title: run.title, + agent: run.agent || 'HERMES', + completedAt: run.completedAt, + }) + ), + recentRuns: workerData.recentRuns || [], + completedTaskCount: (workerData.recentSuccessful || []).length, + agentEnabled: true, + capabilities: workerData.capabilities || localPayload.capabilities, + blockHeight: workerData.blockHeight ?? localPayload.blockHeight, + workerActive: true, + }); + + const buildLiveAgentStatusPayload = async (options?: { logProxyErrors?: boolean }) => { + const localPayload = await buildAgentStatusPayload(); + if (process.env.AGENT_ROLE === 'worker') { + return localPayload; + } + + const shouldProxyWorkerStatus = + localPayload.statusSource !== 'shared' || + !localPayload.workerActive || + localPayload.mode === 'disabled'; + + if (!shouldProxyWorkerStatus) { + return localPayload; + } + + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 4000); + const upstream = await fetch(`${WORKER_INTERNAL_URL}/status`, { + signal: controller.signal, + }); + clearTimeout(timeout); + + if (!upstream.ok) { + return localPayload; + } + + const workerData = await upstream.json(); + return mergeWorkerStatusIntoPayload(localPayload, workerData); + } catch (error: any) { + if (options?.logProxyErrors) { + console.error('[PROXY] worker /status fetch failed:', error?.message || error); + } + return localPayload; + } + }; + + interface AgentLogCursor { + id: string; + timestamp: Date; + } + + const getAgentLogCursor = (row: { id: string; timestamp: string | Date }) => ({ + id: row.id, + timestamp: new Date(row.timestamp), + }); + + const compareAgentLogCursor = (a: AgentLogCursor, b: AgentLogCursor) => { + const delta = a.timestamp.getTime() - b.timestamp.getTime(); + if (delta !== 0) return delta; + return a.id.localeCompare(b.id); + }; + + const fetchRecentAgentStreamRows = async (limit: number = 30) => { + const result = await db.query( + ` + SELECT id, timestamp, type, content, metadata + FROM agent_logs + WHERE type IN ( + 'task_start', + 'analysis_start', + 'tool_use', + 'tool_result', + 'verification_start', + 'verification_result', + 'task_complete', + 'task_blocked', + 'git_commit', + 'error', + 'output' + ) + ORDER BY timestamp DESC, id DESC + LIMIT $1 + `, + [limit] + ); + + return (result.rows || []).reverse(); + }; + + const fetchAgentStreamRowsAfter = async ( + cursor: AgentLogCursor, + limit: number = 50 + ) => { + const result = await db.query( + ` + SELECT id, timestamp, type, content, metadata + FROM agent_logs + WHERE ( + type IN ( + 'task_start', + 'analysis_start', + 'tool_use', + 'tool_result', + 'verification_start', + 'verification_result', + 'task_complete', + 'task_blocked', + 'git_commit', + 'error', + 'output' + ) + ) + AND (timestamp > $1 OR (timestamp = $1 AND id > $2)) + ORDER BY timestamp ASC, id ASC + LIMIT $3 + `, + [cursor.timestamp, cursor.id, limit] + ); + + return result.rows || []; + }; // SSE endpoint for live agent work streaming app.get('/api/agent/stream', (req, res) => { + // SSE replica pinning (TASK-331). With multiple web replicas behind a + // load balancer, we don't get sticky sessions for free; instead, only + // the replica with SSE_REPLICA=true serves the stream. Others 503 with + // X-SSE-Failover so clients can retry against a different host. + if (process.env.SSE_REPLICA === 'false' || + (process.env.SSE_REPLICA === undefined && process.env.SSE_REPLICA_STRICT === 'true')) { + res.setHeader('X-SSE-Failover', 'true'); + res.status(503).json({ error: 'sse-replica-not-here' }); + return; + } // Set up SSE headers res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); @@ -979,13 +1295,42 @@ Live context: agentViewerCount++; console.log(`[AGENT] New viewer connected (total: ${agentViewerCount})`); - void buildAgentStatusPayload().then((payload) => { - res.write(`data: ${JSON.stringify({ - type: 'init', - data: payload, - timestamp: Date.now() - })}\n\n`); - }).catch(() => { + const streamStartedAt = new Date(); + let lastSharedLogCursor: AgentLogCursor = { + id: '', + timestamp: streamStartedAt, + }; + + void buildLiveAgentStatusPayload() + .then(async (payload) => { + res.write(`data: ${JSON.stringify({ + type: 'init', + data: payload, + timestamp: Date.now() + })}\n\n`); + + try { + const recentRows = await fetchRecentAgentStreamRows(30); + if (recentRows.length > 0) { + lastSharedLogCursor = getAgentLogCursor(recentRows[recentRows.length - 1]); + } + + for (const row of recentRows) { + res.write( + `data: ${JSON.stringify({ + type: 'text', + data: formatLogStreamText(row), + viewerCount: agentViewerCount, + timestamp: Date.now(), + })}\n\n` + ); + } + } catch { + // Shared history is best-effort. The live stream should still + // connect even if historical hydration is unavailable. + } + }) + .catch(() => { res.write(`data: ${JSON.stringify({ type: 'init', data: { @@ -1005,10 +1350,8 @@ Live context: }; agentEvents.on('chunk', onChunk); - let lastLogSeenAt = new Date(Date.now() - 60 * 1000); - const statusPulse = setInterval(() => { - void buildAgentStatusPayload() + void buildLiveAgentStatusPayload() .then((payload) => { res.write(`data: ${JSON.stringify({ type: 'status', @@ -1026,19 +1369,13 @@ Live context: if (agentConfig.role !== 'web') return; try { - const result = await db.query( - ` - SELECT id, timestamp, type, content, metadata - FROM agent_logs - WHERE timestamp > $1 - ORDER BY timestamp ASC - LIMIT 50 - `, - [lastLogSeenAt] - ); + const rows = await fetchAgentStreamRowsAfter(lastSharedLogCursor, 50); - for (const row of result.rows || []) { - lastLogSeenAt = new Date(row.timestamp); + for (const row of rows) { + const nextCursor = getAgentLogCursor(row); + if (compareAgentLogCursor(nextCursor, lastSharedLogCursor) > 0) { + lastSharedLogCursor = nextCursor; + } res.write( `data: ${JSON.stringify({ type: 'text', @@ -1080,56 +1417,8 @@ Live context: // Worker is reachable over Railway's private network at the internal domain. // Prefer explicit WORKER_INTERNAL_URL; fall back to the public Railway // domain since private-network DNS+port assumptions aren't guaranteed. - const WORKER_INTERNAL_URL = - process.env.WORKER_INTERNAL_URL || - 'https://hermeschain-worker-production.up.railway.app'; - app.get('/api/agent/status', async (req, res) => { - if (process.env.AGENT_ROLE !== 'worker') { - try { - const controller = new AbortController(); - const t = setTimeout(() => controller.abort(), 4000); - const upstream = await fetch(`${WORKER_INTERNAL_URL}/status`, { - signal: controller.signal, - }); - clearTimeout(t); - if (upstream.ok) { - const workerData: any = await upstream.json(); - // Merge worker runtime state into the web's own payload shape so - // frontend components keep working without changes. - const localPayload = await buildAgentStatusPayload(); - res.json({ - ...localPayload, - mode: workerData.agentMode || localPayload.mode, - streamMode: workerData.agentMode || localPayload.streamMode, - runStatus: workerData.runStatus || localPayload.runStatus, - verificationStatus: - workerData.verificationStatus || localPayload.verificationStatus, - isWorking: workerData.isWorking ?? localPayload.isWorking, - currentTask: workerData.currentTask || localPayload.currentTask, - lastFailure: workerData.lastFailure || localPayload.lastFailure, - blockedReason: - workerData.blockedReason || localPayload.blockedReason, - recentTasks: (workerData.recentSuccessful || []).slice(0, 5).map( - (r: any) => ({ - title: r.title, - agent: r.agent || 'HERMES', - completedAt: r.completedAt, - }) - ), - recentRuns: workerData.recentRuns || [], - completedTaskCount: (workerData.recentSuccessful || []).length, - agentEnabled: true, - capabilities: workerData.capabilities || localPayload.capabilities, - blockHeight: workerData.blockHeight ?? localPayload.blockHeight, - }); - return; - } - } catch (e: any) { - console.error('[PROXY] worker /status fetch failed:', e?.message || e); - } - } - res.json(await buildAgentStatusPayload()); + res.json(await buildLiveAgentStatusPayload({ logProxyErrors: true })); }); // Get persisted task runs (plus legacy completed-task alias) @@ -1347,6 +1636,87 @@ Live context: }); }); + // Address validity checker (TASK-137). Returns parseable + reason. + app.get('/api/wallet/validate/:input', (req, res) => { + const input = req.params.input; + if (typeof input !== 'string' || input.length === 0) { + return res.json({ valid: false, reason: 'empty' }); + } + if (input.length < 32 || input.length > 64) { + return res.json({ valid: false, reason: 'wrong-length' }); + } + if (!/^[1-9A-HJ-NP-Za-km-z]+$/.test(input)) { + return res.json({ valid: false, reason: 'bad-base58' }); + } + res.json({ valid: true }); + }); + + // Block search by height range (TASK-153). + app.get('/api/blocks/search', async (req, res) => { + const from = Math.max(0, Number(req.query.from ?? 0)); + const to = Math.min(from + 1000, Number(req.query.to ?? from + 100)); + const producer = typeof req.query.producer === 'string' ? req.query.producer : null; + const items: any[] = []; + for (let h = from; h <= to; h++) { + const block = chain.getBlockByHeight(h); + if (!block) continue; + if (producer && block.header.producer !== producer) continue; + items.push({ + height: block.header.height, + hash: block.header.hash, + producer: block.header.producer, + timestamp: block.header.timestamp, + transactionCount: block.transactions.length, + }); + } + res.json({ items, count: items.length, from, to }); + }); + + // Mempool snapshot (TASK-166). + app.get('/api/mempool', async (req, res) => { + const limit = Math.min(500, Math.max(1, Number(req.query.limit ?? 200))); + const txs = await txPool.getPendingTransactions(limit); + res.json({ + pending: txs.length, + items: txs.map(tx => ({ + ...tx, + value: tx.value.toString(), + gasPrice: tx.gasPrice.toString(), + gasLimit: tx.gasLimit.toString(), + })), + }); + }); + + // Single pending tx by hash (TASK-167). 404 if mined or unknown. + app.get('/api/mempool/:hash', async (req, res) => { + const all = await txPool.getPendingTransactions(10_000); + const found = all.find(tx => tx.hash === req.params.hash); + if (!found) return res.status(404).json({ error: 'not in mempool' }); + res.json({ + ...found, + value: found.value.toString(), + gasPrice: found.gasPrice.toString(), + gasLimit: found.gasLimit.toString(), + }); + }); + + // Next nonce hint (TASK-057). max(chain_nonce, max_pending_nonce) + 1. + app.get('/api/account/:addr/next-nonce', async (req, res) => { + const chainNonce = stateManager.getNonce(req.params.addr); + const pending = txPool.getPendingForAddress(req.params.addr); + const maxPending = pending.reduce((m, t) => Math.max(m, t.nonce), -1); + const next = Math.max(chainNonce, maxPending + 1); + res.json({ address: req.params.addr, nextNonce: next }); + }); + + // TPS over a configurable window (TASK-051). Default 60s. Powered by + // chain.getRecentTps which already buckets recent block tx counts. + app.get('/api/chain/tps', async (req, res) => { + const window = Math.min(3600, Math.max(1, Number(req.query.window ?? 60))); + const tps = chain.getRecentTps(window); + res.json({ tps, window_sec: window }); + }); + // Get latest block app.get('/api/chain/latest', async (req, res) => { await syncSharedReadState(); @@ -1579,10 +1949,19 @@ Live context: console.error('Error listing frontend files:', e); } - // Serve static files first (images, CSS, JS, etc.) + // Serve versioned static assets aggressively, but keep the HTML shell + // fresh so browsers pick up new bundles after deploys. app.use(express.static(frontendPath, { maxAge: '1y', - etag: true + etag: true, + index: false, + setHeaders: (res, filePath) => { + if (path.extname(filePath).toLowerCase() === '.html') { + res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, private'); + return; + } + res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); + }, })); // Catch-all handler: send back React's index.html file for client-side routing @@ -1600,6 +1979,7 @@ Live context: return res.status(404).send('Static file not found'); } + res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, private'); res.sendFile(indexPath, (err) => { if (err) { console.error('Error serving index.html:', err); @@ -1653,6 +2033,7 @@ Live context: blockProducer.stop(); agentWorker.stop(); stopNetworkHeartbeat(); + githubUpdates.stopBackgroundSync(); process.exitCode = 1; setTimeout(() => process.exit(1), 0); }); @@ -1661,6 +2042,12 @@ Live context: console.log(`[SERVER] Running on http://localhost:${PORT}\n`); if (agentConfig.role === 'worker') { blockProducer.start(); + // Paced commit pusher — drains tier-3-backlog → main. + // No-ops without PACED_PUSH_ENABLED=true and GITHUB_TOKEN. + void import('../agent/PacedPusher').then(({ PacedPusher }) => { + const pacer = new PacedPusher(agentConfig.repoRoot || process.cwd()); + pacer.start(); + }).catch((err) => console.warn('[PACER] failed to load:', err?.message || err)); } }); @@ -1669,6 +2056,7 @@ Live context: blockProducer.stop(); agentWorker.stop(); stopNetworkHeartbeat(); + githubUpdates.stopBackgroundSync(); db.end(); process.exit(0); }); diff --git a/backend/src/blockchain/Block.ts b/backend/src/blockchain/Block.ts index ed4a1c43..405fe473 100644 --- a/backend/src/blockchain/Block.ts +++ b/backend/src/blockchain/Block.ts @@ -150,23 +150,29 @@ export class Block { return generateRandomBase58(44); } - public isValid(previousBlock?: Block): boolean { + public isValid(previousBlock?: Block, now: number = Date.now()): boolean { if (this.header.hash !== this.calculateHash()) { return false; } - + if (previousBlock && this.header.parentHash !== previousBlock.header.hash) { return false; } - + if (previousBlock && this.header.height !== previousBlock.header.height + 1) { return false; } - - if (previousBlock && this.header.timestamp <= previousBlock.header.timestamp) { + + // TASK-015: reject blocks more than 30s in the future (clock-skew defense). + if (this.header.timestamp > now + 30_000) { return false; } - + + // TASK-016: enforce a 2s minimum delta vs parent (no sub-second blocks). + if (previousBlock && this.header.timestamp - previousBlock.header.timestamp < 2000) { + return false; + } + return true; } @@ -183,6 +189,62 @@ export class Block { })) }; } + + /** + * Reconstruct a Block from a toJSON() payload (TASK-001). Round-trip + * property: `Block.fromJSON(b.toJSON()).header.hash === b.header.hash`. + * Throws on missing fields or hash mismatch (tampered header). + */ + public static fromJSON(json: any): Block { + const required = [ + 'height', 'hash', 'parentHash', 'producer', 'timestamp', 'nonce', + 'difficulty', 'gasUsed', 'gasLimit', 'stateRoot', 'transactionsRoot', + 'receiptsRoot', 'transactions', + ]; + for (const field of required) { + if (!(field in json)) { + throw new Error(`fromJSON: missing field '${field}'`); + } + } + + const txs: Transaction[] = (json.transactions as any[]).map((t) => ({ + hash: String(t.hash), + from: String(t.from), + to: String(t.to), + value: BigInt(t.value ?? '0'), + gasPrice: BigInt(t.gasPrice ?? '0'), + gasLimit: BigInt(t.gasLimit ?? '0'), + nonce: Number(t.nonce ?? 0), + data: t.data, + signature: String(t.signature ?? ''), + })); + + const block = new Block( + Number(json.height), + String(json.parentHash), + String(json.producer), + txs, + Number(json.difficulty ?? 1), + ); + + // Override constructor-derived header fields with the deserialized truth. + block.header.timestamp = Number(json.timestamp); + block.header.nonce = Number(json.nonce ?? 0); + block.header.gasUsed = BigInt(json.gasUsed); + block.header.gasLimit = BigInt(json.gasLimit); + block.header.stateRoot = String(json.stateRoot); + block.header.transactionsRoot = String(json.transactionsRoot); + block.header.receiptsRoot = String(json.receiptsRoot); + // Recompute hash from the now-canonical fields and assert. + block.header.hash = (block as any).calculateHash(); + + if (block.header.hash !== String(json.hash)) { + throw new Error( + `fromJSON: hash mismatch — header field tampered (got ${block.header.hash}, expected ${json.hash})`, + ); + } + return block; + } } // Export helper functions diff --git a/backend/src/blockchain/BlockProducer.ts b/backend/src/blockchain/BlockProducer.ts index 44f666b0..cade1021 100644 --- a/backend/src/blockchain/BlockProducer.ts +++ b/backend/src/blockchain/BlockProducer.ts @@ -19,7 +19,20 @@ import type { Log } from './TransactionReceipt'; // Block limits const MAX_BLOCK_GAS = 30000000n; const MAX_TRANSACTIONS_PER_BLOCK = 500; -const BLOCK_REWARD = 10n * 10n**18n; // 10 OPEN per block +// Per-validator block reward (TASK-039) — env-tunable so testnet/mainnet +// can differ without a code change. Default: 10 OPEN. +const BLOCK_REWARD: bigint = (() => { + const raw = process.env.HERMES_BLOCK_REWARD_WEI; + if (!raw) return 10n * 10n ** 18n; + try { + const parsed = BigInt(raw); + if (parsed < 0n) throw new Error('negative reward'); + return parsed; + } catch (err: any) { + console.warn(`[PRODUCER] HERMES_BLOCK_REWARD_WEI invalid (${raw}): ${err?.message || err} — using default 10 OPEN`); + return 10n * 10n ** 18n; + } +})(); export class BlockProducer { private chain: Chain; @@ -87,19 +100,32 @@ export class BlockProducer { // Get pending transactions with limits const pendingTxs = await this.txPool.getPendingTransactions(MAX_TRANSACTIONS_PER_BLOCK); - // Apply transactions to state with gas limit enforcement + // Apply transactions to state with gas + size limit enforcement const validTxs: Transaction[] = []; const receipts: TransactionReceipt[] = []; let totalGasUsed = 0n; let cumulativeGas = 0n; - + let serializedBytes = 0; + const MAX_BLOCK_BYTES = 1_048_576; // TASK-022 — 1MB serialized cap + for (const tx of pendingTxs) { // Check block gas limit if (totalGasUsed + tx.gasLimit > MAX_BLOCK_GAS) { console.log(`[PRODUCER] Block gas limit reached, stopping tx inclusion`); break; } - + // Check block size limit (TASK-022) + const txBytes = Buffer.byteLength(JSON.stringify({ + ...tx, value: tx.value.toString(), gasPrice: tx.gasPrice.toString(), + gasLimit: tx.gasLimit.toString(), + }), 'utf8'); + if (serializedBytes + txBytes > MAX_BLOCK_BYTES) { + console.log(`[PRODUCER] Block size cap reached at ${serializedBytes} bytes`); + break; + } + serializedBytes += txBytes; + + // Verify signature before applying if (!verifyTransactionSignature(tx)) { console.log(`[PRODUCER] Invalid signature for tx ${tx.hash.substring(0, 12)}...`); diff --git a/backend/src/blockchain/TransactionPool.ts b/backend/src/blockchain/TransactionPool.ts index 0d91bf63..c6d9343b 100644 --- a/backend/src/blockchain/TransactionPool.ts +++ b/backend/src/blockchain/TransactionPool.ts @@ -17,6 +17,8 @@ const MIN_GAS_PRICE = 1n; // Minimum gas price (lamports) export class TransactionPool { private pendingTransactions: Map = new Map(); + // Per-tx admit timestamp for TTL eviction (TASK-021). + private addedAtMs: Map = new Map(); private knownHashes: Set = new Set(); // Replay protection private async syncPendingTransactionsFromDb(): Promise { @@ -39,6 +41,7 @@ export class TransactionPool { signature: row.signature, }; this.pendingTransactions.set(tx.hash, tx); + this.addedAtMs.set(tx.hash, Date.now()); this.knownHashes.add(tx.hash); } } catch (error) { @@ -74,6 +77,7 @@ export class TransactionPool { } this.pendingTransactions.set(tx.hash, tx); + this.addedAtMs.set(tx.hash, Date.now()); this.knownHashes.add(tx.hash); try { @@ -117,6 +121,7 @@ export class TransactionPool { async removeTransactions(hashes: string[]) { for (const hash of hashes) { this.pendingTransactions.delete(hash); + this.addedAtMs.delete(hash); } if (hashes.length > 0) { @@ -149,6 +154,7 @@ export class TransactionPool { const validation = await this.validateTransaction(tx); if (!validation.valid) { this.pendingTransactions.delete(hash); + this.addedAtMs.delete(hash); this.knownHashes.delete(hash); dropped.push({ hash, reason: validation.error || 'invalid' }); } @@ -275,10 +281,26 @@ export class TransactionPool { .filter(tx => tx.from === address || tx.to === address); } - // Clear expired transactions (optional TTL) - clearExpired(maxAge: number = 3600000): number { - // This would require storing timestamp with each tx - // For now, just return 0 - return 0; + // Clear expired transactions (TASK-021). Default TTL 1h. + clearExpired(maxAgeMs: number = 3_600_000): number { + const cutoff = Date.now() - maxAgeMs; + let removed = 0; + for (const [hash, addedAt] of this.addedAtMs.entries()) { + if (addedAt < cutoff) { + this.pendingTransactions.delete(hash); + this.addedAtMs.delete(hash); + removed++; + } + } + if (removed > 0) { + console.log(`[POOL] cleared ${removed} expired tx(s) older than ${maxAgeMs}ms`); + } + return removed; + } + + // Start a periodic eviction loop (TASK-021). Returns the interval handle + // so callers can stop it on shutdown. + startExpirationLoop(intervalMs: number = 60_000, maxAgeMs: number = 3_600_000): NodeJS.Timeout { + return setInterval(() => this.clearExpired(maxAgeMs), intervalMs); } } diff --git a/backend/src/database/cacheWarmer.ts b/backend/src/database/cacheWarmer.ts new file mode 100644 index 00000000..17b0c3c1 --- /dev/null +++ b/backend/src/database/cacheWarmer.ts @@ -0,0 +1,50 @@ +import { db, cache, chainState } from './db'; + +/** + * Cache warmer (TASK-328). + * + * Cold boot leaves Redis empty; the first ~30s of traffic causes burst + * PG reads as the cache populates. Pre-warm with the things chainState + * is going to read anyway: latest 100 blocks, top 50 accounts by balance, + * last block height. Runs once at boot when CACHE_WARMER_ENABLED=true. + */ + +export async function warmCache(): Promise<{ entries: number; durationMs: number }> { + const startedAt = Date.now(); + let entries = 0; + + try { + // Latest 100 blocks → block:height:N cache. + const blocksRes = await db.query( + `SELECT * FROM blocks ORDER BY height DESC LIMIT 100` + ); + for (const block of blocksRes.rows) { + await cache.setJSON(`block:height:${block.height}`, block, 300); + entries++; + } + + // Top 50 accounts by balance. + const topRes = await db.query( + `SELECT address, balance, nonce FROM accounts + ORDER BY balance::numeric DESC LIMIT 50` + ); + await cache.setJSON('top_accounts:50', topRes.rows, 60); + entries++; + + // Last block height (already maintained by chainState but make sure it's there). + const headRes = await db.query( + `SELECT MAX(height) AS max_height FROM blocks` + ); + const head = Number(headRes.rows[0]?.max_height ?? 0); + if (head > 0) { + await chainState.saveBlockHeight(head); + entries++; + } + } catch (err: any) { + console.warn(`[CACHE WARMER] partial failure: ${err?.message || err}`); + } + + const durationMs = Date.now() - startedAt; + console.log(`[CACHE WARMER] ${entries} entries warmed in ${durationMs}ms`); + return { entries, durationMs }; +} diff --git a/backend/src/database/db.ts b/backend/src/database/db.ts index a6d09f09..132de2c1 100644 --- a/backend/src/database/db.ts +++ b/backend/src/database/db.ts @@ -1,25 +1,34 @@ import { Pool } from 'pg'; import Redis from 'ioredis'; import * as dotenv from 'dotenv'; +import { recordQuery, recordQueryError } from './queryMetrics'; dotenv.config(); +// Slow-query threshold (TASK-321). Queries above this log a [PG SLOW] warning. +const PG_SLOW_QUERY_MS = Math.max(1, Number(process.env.PG_SLOW_QUERY_MS || '1000')); + // PostgreSQL connection - uses Railway's DATABASE_URL const DATABASE_URL = process.env.DATABASE_URL; let pool: Pool | null = null; let redis: Redis | null = null; -// Initialize PostgreSQL +// Initialize PostgreSQL — tunable via env so dev (small) and prod (large) +// can size differently without a code change. +const POOL_MAX = Math.max(1, Number(process.env.PG_POOL_MAX || '20')); +const POOL_IDLE_MS = Math.max(1000, Number(process.env.PG_POOL_IDLE_MS || '30000')); +const POOL_CONNECT_MS = Math.max(500, Number(process.env.PG_POOL_CONNECT_MS || '2000')); + if (DATABASE_URL) { pool = new Pool({ connectionString: DATABASE_URL, ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false, - max: 20, - idleTimeoutMillis: 30000, - connectionTimeoutMillis: 2000, + max: POOL_MAX, + idleTimeoutMillis: POOL_IDLE_MS, + connectionTimeoutMillis: POOL_CONNECT_MS, }); - console.log('[DB] PostgreSQL pool created'); + console.log(`[DB] PostgreSQL pool created (max=${POOL_MAX}, idle=${POOL_IDLE_MS}ms, connect=${POOL_CONNECT_MS}ms)`); } else { console.warn('[DB] DATABASE_URL not set - using in-memory fallback'); } @@ -64,35 +73,63 @@ export const db = { console.log('Using in-memory storage (no DATABASE_URL)'); return; } - + + const startedAt = Date.now(); try { // Split by semicolon and execute each statement const statements = sql.split(';').filter(s => s.trim().length > 0); for (const statement of statements) { await pool.query(statement); } + const duration = Date.now() - startedAt; + recordQuery(duration); + if (duration > PG_SLOW_QUERY_MS) { + console.warn(`[PG SLOW] exec ${duration}ms (${sql.length} chars, ${sql.split(';').length} stmts)`); + } } catch (error) { + recordQueryError(); console.error('Database exec error:', error); throw error; } }, - + // Execute single query with parameters query: async (text: string, params: any[] = []): Promise<{ rows: any[]; rowCount?: number }> => { if (!pool) { // Fallback to memory return { rows: [], rowCount: 0 }; } - + + const startedAt = Date.now(); try { const result = await pool.query(text, params); + const duration = Date.now() - startedAt; + recordQuery(duration); + if (duration > PG_SLOW_QUERY_MS) { + // Truncate SQL to 200 chars; never log param VALUES (PII), only count. + const sqlPreview = text.length > 200 ? text.slice(0, 200) + '…' : text; + console.warn(`[PG SLOW] query ${duration}ms params=${params.length} sql=${JSON.stringify(sqlPreview)}`); + } return { rows: result.rows, rowCount: result.rowCount || 0 }; } catch (error) { + recordQueryError(); console.error('Database query error:', error); throw error; } }, + // Pool capacity snapshot for /api/metrics + /health/ready. + // Returns zeros when running in-memory (no pool). + poolStats: (): { total: number; idle: number; waiting: number; max: number } => { + if (!pool) return { total: 0, idle: 0, waiting: 0, max: POOL_MAX }; + return { + total: (pool as any).totalCount ?? 0, + idle: (pool as any).idleCount ?? 0, + waiting: (pool as any).waitingCount ?? 0, + max: POOL_MAX, + }; + }, + // Test connection connect: async (): Promise => { if (!pool) { diff --git a/backend/src/database/migrations.ts b/backend/src/database/migrations.ts index 4db7b08f..c21235cc 100644 --- a/backend/src/database/migrations.ts +++ b/backend/src/database/migrations.ts @@ -39,6 +39,16 @@ export interface MigrationResult { } /** Load every .sql file from `dir` sorted lexicographically. */ +// Exposed so the migrate:down CLI (TASK-325) and dry-run mode (TASK-326) +// can reuse the same loader the boot path uses. +export function loadMigrationsFromDir(dir: string): MigrationFile[] { + return loadMigrationsFrom(dir); +} + +export function getMigrationsDir(): string { + return resolveMigrationsDir(); +} + function loadMigrationsFrom(dir: string): MigrationFile[] { let entries: string[] = []; try { @@ -107,7 +117,13 @@ function resolveMigrationsDir(): string { return srcCandidate; } -export async function applyPendingMigrations(): Promise { +export interface ApplyOptions { + /** When true, log the SQL each pending migration would execute and exit + * without touching the database. Used by `npm run migrate:status -- --dry-run`. */ + readonly dryRun?: boolean; +} + +export async function applyPendingMigrations(opts: ApplyOptions = {}): Promise { await ensureMigrationsTable(); const gotLock = await acquireLock(); try { @@ -118,6 +134,13 @@ export async function applyPendingMigrations(): Promise { for (const migration of migrations) { if (applied.has(migration.name)) continue; const startedAt = Date.now(); + + if (opts.dryRun) { + console.log(`[DRY-RUN] ${migration.name}\n${migration.up}\n---`); + results.push({ name: migration.name, success: true, durationMs: 0 }); + continue; + } + try { await db.exec(migration.up); await db.query( diff --git a/backend/src/database/migrations/0002_tx_block_height_idx.sql b/backend/src/database/migrations/0002_tx_block_height_idx.sql new file mode 100644 index 00000000..987e4ca3 --- /dev/null +++ b/backend/src/database/migrations/0002_tx_block_height_idx.sql @@ -0,0 +1,6 @@ +-- up: +CREATE INDEX IF NOT EXISTS idx_transactions_block_height + ON transactions(block_height); + +-- down: +DROP INDEX IF EXISTS idx_transactions_block_height; diff --git a/backend/src/database/migrations/0003_tx_from_nonce_idx.sql b/backend/src/database/migrations/0003_tx_from_nonce_idx.sql new file mode 100644 index 00000000..3d5d3918 --- /dev/null +++ b/backend/src/database/migrations/0003_tx_from_nonce_idx.sql @@ -0,0 +1,6 @@ +-- up: +CREATE INDEX IF NOT EXISTS idx_transactions_from_nonce + ON transactions(from_address, nonce DESC); + +-- down: +DROP INDEX IF EXISTS idx_transactions_from_nonce; diff --git a/backend/src/database/migrations/0004_accounts_balance_idx.sql b/backend/src/database/migrations/0004_accounts_balance_idx.sql new file mode 100644 index 00000000..e8d849cb --- /dev/null +++ b/backend/src/database/migrations/0004_accounts_balance_idx.sql @@ -0,0 +1,6 @@ +-- up: +CREATE INDEX IF NOT EXISTS idx_accounts_balance_desc + ON accounts ((balance::numeric) DESC); + +-- down: +DROP INDEX IF EXISTS idx_accounts_balance_desc; diff --git a/backend/src/database/migrations/0005_receipts_status_idx.sql b/backend/src/database/migrations/0005_receipts_status_idx.sql new file mode 100644 index 00000000..408059e8 --- /dev/null +++ b/backend/src/database/migrations/0005_receipts_status_idx.sql @@ -0,0 +1,6 @@ +-- up: +CREATE INDEX IF NOT EXISTS idx_receipts_status + ON receipts(status, block_number DESC); + +-- down: +DROP INDEX IF EXISTS idx_receipts_status; diff --git a/backend/src/database/migrations/0006_receipts_logs_gin.sql b/backend/src/database/migrations/0006_receipts_logs_gin.sql new file mode 100644 index 00000000..0c2a4069 --- /dev/null +++ b/backend/src/database/migrations/0006_receipts_logs_gin.sql @@ -0,0 +1,10 @@ +-- up: +ALTER TABLE receipts + ADD COLUMN IF NOT EXISTS logs_jsonb JSONB + GENERATED ALWAYS AS (logs_json::jsonb) STORED; +CREATE INDEX IF NOT EXISTS idx_receipts_logs_gin + ON receipts USING GIN (logs_jsonb); + +-- down: +DROP INDEX IF EXISTS idx_receipts_logs_gin; +ALTER TABLE receipts DROP COLUMN IF EXISTS logs_jsonb; diff --git a/backend/src/database/migrations/0007_validators_stake.sql b/backend/src/database/migrations/0007_validators_stake.sql new file mode 100644 index 00000000..99cbad70 --- /dev/null +++ b/backend/src/database/migrations/0007_validators_stake.sql @@ -0,0 +1,6 @@ +-- up: +ALTER TABLE validators + ADD COLUMN IF NOT EXISTS stake NUMERIC NOT NULL DEFAULT 1; + +-- down: +ALTER TABLE validators DROP COLUMN IF EXISTS stake; diff --git a/backend/src/database/migrations/0008_validator_slashes.sql b/backend/src/database/migrations/0008_validator_slashes.sql new file mode 100644 index 00000000..1a5c0a03 --- /dev/null +++ b/backend/src/database/migrations/0008_validator_slashes.sql @@ -0,0 +1,20 @@ +-- up: +CREATE TABLE IF NOT EXISTS validator_slashes ( + id BIGSERIAL PRIMARY KEY, + validator_address TEXT NOT NULL, + block_height BIGINT NOT NULL, + reason TEXT NOT NULL, + evidence_json TEXT NOT NULL DEFAULT '{}', + stake_before NUMERIC NOT NULL, + stake_after NUMERIC NOT NULL, + slashed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_slashes_validator + ON validator_slashes(validator_address); +CREATE INDEX IF NOT EXISTS idx_slashes_height + ON validator_slashes(block_height); + +-- down: +DROP INDEX IF EXISTS idx_slashes_height; +DROP INDEX IF EXISTS idx_slashes_validator; +DROP TABLE IF EXISTS validator_slashes; diff --git a/backend/src/database/migrations/0009_contract_code.sql b/backend/src/database/migrations/0009_contract_code.sql new file mode 100644 index 00000000..e2f0e413 --- /dev/null +++ b/backend/src/database/migrations/0009_contract_code.sql @@ -0,0 +1,18 @@ +-- up: +CREATE TABLE IF NOT EXISTS contract_code ( + address TEXT PRIMARY KEY, + code_hash TEXT NOT NULL, + bytecode TEXT NOT NULL, + deployed_at_block BIGINT NOT NULL, + deployed_by TEXT NOT NULL, + deployed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_contract_code_hash + ON contract_code(code_hash); +CREATE INDEX IF NOT EXISTS idx_contract_code_deployer + ON contract_code(deployed_by); + +-- down: +DROP INDEX IF EXISTS idx_contract_code_deployer; +DROP INDEX IF EXISTS idx_contract_code_hash; +DROP TABLE IF EXISTS contract_code; diff --git a/backend/src/database/migrations/0010_contract_storage.sql b/backend/src/database/migrations/0010_contract_storage.sql new file mode 100644 index 00000000..0fe99014 --- /dev/null +++ b/backend/src/database/migrations/0010_contract_storage.sql @@ -0,0 +1,15 @@ +-- up: +CREATE TABLE IF NOT EXISTS contract_storage ( + contract_address TEXT NOT NULL, + storage_key TEXT NOT NULL, + storage_value TEXT NOT NULL, + updated_at_block BIGINT NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (contract_address, storage_key) +); +CREATE INDEX IF NOT EXISTS idx_contract_storage_block + ON contract_storage(updated_at_block); + +-- down: +DROP INDEX IF EXISTS idx_contract_storage_block; +DROP TABLE IF EXISTS contract_storage; diff --git a/backend/src/database/migrations/0011_contract_metadata.sql b/backend/src/database/migrations/0011_contract_metadata.sql new file mode 100644 index 00000000..831161d8 --- /dev/null +++ b/backend/src/database/migrations/0011_contract_metadata.sql @@ -0,0 +1,17 @@ +-- up: +CREATE TABLE IF NOT EXISTS contract_metadata ( + address TEXT PRIMARY KEY, + name TEXT, + abi_json TEXT, + source_url TEXT, + source_verified BOOLEAN NOT NULL DEFAULT FALSE, + verifier_notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_contract_metadata_verified + ON contract_metadata(source_verified) WHERE source_verified = TRUE; + +-- down: +DROP INDEX IF EXISTS idx_contract_metadata_verified; +DROP TABLE IF EXISTS contract_metadata; diff --git a/backend/src/database/migrations/0012_state_snapshots.sql b/backend/src/database/migrations/0012_state_snapshots.sql new file mode 100644 index 00000000..e668a95c --- /dev/null +++ b/backend/src/database/migrations/0012_state_snapshots.sql @@ -0,0 +1,12 @@ +-- up: +CREATE TABLE IF NOT EXISTS state_snapshots ( + height BIGINT PRIMARY KEY, + state_root TEXT NOT NULL, + account_count INTEGER NOT NULL, + storage_count INTEGER NOT NULL, + snapshot_blob BYTEA NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- down: +DROP TABLE IF EXISTS state_snapshots; diff --git a/backend/src/database/migrations/0013_peers.sql b/backend/src/database/migrations/0013_peers.sql new file mode 100644 index 00000000..2868552b --- /dev/null +++ b/backend/src/database/migrations/0013_peers.sql @@ -0,0 +1,15 @@ +-- up: +CREATE TABLE IF NOT EXISTS peers ( + peer_id TEXT PRIMARY KEY, + url TEXT NOT NULL, + chain_height BIGINT NOT NULL DEFAULT 0, + public_key TEXT NOT NULL DEFAULT '', + last_seen_ms BIGINT NOT NULL, + first_seen_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_peers_last_seen + ON peers(last_seen_ms DESC); + +-- down: +DROP INDEX IF EXISTS idx_peers_last_seen; +DROP TABLE IF EXISTS peers; diff --git a/backend/src/database/migrations/0014_rename_chat_logs.sql b/backend/src/database/migrations/0014_rename_chat_logs.sql new file mode 100644 index 00000000..874930bd --- /dev/null +++ b/backend/src/database/migrations/0014_rename_chat_logs.sql @@ -0,0 +1,7 @@ +-- up: +ALTER TABLE chat_logs RENAME TO agent_chat_logs; +CREATE OR REPLACE VIEW chat_logs AS SELECT * FROM agent_chat_logs; + +-- down: +DROP VIEW IF EXISTS chat_logs; +ALTER TABLE agent_chat_logs RENAME TO chat_logs; diff --git a/backend/src/database/migrations/0015_newsletter.sql b/backend/src/database/migrations/0015_newsletter.sql new file mode 100644 index 00000000..a4977c17 --- /dev/null +++ b/backend/src/database/migrations/0015_newsletter.sql @@ -0,0 +1,14 @@ +-- up: +CREATE TABLE IF NOT EXISTS newsletter_subscribers ( + id BIGSERIAL PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + source TEXT, + confirmed BOOLEAN NOT NULL DEFAULT FALSE, + subscribed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + unsubscribed_at TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_newsletter_email ON newsletter_subscribers(email); + +-- down: +DROP INDEX IF EXISTS idx_newsletter_email; +DROP TABLE IF EXISTS newsletter_subscribers; diff --git a/backend/src/database/queryMetrics.ts b/backend/src/database/queryMetrics.ts new file mode 100644 index 00000000..8416e529 --- /dev/null +++ b/backend/src/database/queryMetrics.ts @@ -0,0 +1,64 @@ +/** + * Query latency histogram for /api/metrics (TASK-320). + * + * Cumulative-bucket Prometheus histogram. Bucket boundaries chosen to + * cover human-noticeable PG latency: 1ms (in-memory cache hit) to 5s + * (a query you'd want a slow-log entry for). + */ + +const BUCKET_BOUNDS_MS = [1, 5, 10, 50, 100, 500, 1000, 5000] as const; + +let counts = new Array(BUCKET_BOUNDS_MS.length).fill(0); +let sum = 0; +let count = 0; +let errors = 0; + +export function recordQuery(durationMs: number): void { + for (let i = 0; i < BUCKET_BOUNDS_MS.length; i++) { + if (durationMs <= BUCKET_BOUNDS_MS[i]) { + counts[i]++; + break; + } + } + sum += durationMs; + count++; +} + +export function recordQueryError(): void { + errors++; +} + +export interface HistogramSnapshot { + buckets: readonly number[]; + counts: number[]; + cumulativeCounts: number[]; + sum: number; + count: number; + errors: number; + meanMs: number; +} + +export function getHistogram(): HistogramSnapshot { + const cumulative: number[] = []; + let running = 0; + for (const c of counts) { + running += c; + cumulative.push(running); + } + return { + buckets: BUCKET_BOUNDS_MS, + counts: [...counts], + cumulativeCounts: cumulative, + sum, + count, + errors, + meanMs: count === 0 ? 0 : sum / count, + }; +} + +export function resetHistogram(): void { + counts = new Array(BUCKET_BOUNDS_MS.length).fill(0); + sum = 0; + count = 0; + errors = 0; +} diff --git a/backend/src/events/RedisBridge.ts b/backend/src/events/RedisBridge.ts new file mode 100644 index 00000000..ddb98757 --- /dev/null +++ b/backend/src/events/RedisBridge.ts @@ -0,0 +1,81 @@ +import Redis from 'ioredis'; +import { EventBus } from './EventBus'; + +/** + * Cross-replica event bridge over Redis pub/sub (TASK-330). + * + * The local EventBus is in-process; SSE clients on replica A miss events + * emitted on replica B. This bridge republishes whitelisted local events + * to a Redis channel and re-emits incoming channel messages locally. + * + * Loop prevention: every bridged payload is tagged with `_origin` (this + * replica's id). Echoes from the same origin are dropped. + */ + +const CHANNEL = 'hermes:events:v1'; + +const BRIDGED_EVENTS = [ + 'block_produced', + 'consensus_quorum', + 'consensus_failed', + 'chain_reorg', + 'mesh_block_received', + 'ci_results', + 'ci_failure', + 'ci_watch_triggered', + 'network_message', + 'state_root_mismatch', +] as const; + +export interface BridgeHandle { + detach(): void; +} + +export function attachRedisBridge(eventBus: EventBus, redisUrl: string): BridgeHandle { + const originId = process.env.REPLICA_ID || `${process.pid}@${require('os').hostname()}`; + const publisher = new Redis(redisUrl); + const subscriber = new Redis(redisUrl); + + const localHandlers = new Map void>(); + + for (const name of BRIDGED_EVENTS) { + const handler = (payload: any) => { + // Don't republish messages we just received from the channel. + if (payload && payload.__bridged) return; + const wrapped = JSON.stringify({ event: name, payload, _origin: originId }); + publisher.publish(CHANNEL, wrapped).catch((err) => + console.warn(`[REDIS BRIDGE] publish ${name} failed: ${err?.message || err}`)); + }; + eventBus.on(name, handler); + localHandlers.set(name, handler); + } + + subscriber.subscribe(CHANNEL).catch((err) => + console.error(`[REDIS BRIDGE] subscribe failed: ${err?.message || err}`)); + + subscriber.on('message', (_channel, raw) => { + try { + const msg = JSON.parse(raw); + if (msg._origin === originId) return; // skip our own + if (!BRIDGED_EVENTS.includes(msg.event)) return; + // Mark so the local handler doesn't re-publish. + const payload = { ...(msg.payload ?? {}), __bridged: true }; + eventBus.emit(msg.event, payload); + } catch (err: any) { + console.warn(`[REDIS BRIDGE] bad message: ${err?.message || err}`); + } + }); + + console.log(`[REDIS BRIDGE] attached as ${originId} on channel ${CHANNEL}`); + + return { + detach() { + for (const [name, handler] of localHandlers.entries()) { + eventBus.off(name, handler); + } + subscriber.unsubscribe(CHANNEL).catch(() => {}); + subscriber.disconnect(); + publisher.disconnect(); + }, + }; +} diff --git a/backend/src/network/api.ts b/backend/src/network/api.ts index fdc6fcd1..fabfa21e 100644 --- a/backend/src/network/api.ts +++ b/backend/src/network/api.ts @@ -1,5 +1,6 @@ import { Router } from 'express'; import { Chain } from '../blockchain/Chain'; +import { Block } from '../blockchain/Block'; import { peerRegistry } from './PeerRegistry'; import { eventBus } from '../events/EventBus'; @@ -42,6 +43,42 @@ export function createMeshRouter(chain: Chain): Router { }); }); + // Header-only range fetch (TASK-003) — peers measuring honest-majority + // or chain tip don't need full transaction payloads. + router.get('/headers', (req, res) => { + const from = Math.max(0, Number(req.query.from ?? 0)); + const to = Math.max(from, Number(req.query.to ?? from)); + if (to - from > 1000) { + return res.status(400).json({ error: 'range exceeds 1000' }); + } + const headers = []; + for (let h = from; h <= to; h++) { + const block = chain.getBlockByHeight(h); + if (!block) continue; + headers.push({ + ...block.header, + gasUsed: block.header.gasUsed.toString(), + gasLimit: block.header.gasLimit.toString(), + }); + } + res.json({ headers }); + }); + + // Bulk block fetch (TASK-004) — full Block.toJSON for sync. + router.get('/blocks', (req, res) => { + const from = Math.max(0, Number(req.query.from ?? 0)); + const to = Math.max(from, Number(req.query.to ?? from)); + if (to - from > 100) { + return res.status(400).json({ error: 'range exceeds 100' }); + } + const blocks = []; + for (let h = from; h <= to; h++) { + const block = chain.getBlockByHeight(h); + if (block) blocks.push(block.toJSON()); + } + res.json({ blocks }); + }); + router.get('/head', (_req, res) => { const head = chain.getLatestBlock(); res.json({ @@ -52,25 +89,45 @@ export function createMeshRouter(chain: Chain): Router { }); }); - router.post('/block', (req, res) => { - // Gossip acceptance is conservative: we log the claim + emit an event - // so monitors can see the incoming block, but we don't apply it to our - // chain until the full gossip deserializer + verification pipeline is - // wired (it needs Block.fromJSON, which doesn't exist yet). - const body = req.body || {}; - const height = Number(body?.header?.height ?? body?.height ?? -1); - const hash = String(body?.header?.hash ?? body?.hash ?? ''); - const producer = String(body?.header?.producer ?? body?.producer ?? ''); - if (height < 0 || !hash) { - return res.status(400).json({ accepted: false, reason: 'missing height or hash' }); + router.post('/block', async (req, res) => { + // Real gossip acceptance (TASK-002): deserialize via Block.fromJSON, + // then run through chain.addBlock which routes forks via ForkManager. + let block: Block; + try { + block = Block.fromJSON(req.body); + } catch (err: any) { + return res.status(400).json({ + accepted: false, + reason: `fromJSON failed: ${err?.message || err}`, + }); } - eventBus.emit('mesh_block_received', { height, hash, producer }); - const head = chain.getLatestBlock(); - res.json({ - accepted: false, - reason: 'gossip-apply pending Block.fromJSON deserializer', - head: { height: chain.getChainLength(), hash: head?.header.hash || '' }, + + eventBus.emit('mesh_block_received', { + height: block.header.height, + hash: block.header.hash, + producer: block.header.producer, }); + + try { + const added = await chain.addBlock(block); + const head = chain.getLatestBlock(); + if (added) { + return res.json({ + accepted: true, + head: { height: chain.getChainLength(), hash: head?.header.hash || '' }, + }); + } + return res.status(409).json({ + accepted: false, + reason: 'addBlock rejected (parent unknown, finality violation, or invalid)', + head: { height: chain.getChainLength(), hash: head?.header.hash || '' }, + }); + } catch (err: any) { + return res.status(409).json({ + accepted: false, + reason: err?.message || 'addBlock threw', + }); + } }); return router; diff --git a/backend/src/vm/GasMeter.ts b/backend/src/vm/GasMeter.ts index 97ffe29f..391123c1 100644 --- a/backend/src/vm/GasMeter.ts +++ b/backend/src/vm/GasMeter.ts @@ -7,6 +7,16 @@ export const GAS_COSTS = { POP: 2n, ADD: 3n, SUB: 3n, + // Arithmetic + comparisons + bitwise ops added across TASK-061..065. + MUL: 5n, + DIV: 5n, + MOD: 5n, + EQ: 3n, + LT: 3n, + GT: 3n, + AND: 3n, + OR: 3n, + NOT: 3n, SSTORE: 20000n, STOP: 0n, REVERT: 0n, diff --git a/backend/src/vm/Interpreter.ts b/backend/src/vm/Interpreter.ts index 7e505071..cd7f5e02 100644 --- a/backend/src/vm/Interpreter.ts +++ b/backend/src/vm/Interpreter.ts @@ -23,6 +23,15 @@ export type VmOp = | { op: 'POP' } | { op: 'ADD' } | { op: 'SUB' } + | { op: 'MUL' } + | { op: 'DIV' } + | { op: 'MOD' } + | { op: 'EQ' } + | { op: 'LT' } + | { op: 'GT' } + | { op: 'AND' } + | { op: 'OR' } + | { op: 'NOT' } | { op: 'SSTORE'; args: [string, string] } | { op: 'LOG'; args: { topics?: string[]; data?: string } } | { op: 'STOP' } @@ -86,6 +95,74 @@ export class Interpreter { stack.push(b - a); break; } + case 'MUL': { + if (!meter.charge(GAS_COSTS.MUL)) { + return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at MUL' }; + } + const a = Number(stack.pop() ?? 0); + const b = Number(stack.pop() ?? 0); + stack.push(a * b); + break; + } + case 'DIV': { + if (!meter.charge(GAS_COSTS.DIV)) { + return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at DIV' }; + } + const b = Number(stack.pop() ?? 0); + const a = Number(stack.pop() ?? 0); + if (b === 0) return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'division by zero' }; + stack.push(Math.trunc(a / b)); + break; + } + case 'MOD': { + if (!meter.charge(GAS_COSTS.MOD)) { + return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at MOD' }; + } + const b = Number(stack.pop() ?? 0); + const a = Number(stack.pop() ?? 0); + if (b === 0) return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'modulo by zero' }; + stack.push(a % b); + break; + } + case 'EQ': { + if (!meter.charge(GAS_COSTS.EQ)) return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at EQ' }; + stack.push(stack.pop() === stack.pop() ? 1 : 0); + break; + } + case 'LT': { + if (!meter.charge(GAS_COSTS.LT)) return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at LT' }; + const b = Number(stack.pop() ?? 0); + const a = Number(stack.pop() ?? 0); + stack.push(a < b ? 1 : 0); + break; + } + case 'GT': { + if (!meter.charge(GAS_COSTS.GT)) return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at GT' }; + const b = Number(stack.pop() ?? 0); + const a = Number(stack.pop() ?? 0); + stack.push(a > b ? 1 : 0); + break; + } + case 'AND': { + if (!meter.charge(GAS_COSTS.AND)) return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at AND' }; + const a = BigInt(stack.pop() ?? 0); + const b = BigInt(stack.pop() ?? 0); + stack.push(Number(a & b)); + break; + } + case 'OR': { + if (!meter.charge(GAS_COSTS.OR)) return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at OR' }; + const a = BigInt(stack.pop() ?? 0); + const b = BigInt(stack.pop() ?? 0); + stack.push(Number(a | b)); + break; + } + case 'NOT': { + if (!meter.charge(GAS_COSTS.NOT)) return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at NOT' }; + const a = BigInt(stack.pop() ?? 0); + stack.push(Number(~a)); + break; + } case 'SSTORE': { if (!meter.charge(GAS_COSTS.SSTORE)) { return { status: 'revert', gasUsed: meter.getSpent(), logs, storage, error: 'out-of-gas at SSTORE' }; diff --git a/backend/src/worker.ts b/backend/src/worker.ts index 37620bd0..ddbc3f5c 100644 --- a/backend/src/worker.ts +++ b/backend/src/worker.ts @@ -7,6 +7,7 @@ import { EventBus } from './events/EventBus'; import { stateManager } from './blockchain/StateManager'; import { db } from './database/db'; import { createTables } from './database/schema'; +import { applyPendingMigrations } from './database/migrations'; import { configureAgentSubsystems, createAgentConfig, @@ -15,6 +16,7 @@ import { agentTaskStore, skillManager, agentWorker, + githubUpdates, } from './agent'; import { initializeLogsTable, @@ -42,6 +44,7 @@ async function main() { const connected = await db.connect(); if (connected) { await db.exec(createTables); + await applyPendingMigrations(); console.log('[WORKER] Database ready'); } @@ -66,6 +69,8 @@ async function main() { await skillManager.initialize(); await agentTaskStore.initialize(); await agentRuntimeStore.initialize(); + await githubUpdates.initialize(agentConfig.repoRoot); + githubUpdates.startBackgroundSync(); let currentTaskId: string | undefined; let currentTaskTitle: string | undefined; @@ -188,6 +193,12 @@ async function main() { }); } + // Paced commit pusher — drains tier-3-backlog → main at controlled cadence. + // No-ops unless PACED_PUSH_ENABLED=true and GITHUB_TOKEN set. + const { PacedPusher } = await import('./agent/PacedPusher'); + const pacer = new PacedPusher(agentConfig.repoRoot || process.cwd()); + pacer.start(); + blockProducer.start(); const port = Number(process.env.PORT || 4000); @@ -243,6 +254,7 @@ async function main() { blockProducer.stop(); agentWorker.stop(); stopNetworkHeartbeat(); + githubUpdates.stopBackgroundSync(); process.exitCode = 1; setTimeout(() => process.exit(1), 0); }); @@ -257,6 +269,7 @@ async function main() { blockProducer.stop(); agentWorker.stop(); stopNetworkHeartbeat(); + githubUpdates.stopBackgroundSync(); await db.end(); process.exit(0); }; diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 00000000..2e400e7c --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,27 @@ +/** + * Commitlint config — enforces conventional-commits. + * + * Examples that pass: + * feat: add new wallet endpoint + * feat(api): TASK-152 — Prometheus metrics + * fix(chain): off-by-one in finality calc + * docs(backlog): expand section 04 specs + * + * Examples that fail: + * "WIP" + * "added some stuff" + * "Fix bug" (uppercase F + no scope/colon) + */ + +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-enum': [2, 'always', [ + 'feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', + 'build', 'ci', 'chore', 'revert', + ]], + 'subject-case': [0], // allow proper nouns / TASK-IDs + 'header-max-length': [2, 'always', 200], + 'body-max-line-length': [0], // allow long body lines (URL refs, etc.) + }, +}; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..27977903 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,54 @@ +version: '3.8' + +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: hermeschain + POSTGRES_USER: hermes + POSTGRES_PASSWORD: hermes + ports: + - "5432:5432" + volumes: + - pg_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "hermes"] + interval: 5s + timeout: 3s + retries: 5 + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + + backend: + build: + context: ./backend + dockerfile: ../Dockerfile + environment: + DATABASE_URL: postgresql://hermes:hermes@postgres:5432/hermeschain + REDIS_URL: redis://redis:6379 + AGENT_ROLE: web + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} + HERMES_MODEL: claude-haiku-4-5-20251001 + ADMIN_TOKEN: ${ADMIN_TOKEN} + NODE_ENV: development + ports: + - "4000:4000" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - ./backend/src:/app/src + - ./backend/data:/app/data + +volumes: + pg_data: diff --git a/docs/backlog/queue.md b/docs/backlog/queue.md new file mode 100644 index 00000000..99615c20 --- /dev/null +++ b/docs/backlog/queue.md @@ -0,0 +1,64 @@ +# Backlog Queue Index + +490 tasks across 13 sections. Each section file holds detailed specs (files, reuse hooks, API contracts, migration SQL, acceptance, verification) for every task in that section. The implementation pass commits one task per commit on the `tier-3-backlog` branch; the paced-push script (`backend/scripts/paced-push.ts`, to be authored) advances `origin/main` by N commits per fire at the configured cadence. + +**Plan reference:** `/Users/white_roze/.claude/plans/sunny-tumbling-wind.md` + +## Status + +| # | Section | Range | Specs | Implemented | File | +|---|---|---|---|---|---| +| 01 | Chain & consensus | TASK-001..060 | 60/60 | 0/60 | [01-chain-consensus.md](queue/01-chain-consensus.md) | +| 02 | VM | TASK-061..105 | 45/45 | 0/45 | [02-vm.md](queue/02-vm.md) | +| 03 | Wallet & accounts | TASK-106..140 | 35/35 | 0/35 | [03-wallet.md](queue/03-wallet.md) | +| 04 | API & explorer | TASK-141..180 | 40/40 | 0/40 | [04-api-explorer.md](queue/04-api-explorer.md) | +| 05 | Agent worker | TASK-181..215 | 35/35 | 0/35 | [05-agent-worker.md](queue/05-agent-worker.md) | +| 06 | Frontend / HUD | TASK-216..265 | 50/50 | 0/50 | [06-frontend-hud.md](queue/06-frontend-hud.md) | +| 07 | Docs & site | TASK-266..305 | 40/40 | 0/40 | [07-docs-site.md](queue/07-docs-site.md) | +| 08 | Database & ops | TASK-306..335 | 30/30 | 0/30 | [08-database-ops.md](queue/08-database-ops.md) | +| 09 | Security | TASK-336..370 | 35/35 | 0/35 | [09-security.md](queue/09-security.md) | +| 10 | Testing | TASK-371..410 | 40/40 | 0/40 | [10-testing.md](queue/10-testing.md) | +| 11 | DX & tooling | TASK-411..445 | 35/35 | 0/35 | [11-dx-tooling.md](queue/11-dx-tooling.md) | +| 12 | Ecosystem stubs | TASK-446..475 | 30/30 | 0/30 | [12-ecosystem.md](queue/12-ecosystem.md) | +| 13 | Final polish | TASK-476..490 | 15/15 | 0/15 | [13-final-polish.md](queue/13-final-polish.md) | +| **TOTAL** | | | **490/490** | **0/490** | | + +## Spec format (per task) + +Every spec block carries: + +- **Section / Effort / Depends on / Type** — quick triage line. +- **Goal** — 1–2 sentences explaining the gap this closes. +- **Files** — new + edits with line ranges. +- **Reuses** — existing functions/patterns to leverage (cited by `path:line`). +- **API contract** or **Migration SQL** — when applicable. +- **Implementation sketch** — 3–8 ordered bullets. +- **Acceptance** — checklist. +- **Verification** — runnable command or concrete observable check. + +## Operations + +- **Author commits in section order**: 08 → 01 → 02 → 04 → 03 → 09 → 05 → 10 → 06 → 11 → 07 → 12 → 13 (matches the spec-write order; later sections cite earlier task IDs). +- **Push pacing**: 60 commits/day via `backend/scripts/paced-push.ts` reading `data/push_pointer.txt`. Push interval ≈ 24 minutes. +- **Status tracking**: as each implementation commit lands on `main`, mark its `[ ]` Acceptance bullets `[x]` in the section file and bump the **Implemented** column above. + +## Costs + +Authoring all 490 commits in this session is covered by the user's $100 Claude flat plan (no per-token API charges). The Hermes Railway worker only invokes `git push`, which costs $0 in API credits beyond the trivial scheduling LLM call. + +## Quality gate + +After spec-expansion completes, randomly pick 10 task IDs across sections and verify: + +- Every "Files" path exists or is plausibly new. +- Every "Reuses" reference resolves to a real symbol via grep. +- Every "Verification" line is a runnable command or concrete observable check. +- No spec is shorter than the template's required fields. + +≤1 fail = pass. + +## Non-goals + +- No implementation commits in this branch yet — specs only. +- No re-prioritization of the original 490 list. +- No automation of spec generation by sub-agents — every spec was authored directly so cited file references are accurate, not hallucinated. diff --git a/docs/backlog/queue/01-chain-consensus.md b/docs/backlog/queue/01-chain-consensus.md new file mode 100644 index 00000000..6b604081 --- /dev/null +++ b/docs/backlog/queue/01-chain-consensus.md @@ -0,0 +1,1815 @@ +# Section 01 — Chain & Consensus Specs (TASK-001..060) + +60 tasks. Block deserialization for gossip-apply, peer sync, finality, validator rotation/slashing, mempool hardening, log indexing, fork accounting, fee burning, scripts/CLIs, and a batch of read endpoints (`tps`, `block-times`, account/validator history, reorg log). + +**Preconditions used throughout this section:** +- Chain core: [Chain.ts](backend/src/blockchain/Chain.ts) — `addBlock` (line 193) routes non-canonical blocks via `forkManager.addBlock()` (line 205); `handleReorg(newBlocks, commonAncestorHeight)` at line 361 reverts state then re-applies; `getChainLength()` (line 308) returns synthetic wall-clock height. +- BlockProducer: [BlockProducer.ts](backend/src/blockchain/BlockProducer.ts) `produceBlock()` loop at line 70. +- Consensus singletons: `proofOfAI`, `difficultyManager`, `forkManager` from [Consensus.ts](backend/src/blockchain/Consensus.ts). `ForkManager.isFinalized(hash)` at line 239 uses `FORK_CHOICE_DEPTH = 6`. +- TransactionPool: [TransactionPool.ts](backend/src/blockchain/TransactionPool.ts) — `addTransaction` (line 68), `evictInvalid` (line 143), `readmitOrphaned` (line 168), `validateTransaction` (line 184). +- StateManager: [StateManager.ts](backend/src/blockchain/StateManager.ts) — `applyTransaction`, `revertBlock`, `applyBlockReward`, `commitBlock`, `getStateRoot`. +- Receipts: [TransactionReceipt.ts](backend/src/blockchain/TransactionReceipt.ts) — `createReceipt`, `storeReceipt`, `loadReceipt`, `loadBlockReceipts`, `calculateReceiptsRoot`. +- Block class: [Block.ts](backend/src/blockchain/Block.ts) — constructor `(height, parentHash, producer, transactions, difficulty)`, `isValid(prev?)`, `toJSON()`. +- Crypto: [Crypto.ts](backend/src/blockchain/Crypto.ts) — `verifyTransactionSignature(tx)`, `verify(message, sig, pubkey)`, `sha256Base58`. +- Mesh router: [backend/src/network/api.ts](backend/src/network/api.ts) — `/api/mesh/*` routes; `POST /api/mesh/block` currently returns `accepted:false`. +- Event bus: [EventBus.ts](backend/src/events/EventBus.ts) — `eventBus.emit(name, payload)`. Existing events listed in section 08 preamble. +- SSE pattern: [server.ts:971-1057](backend/src/api/server.ts#L971-L1057) — pattern for new SSE endpoints in TASK-047/048/049. + +--- + +### TASK-001 — Block.fromJSON deserializer + +**Section:** chain +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +[Block.ts](backend/src/blockchain/Block.ts) only has `toJSON()` (line 173). Gossip-apply (TASK-002) and chain import (TASK-029) need the inverse: take the JSON shape produced by `toJSON()` and reconstruct an equivalent Block instance whose `.header.hash` matches the original. + +**Files** +- edit: `backend/src/blockchain/Block.ts` — add `static fromJSON(json: any): Block` after the existing `toJSON` method. + +**Reuses** +- The Block constructor's hash calculation in [Block.ts:103-115](backend/src/blockchain/Block.ts#L103-L115). +- `setStateRoot()` (line 143) for re-applying the deserialized state root. + +**Implementation sketch** +- Validate that `json` has every required header field; throw `Error('fromJSON: missing field ')` with explicit field name on miss. +- Reconstruct each transaction by parsing `value`, `gasPrice`, `gasLimit` back from string → bigint. +- Construct `new Block(json.height, json.parentHash, json.producer, txs, json.difficulty)`. +- Override generated `timestamp`, `nonce`, `gasUsed`, `gasLimit` via direct header assignment. +- Call `setStateRoot(json.stateRoot)` so the hash recomputes. +- Override `transactionsRoot` and `receiptsRoot` to the deserialized values, then recompute hash. +- Final assertion: `block.header.hash === json.hash`; throw `Error('fromJSON: hash mismatch — header field tampered')` if not. + +**Acceptance** +- [ ] Round-trip property: `Block.fromJSON(b.toJSON()).header.hash === b.header.hash` for any locally-produced block. +- [ ] Tampered field (e.g. mutate `json.height`) → fromJSON throws `hash mismatch`. +- [ ] Empty transactions list works. + +**Verification** +- Local script: produce a block, JSON it, fromJSON it, compare every header field. + +--- + +### TASK-002 — Wire /api/mesh/block to call chain.addBlock after fromJSON + +**Section:** chain +**Effort:** S +**Depends on:** TASK-001 +**Type:** edit + +**Goal** +[network/api.ts:55-73](backend/src/network/api.ts#L55-L73) currently returns `accepted: false` because the deserializer was missing. With TASK-001, complete the gossip path: deserialize → addBlock → respond with the accept/reorg outcome. + +**Files** +- edit: `backend/src/network/api.ts:55-73` — replace the conservative stub with real acceptance. + +**Reuses** +- `Block.fromJSON` from TASK-001. +- `chain.addBlock(block)` from [Chain.ts:193](backend/src/blockchain/Chain.ts#L193). + +**API contract** +``` +POST /api/mesh/block +body: { ...Block.toJSON output... } +→ 200 { accepted: true, head: { height, hash } } +→ 409 { accepted: false, reason: 'parent unknown' | 'finalized-conflict' | 'invalid' } +→ 400 { accepted: false, reason: 'fromJSON failed: ' } +``` + +**Implementation sketch** +- Try `Block.fromJSON(req.body)` inside try/catch → 400 on throw. +- Call `chain.addBlock(block)`. +- 200 with new head if accepted; 409 with reason otherwise. +- Continue emitting `mesh_block_received` for observability. + +**Acceptance** +- [ ] Valid block from a peer at our parent → accepted. +- [ ] Block with unknown parent → 409 `parent unknown`. +- [ ] Tampered block → 400 with deserializer message. + +**Verification** +- Local two-node sim: A produces, B receives via curl. + +--- + +### TASK-003 — Header-only sync endpoint /api/mesh/headers + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Full-block sync is bandwidth-heavy. Peers that only need to verify the chain tip / measure honest-majority can pull headers only. Add a header-range fetch. + +**Files** +- edit: `backend/src/network/api.ts` — add `GET /api/mesh/headers`. + +**Reuses** +- `chain.getBlockByHeight(h)` from [Chain.ts:296](backend/src/blockchain/Chain.ts#L296). + +**API contract** +``` +GET /api/mesh/headers?from=100&to=200 +→ 200 { headers: [ { height, hash, parentHash, producer, timestamp, stateRoot, transactionsRoot, receiptsRoot, gasUsed, gasLimit, difficulty } ... ] } +→ 400 { error: 'from > to' | 'range exceeds 1000' } +``` + +**Implementation sketch** +- Hard cap range: `to - from <= 1000`. +- Loop `from..=to`, look up each block, push its header (not transactions). +- Skip-and-continue on missing heights (don't 500). +- Convert `gasUsed`/`gasLimit` bigint → string. + +**Acceptance** +- [ ] Range under 1000 returns headers. +- [ ] Range over 1000 returns 400. +- [ ] Out-of-range heights silently skipped, not erroring. + +**Verification** +- `curl /api/mesh/headers?from=0&to=5` returns 6 headers (genesis + 5). + +--- + +### TASK-004 — Bulk block fetch /api/mesh/blocks + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +TASK-003's headers tell a peer what's missing; this endpoint serves the actual blocks for sync. + +**Files** +- edit: `backend/src/network/api.ts` — add `GET /api/mesh/blocks`. + +**Reuses** +- `chain.getBlockByHeight()` and `block.toJSON()`. + +**API contract** +``` +GET /api/mesh/blocks?from=100&to=200 +→ 200 { blocks: [ { ...toJSON() } ... ] } +→ 400 { error: 'range exceeds 100' } // tighter cap than headers +``` + +**Implementation sketch** +- Cap range: `to - from <= 100` (full blocks are bigger than headers). +- Same skip-on-missing semantics as TASK-003. + +**Acceptance** +- [ ] Returns full block objects compatible with TASK-001's `fromJSON`. +- [ ] Range >100 returns 400. + +**Verification** +- Round-trip: pull blocks via this endpoint, fromJSON each, hash matches original. + +--- + +### TASK-005 — Peer head poller + +**Section:** chain +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +A peer that announces never updates its `chainHeight` again until we ask. We need a 30s poller that hits each peer's `/api/mesh/head` and refreshes their `chainHeight` in our [PeerRegistry](backend/src/network/PeerRegistry.ts). + +**Files** +- new: `backend/src/network/headPoller.ts` — exports `startHeadPoller(): {stop()}`. +- edit: `backend/src/api/server.ts` — start poller after mesh router mount. + +**Reuses** +- `peerRegistry.listPeers()` and `registerPeer({...input, lastSeenMs: now})`. +- Native `fetch()`. + +**Implementation sketch** +- Every 30s: `Promise.allSettled(peers.map(p => fetch(p.url + '/api/mesh/head')))`. +- On 200: update peer's `chainHeight` via `registerPeer()` (which doubles as a heartbeat). +- On error: do NOT decrement lastSeen; eviction loop handles dead peers. +- Skip self (compare against `HERMES_PUBLIC_URL`). + +**Acceptance** +- [ ] Two-node setup: heights converge in PeerRegistry within ~60s of one node producing a new block. +- [ ] Dead peer doesn't crash the loop. + +**Verification** +- Run two backends locally, watch peerRegistry.listPeers() over time. + +--- + +### TASK-006 — Auto-sync on start: pick highest-height peer, pull missing + +**Section:** chain +**Effort:** M +**Depends on:** TASK-001, TASK-002, TASK-004 +**Type:** new-file + +**Goal** +Cold-boot replica is at height 0 (or wherever DB left off). It should detect peers, find the highest known head, and pull missing blocks until caught up — automatically. + +**Files** +- new: `backend/src/network/syncManager.ts` — exports `runInitialSync(chain): Promise<{synced: number, from: string|null}>`. +- edit: `backend/src/api/server.ts` — invoke after `chain.initialize()` and after first peer announce returns (give it 5s for peers to reply). + +**Reuses** +- `peerRegistry.listPeers()`, `chain.getChainLength()`, `chain.addBlock()`, `Block.fromJSON()`. + +**Implementation sketch** +- Wait 5s after boot for at least one peer announce. +- If no peers, no-op (we may BE the seed). +- Else: pick `peers.sort((a,b) => b.chainHeight - a.chainHeight)[0]`. +- If their height ≤ ours, no-op. +- Loop: pull `[ourHeight+1 .. min(theirHeight, ourHeight+100)]` via `/api/mesh/blocks`, fromJSON each, addBlock. +- If addBlock returns false, log + skip (will be retried next sync tick). +- After full catch-up, log `[SYNC] caught up to height N from peer X`. + +**Acceptance** +- [ ] Empty replica + one full peer: replica catches up to peer's height. +- [ ] No peers: silent no-op, doesn't block boot. + +**Verification** +- Drop a fresh DB, start backend with bootstrap peer pointing at a populated peer. + +--- + +### TASK-007 — Reorg-on-sync: walk back to common ancestor + +**Section:** chain +**Effort:** L +**Depends on:** TASK-006 +**Type:** edit + +**Goal** +TASK-006 assumes the local chain is a strict prefix of the peer's. If they've diverged, we need to find the common ancestor (via header probes), then reorg from there. Otherwise we'd just see "parent unknown" rejections forever. + +**Files** +- edit: `backend/src/network/syncManager.ts` — extend with `findCommonAncestor(peer)` + reorg-walk path. + +**Reuses** +- `Chain.findCommonAncestor` exists at [Chain.ts:476](backend/src/blockchain/Chain.ts#L476) but works on in-memory blocks; we need a peer-side variant. +- `chain.handleReorg(newBlocks, commonAncestorHeight)` at [Chain.ts:361](backend/src/blockchain/Chain.ts#L361). + +**Implementation sketch** +- `findCommonAncestor(peerUrl)`: + - Pull peer's headers in chunks of 100, walking back from peer head. + - For each header, check if `chain.getBlockByHash(header.hash)` exists locally. + - First match is the ancestor. If walk exhausts without match, ancestor = height 0 (genesis). +- After ancestor found: pull peer's full blocks for `[ancestor+1 .. peerHead]`, then call `chain.handleReorg(newBlocks, ancestor)`. +- Add a max-depth guard (default 1000) — refuse to reorg deeper than that without operator confirmation (env `MAX_REORG_DEPTH`). + +**Acceptance** +- [ ] Two nodes diverge at height 50, sync converges them on the longer chain. +- [ ] Reorg deeper than `MAX_REORG_DEPTH` is refused with `[SYNC] refusing reorg of depth N > max M`. + +**Verification** +- Local: produce different chains on two nodes, then bring up syncManager. + +--- + +### TASK-008 — Finalized block flag at depth N-12 + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +[ForkManager.isFinalized()](backend/src/blockchain/Consensus.ts) uses depth-6. Hermeschain wants 12 (matching common L1 conventions). Centralize the constant, expose finality height in chainState, surface in `/api/status`. + +**Files** +- edit: `backend/src/blockchain/Consensus.ts` — change `FORK_CHOICE_DEPTH` from 6 → 12 (or via env `FINALITY_DEPTH`). +- edit: `backend/src/database/db.ts:225-280` (chainState) — add `setFinalizedHeight(h)`, `getFinalizedHeight()`. +- edit: `backend/src/blockchain/Chain.ts` — after each successful `addBlock`, write finalized height = `chainLength - 12` to chainState. + +**Reuses** +- `chainState` Redis-backed store. + +**Implementation sketch** +- After `addBlock` succeeds and updates `chainState.saveBlockHeight()`, additionally call `chainState.setFinalizedHeight(Math.max(0, this.getChainLength() - 12))`. +- Surface in `/api/status` payload. + +**Acceptance** +- [ ] After 100 blocks, finalized height = 88. +- [ ] Visible in `/api/status` JSON. + +**Verification** +- `curl /api/status | jq .finalizedHeight`. + +--- + +### TASK-009 — Reject reorg attempts past finality depth + +**Section:** chain +**Effort:** S +**Depends on:** TASK-008 +**Type:** edit + +**Goal** +Reorgs deeper than the finality depth violate finality guarantees. Refuse them in `addBlock` and `handleReorg`. + +**Files** +- edit: `backend/src/blockchain/Chain.ts:193,361` — pre-check reorg depth against finalized height. + +**Reuses** +- `chainState.getFinalizedHeight()` from TASK-008. +- `findCommonAncestor` at [Chain.ts:476](backend/src/blockchain/Chain.ts#L476). + +**Implementation sketch** +- In `addBlock` fork-route: before calling `forkManager.addBlock`, compute the would-be reorg depth. If `commonAncestorHeight < finalizedHeight`, log and return false with reason `finality-violation`. +- Same check in `handleReorg`. +- Caller endpoints (TASK-002) translate this to 409 with `reason: 'finalized-conflict'`. + +**Acceptance** +- [ ] Reorg at depth ≥ finality depth: refused. +- [ ] Shallow reorg: works as before. + +**Verification** +- Force a deep reorg via `/api/mesh/block` with a competing chain at depth 20; expect 409. + +--- + +### TASK-010 — VRF-style proposer rotation hash(prev_hash + height) mod n + +**Section:** chain +**Effort:** S +**Depends on:** TASK-013 (need stake column for validator listing) +**Type:** edit + +**Goal** +Current rotation in [ValidatorManager.ts](backend/src/validators/ValidatorManager.ts) uses `height % n`. That's predictable; replace with a deterministic hash-based rotation that uses parent hash entropy too: `producer = validators[ hash(prevHash + height) mod n ]`. + +**Files** +- edit: `backend/src/validators/ValidatorManager.ts` — `selectProducer` accepts `(nextHeight, parentHash)`; use sha256 → bigint → mod. +- edit: `backend/src/blockchain/BlockProducer.ts:75` — pass parent hash to selectProducer. + +**Reuses** +- `sha256Base58` from [Crypto.ts:194](backend/src/blockchain/Crypto.ts#L194). + +**Implementation sketch** +- `const seed = sha256(`${parentHash}:${nextHeight}`)`. +- `const idx = Number(BigInt('0x' + seed.slice(0, 16)) % BigInt(validatorOrder.length))`. +- For backwards-compat, when `parentHash` not provided, fall back to `height % n`. + +**Acceptance** +- [ ] Same (parentHash, height) → same producer. +- [ ] Different parentHash → distribution flat over many trials. +- [ ] No-arg call works as before. + +**Verification** +- Unit test: 1000 selections across (parentHash, height) pairs cover all validators ±10%. + +--- + +### TASK-011 — validator_slashes read endpoint + +**Section:** chain +**Effort:** S +**Depends on:** TASK-312 (table exists) +**Type:** edit + +**Goal** +Surface the slashing log built by TASK-012. Two endpoints: per-validator history and global recent. + +**Files** +- new: handlers in `backend/src/api/server.ts` (or new `backend/src/api/slashing.ts` router). + +**Reuses** +- `db.query` against `validator_slashes`. + +**API contract** +``` +GET /api/validator/:addr/slashes +→ 200 { slashes: [ { id, block_height, reason, evidence_json, stake_before, stake_after, slashed_at } ] } + +GET /api/slashing/recent?limit=50 +→ 200 { items: [...same shape, plus validator_address] } +``` + +**Implementation sketch** +- Cap `limit` at 200. +- Order DESC on `slashed_at`. + +**Acceptance** +- [ ] Empty → returns `[]`. +- [ ] After a slash event lands → entry visible. + +**Verification** +- Insert a row manually, curl endpoint. + +--- + +### TASK-012 — Slash on equivocation + +**Section:** chain +**Effort:** L +**Depends on:** TASK-011, TASK-008 +**Type:** edit + +**Goal** +If the same validator signs two different blocks at the same height (equivocation), that's a slash-able offense. Detect it and reduce stake. + +**Files** +- new: `backend/src/blockchain/equivocationDetector.ts` — exports `detectEquivocation(block: Block): Promise<{equivocated: bool, otherHash?: string}>`. +- edit: `backend/src/blockchain/Chain.ts:193` — invoke detector on every accepted block; on hit, write to `validator_slashes` and decrement stake. + +**Reuses** +- `chain.getBlockByHash`, validators table. + +**Implementation sketch** +- For each accepted block at height H, query if we already have ANOTHER hash at H signed by the same producer (in `blocks` table). +- If yes: insert into `validator_slashes` with evidence `{firstHash, secondHash, height}`. +- `UPDATE validators SET stake = stake - LEAST(stake, 10) WHERE address = $1`. +- Emit `validator_slashed` event. +- Slashing capped at zero (no negative stake). + +**Acceptance** +- [ ] Same producer at same height with different hashes → slash row inserted, stake reduced. +- [ ] Same producer at different heights → no slash. + +**Verification** +- Force two blocks at same height through addBlock with same producer; check `validator_slashes`. + +--- + +### TASK-013 — validators.stake column + weighted producer selection + +**Section:** chain +**Effort:** M +**Depends on:** TASK-311 (column exists) +**Type:** edit + +**Goal** +Combine TASK-311's column with TASK-010's hash-based rotation to make heavier-staked validators proposer more often. Stake-weighted lottery instead of uniform. + +**Files** +- edit: `backend/src/validators/ValidatorManager.ts:selectProducer` — replace mod-n with stake-weighted pick. + +**Reuses** +- `validatorOrder`, plus query `SELECT address, stake FROM validators`. + +**Implementation sketch** +- Cache stakes in memory; refresh every 100 blocks (cheap query). +- `cumulative = []`; build a running-sum array of stakes in validatorOrder order. +- `seed mod totalStake` → first index where `cumulative[i] > seed`. +- Same fallback to mod-n when stakes unavailable. + +**Acceptance** +- [ ] Validator with 2x stake produces ~2x the blocks over many rounds. +- [ ] Single-validator unchanged. + +**Verification** +- Unit test: 10k draws with stakes [1,2,3] yield ~16/33/50% distribution. + +--- + +### TASK-014 — Quorum weight by stake instead of head count + +**Section:** chain +**Effort:** M +**Depends on:** TASK-013 +**Type:** edit + +**Goal** +[ValidatorManager.getConsensus()](backend/src/validators/ValidatorManager.ts) currently counts head approvals: `Math.ceil(n * 2/3)`. With stake, threshold should be `Math.ceil(totalStake * 2/3)` and approvals tallied by stake. + +**Files** +- edit: `backend/src/validators/ValidatorManager.ts:getConsensus` — change tally + threshold logic. + +**Reuses** +- Cached stakes from TASK-013. + +**Implementation sketch** +- `const total = validators.reduce((s, v) => s + getStake(v), 0n)`. +- `const required = (total * 2n + 2n) / 3n` (BigInt ceiling-divide). +- Approval count by sum of approver stakes. +- Default-stake-1 still produces head-count behavior. + +**Acceptance** +- [ ] All-stake-1 quorum behaves identically to current. +- [ ] Validator with 51% stake can finalize alone. + +**Verification** +- Unit test with mixed stakes. + +--- + +### TASK-015 — Block timestamp drift check (>30s future = reject) + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +[Block.isValid](backend/src/blockchain/Block.ts#L153) checks monotonicity vs parent but not absolute future-time. A producer with a fast clock could mint future blocks. Reject blocks with `timestamp > now + 30000`. + +**Files** +- edit: `backend/src/blockchain/Block.ts:153-171` — add a `now` parameter, default `Date.now()`, check drift. + +**Implementation sketch** +- New signature: `isValid(previousBlock?: Block, now: number = Date.now()): boolean`. +- Add: `if (this.header.timestamp > now + 30_000) return false;`. +- Update existing callers in BlockProducer / ForkManager to pass `Date.now()` or accept default. + +**Acceptance** +- [ ] Block with future timestamp 60s ahead → isValid returns false. +- [ ] Block with timestamp now+5s → still valid. + +**Verification** +- Unit test: synthesize a future-time block. + +--- + +### TASK-016 — Min block time enforcement (<2s after parent = reject) + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +A producer with a backwards clock could mint sub-second blocks. Enforce a 2s minimum delta vs parent. + +**Files** +- edit: `backend/src/blockchain/Block.ts:isValid` — add `if (previousBlock && this.header.timestamp - previousBlock.header.timestamp < 2000) return false;`. + +**Implementation sketch** +- Replace existing strict `<=` check (line 166) with new `< 2000ms` rule. + +**Acceptance** +- [ ] Block 1000ms after parent → invalid. +- [ ] Block 5000ms after parent → valid. + +**Verification** +- Unit. + +--- + +### TASK-017 — Difficulty retarget every 100 blocks + +**Section:** chain +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +[DifficultyManager](backend/src/blockchain/Consensus.ts) has `adjustDifficulty` but it's never invoked. Periodically retarget based on observed block time vs the 10s target. + +**Files** +- edit: `backend/src/blockchain/Consensus.ts:DifficultyManager` — add `retarget(observedAvgMs: number, targetMs: number = 10_000): void`. +- edit: `backend/src/blockchain/BlockProducer.ts` — after every 100 blocks, compute observed avg over last 100 timestamps and call `retarget`. + +**Reuses** +- `chain.getRecentBlocks(100)` for the observed window. + +**Implementation sketch** +- `observedAvg = (blocks[99].ts - blocks[0].ts) / 99`. +- `factor = observedAvg / target`. +- New difficulty = clamp(`current * factor`, [1, 1e9]); adjust by at most 4× per retarget to avoid oscillation. + +**Acceptance** +- [ ] After 100 blocks at 5s avg, difficulty ~halves. +- [ ] Bounded swing (max 4× per period). + +**Verification** +- Run BlockProducer with synthetic timestamps; observe difficulty adjustment. + +--- + +### TASK-018 — Persist mempool to disk on shutdown, restore on boot + +**Section:** chain +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +Mempool sits in `pendingTransactions: Map`. Process restart drops it. Persist to disk on shutdown, restore on boot so users don't have to resubmit. + +**Files** +- edit: `backend/src/blockchain/TransactionPool.ts` — add `dumpToDisk(): Promise` and `restoreFromDisk(): Promise`. +- edit: `backend/src/api/server.ts` — call dumpToDisk in graceful-shutdown hook (already exists from prior work). +- edit: `backend/src/blockchain/TransactionPool.ts:initialize` — call restoreFromDisk after the existing init. + +**Reuses** +- `addTransaction` for re-validation on restore. +- `data/` directory. + +**Implementation sketch** +- Dump path: `data/mempool.json` containing array of tx JSON (with bigints stringified). +- On restore: read file, for each tx call `addTransaction` (which re-validates), count successful re-admits, log. +- File deleted after successful restore so partial restart doesn't double-replay. + +**Acceptance** +- [ ] Submit 5 txs, shutdown gracefully, restart, all 5 still pending. +- [ ] Crashed shutdown (no dump) → graceful no-op. + +**Verification** +- Manual restart sequence. + +--- + +### TASK-019 — Tx replacement-by-fee (same nonce, higher gasPrice replaces) + +**Section:** chain +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +Standard "RBF": if a sender submits a new tx with the same nonce as a pending one, accept only if `newGasPrice >= oldGasPrice * 1.1`. Otherwise reject as stale. + +**Files** +- edit: `backend/src/blockchain/TransactionPool.ts:addTransaction` (line 68) — pre-check for existing-nonce conflict. + +**Implementation sketch** +- On `addTransaction`, find existing tx in `pendingTransactions` with same `from` + `nonce`. +- If found: require `tx.gasPrice >= existing.gasPrice * 11n / 10n`. Else reject `replacement gas price too low`. +- If accepted: delete the old tx (and its DB row) before inserting the new. + +**Acceptance** +- [ ] Resubmit same nonce + same price → rejected. +- [ ] Resubmit same nonce + 11% higher price → old replaced. +- [ ] Resubmit same nonce + 9% higher price → rejected. + +**Verification** +- Three sequential submits with the conditions above. + +--- + +### TASK-020 — Mempool size cap 10k with lowest-gasPrice eviction + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Unbounded mempool is a DoS vector. Cap at 10k. When full, evict the lowest-gasPrice pending tx to make room (assuming the new one is higher). + +**Files** +- edit: `backend/src/blockchain/TransactionPool.ts:addTransaction` — after validation, check size; evict if needed. + +**Implementation sketch** +- `MEMPOOL_MAX = 10000` (env override `MEMPOOL_MAX_SIZE`). +- If at cap: find min-gasPrice pending; if `newTx.gasPrice > min.gasPrice`, evict min, insert new. Else reject `mempool full and price too low`. + +**Acceptance** +- [ ] Filling to 10k + 1 with high-price tx → one evicted. +- [ ] Filling to 10k + 1 with too-low tx → reject. + +**Verification** +- Stress test. + +--- + +### TASK-021 — Pending tx TTL 1h + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +[TransactionPool.clearExpired](backend/src/blockchain/TransactionPool.ts#L279) is a stub returning 0. Implement: drop pending txs older than 1h (configurable). + +**Files** +- edit: `backend/src/blockchain/TransactionPool.ts:clearExpired` — real implementation. +- edit: same — start a 1-min interval calling it. + +**Implementation sketch** +- Each pending tx already has a timestamp (`addedAt`); add the field if missing. +- `clearExpired(ageMs = 3_600_000)`: iterate, delete if `now - addedAt > ageMs`. +- Return count cleared. +- Log when count > 0. + +**Acceptance** +- [ ] Tx older than 1h disappears from pool. +- [ ] Tx within window stays. + +**Verification** +- Inject older `addedAt`, run clearExpired. + +--- + +### TASK-022 — Block size limit 1MB serialized + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Currently only gas-bounded. A pathological block could have 10k tiny txs that fit gas but blow up serialized size. Add a 1MB cap. + +**Files** +- edit: `backend/src/blockchain/BlockProducer.ts:produceBlock` — track running serialized size of pushed txs; stop including more when ≥ 1MB. + +**Implementation sketch** +- After each tx accepted into validTxs: add `Buffer.byteLength(JSON.stringify(tx))`. +- Stop when ≥ 1_048_576. +- Log `[PRODUCER] block size cap reached at X bytes`. + +**Acceptance** +- [ ] Producing a block with > 1MB worth of txs caps at the limit. + +**Verification** +- Synthetic load test. + +--- + +### TASK-023 — logs_topic0_idx index migration + +**Section:** chain +**Effort:** S +**Depends on:** TASK-310 (logs_jsonb GIN exists) +**Type:** migration + +**Goal** +TASK-310 adds GIN over the full logs_json. Topic-0 (event signature) lookups dominate; add a more specific index to make them fast. + +**Files** +- new: `backend/src/database/migrations/0017_logs_topic0_idx.sql` + +**Migration SQL** +```sql +-- up: +CREATE INDEX IF NOT EXISTS idx_receipts_logs_topic0 + ON receipts USING GIN ((logs_jsonb -> 'topics' -> 0)); + +-- down: +DROP INDEX IF EXISTS idx_receipts_logs_topic0; +``` + +**Acceptance** +- [ ] Index visible. +- [ ] `EXPLAIN SELECT * FROM receipts WHERE logs_jsonb @> '[{"topics":["0xabc"]}]'` uses it. + +--- + +### TASK-024 — /api/logs?fromBlock=&toBlock=&address=&topic0= + +**Section:** chain +**Effort:** M +**Depends on:** TASK-023 +**Type:** new-file + +**Goal** +Standard log filtering endpoint matching `eth_getLogs` semantics. Filter by block range, contract address, and indexed topic0. + +**Files** +- new: `backend/src/api/logs-query.ts` — exports router. +- edit: `backend/src/api/server.ts` — mount at `/api/logs` (separate from existing `/api/logs` which is the agent log feed; check if collision — if so use `/api/chain/logs`). + +**API contract** +``` +GET /api/chain/logs?fromBlock=100&toBlock=200&address=0xabc&topic0=0xdef&limit=100 +→ 200 { logs: [ { address, topics, data, blockNumber, transactionHash, logIndex } ] } +``` + +**Implementation sketch** +- All filter params optional except limit (default 100, max 1000). +- Build dynamic SQL using indexed columns first. +- JSONB containment for topic0; address filter via `logs_jsonb @> '[{"address":"..."}]'`. +- Block range via `WHERE block_number BETWEEN $f AND $t`. + +**Acceptance** +- [ ] No filters: returns most recent 100 logs. +- [ ] address filter: only logs from that contract. +- [ ] topic0 filter: only matching event signatures. + +**Verification** +- Curl with various filter combinations after seed data. + +--- + +### TASK-025 — /api/logs/bloom-check helper + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Block headers carry a bloom filter ([Receipt.ts:logsBloom](backend/src/blockchain/TransactionReceipt.ts)). Clients can pre-filter blocks by checking the bloom before fetching full receipts. Expose the membership check. + +**Files** +- new endpoint in `backend/src/api/logs-query.ts`. + +**Reuses** +- `bloomContains` from [TransactionReceipt.ts:75-88](backend/src/blockchain/TransactionReceipt.ts#L75-L88). + +**API contract** +``` +GET /api/chain/logs/bloom-check?height=100&item=0xabc +→ 200 { mightContain: true|false } +``` + +**Implementation sketch** +- Look up the block by height, read its `logsBloom` (combined from all receipts at calculateReceiptsRoot time — may need to also store on block header). +- Return `bloomContains(bloomHex, item)`. + +**Acceptance** +- [ ] Definitely-not-present item → false. +- [ ] Present item → true. +- [ ] Note false-positive rate inherent to bloom. + +**Verification** +- Insert a known log, check both true and false cases. + +--- + +### TASK-026 — Block uncles tracking + +**Section:** chain +**Effort:** M +**Depends on:** TASK-007 +**Type:** edit + +**Goal** +Track orphaned-but-valid blocks (uncles) for the GHOST fork-choice rule (TASK-027). Currently orphans are pruned. Keep them in a side table. + +**Files** +- new: `backend/src/database/migrations/0018_uncles.sql` — `uncles(block_hash, parent_hash, height, producer, included_in_block_hash, found_at)`. +- edit: `backend/src/blockchain/Chain.ts:handleReorg` — when reverting a block, record it as an uncle of the new canonical block. + +**Implementation sketch** +- On reorg, the orphaned blocks become uncles of the same-height canonical block. +- Insert one row per orphaned block. +- `included_in_block_hash` = canonical at that height. + +**Acceptance** +- [ ] After reorg, `uncles` table has rows for the orphaned chain. + +**Verification** +- Force reorg, query uncles. + +--- + +### TASK-027 — GHOST fork-choice weighting + +**Section:** chain +**Effort:** L +**Depends on:** TASK-026 +**Type:** edit + +**Goal** +[ForkManager.addBlock](backend/src/blockchain/Consensus.ts) currently picks longest chain. GHOST: weight = (canonical depth) + (uncle count under this subtree). The heaviest subtree wins, not just the longest. + +**Files** +- edit: `backend/src/blockchain/Consensus.ts:ForkManager.addBlock` — replace longest-chain logic with subtree-weight logic. + +**Reuses** +- `uncles` table from TASK-026. + +**Implementation sketch** +- For each fork tip, compute weight = blocks-in-fork + uncles-in-fork. +- Switch canonical iff candidate weight > current weight (strict). +- Tie-break by lower-hash for determinism. + +**Acceptance** +- [ ] Two equal-length forks, the one with more uncles becomes canonical. + +**Verification** +- Synthetic test with engineered uncle counts. + +--- + +### TASK-028 — /api/chain/export?from=&to= NDJSON stream + +**Section:** chain +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +Operators need a fast way to bulk-export a height range for backup, analytics, or seeding a new node. + +**Files** +- new endpoint in `backend/src/api/server.ts` (or a new chain-tools router). + +**API contract** +``` +GET /api/chain/export?from=0&to=100000 +→ 200 (text/plain; application/x-ndjson) + {"type":"block", ...block.toJSON()} + {"type":"receipt", ...receipt} + ... line-delimited +``` + +**Implementation sketch** +- Use Node `Readable` stream; write one JSON-line per block, then per receipt. +- Don't buffer — write+flush as you go. +- Cap range to 1M blocks (way bigger than current chain but still bounded). + +**Acceptance** +- [ ] Streams without buffering full result. +- [ ] Each line valid JSON parsable independently. + +**Verification** +- `curl /api/chain/export?from=0&to=10 | jq -c` parses cleanly. + +--- + +### TASK-029 — backend/scripts/import-chain.ts + +**Section:** chain +**Effort:** M +**Depends on:** TASK-001, TASK-028 +**Type:** script + +**Goal** +The inverse of TASK-028: read NDJSON, fromJSON each block, addBlock, plus apply receipts via storeReceipt. + +**Files** +- new: `backend/scripts/import-chain.ts` +- edit: `backend/package.json:scripts` — `"chain:import": "ts-node backend/scripts/import-chain.ts"`. + +**Implementation sketch** +- Args: `npm run chain:import -- --file path.ndjson --from-stdin`. +- Stream-parse each line, dispatch by type. +- On addBlock failure (parent unknown), buffer + retry once full file read; if still failing, exit 1. + +**Acceptance** +- [ ] Import + export round-trip preserves all blocks + receipts. + +**Verification** +- Export from one DB, import to fresh DB, compare counts. + +--- + +### TASK-030 — Genesis parameterization via genesis.json + +**Section:** chain +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +[Chain.ts](backend/src/blockchain/Chain.ts) hardcodes genesis time and producer. Move to a tracked `genesis.json` so testnets/forks can use different params without code change. + +**Files** +- new: `backend/genesis.json` — `{ chainId: "hermes-mainnet-1", genesisTimestamp: 1776067200000, genesisProducer: "...", initialAllocations: [{address, balance}, ...] }`. +- edit: `backend/src/blockchain/Chain.ts` — read genesis.json at boot, override defaults. +- edit: `backend/src/blockchain/StateManager.ts:initialize` — apply initialAllocations if state empty. + +**Implementation sketch** +- Path resolved relative to repo root or backend/ dir; env `HERMES_GENESIS_FILE` overrides. +- Strict JSON shape; throw on malformed. + +**Acceptance** +- [ ] Default genesis.json reproduces current behavior. +- [ ] Custom genesis with different chainId boots a different chain. + +**Verification** +- Cold boot with default genesis, observe height-0 state matches expectation. + +--- + +### TASK-031 — Genesis hash verification at boot + +**Section:** chain +**Effort:** S +**Depends on:** TASK-030 +**Type:** edit + +**Goal** +On boot, the chain's genesis block hash must match `genesisHash` in genesis.json. Mismatch = wrong chain config; halt boot rather than corrupt. + +**Files** +- edit: `backend/src/blockchain/Chain.ts:initialize` — compute genesis hash from in-memory genesis block, compare to genesis.json field. + +**Implementation sketch** +- `genesis.json` gets a `genesisHash` field (deterministic from other fields). +- After loading/creating genesis: assert hash equality. Throw with both hashes printed on mismatch. + +**Acceptance** +- [ ] Matching → boots silently. +- [ ] Mismatched → throws on boot, process exits. + +**Verification** +- Tweak genesis.json hash, observe boot failure. + +--- + +### TASK-032 — Validator handoff record on rotation + +**Section:** chain +**Effort:** S +**Depends on:** TASK-013 +**Type:** edit + +**Goal** +When the producer changes between blocks (rotation), record the handoff in `consensus_events` for an audit trail. + +**Files** +- edit: `backend/src/blockchain/BlockProducer.ts` — after block accepted, if `block.producer != lastBlock.producer`, INSERT a `consensus_events` row. + +**Reuses** +- Existing `consensus_events` table. + +**Implementation sketch** +- `event_type = 'producer_handoff'`, metadata `{ from, to, height }`. + +**Acceptance** +- [ ] Multi-validator chain shows handoff rows in consensus_events. + +**Verification** +- After 10 blocks with rotation, query event_type='producer_handoff'. + +--- + +### TASK-033 — Per-block VRF beacon for randomness + +**Section:** chain +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +Contracts (TASK-076 BLOCKNUMBER/TIMESTAMP/DIFFICULTY) and validators want a tamper-resistant per-block randomness beacon. Use the producer's signature over `(prevHash || height)` as the beacon. + +**Files** +- new: `backend/src/blockchain/Beacon.ts` — `computeBeacon(block, producerKeypair): string`. +- edit: `backend/src/blockchain/Block.ts:BlockHeader` — add `beacon?: string` field. +- edit: BlockProducer to attach beacon at production time. + +**Reuses** +- `sign()` from [Crypto.ts:97](backend/src/blockchain/Crypto.ts#L97). + +**Implementation sketch** +- `beacon = sign(`${prevHash}:${height}`, producerPriv)` — verifiable by anyone. +- VM consumes `beacon` for randomness opcode. + +**Acceptance** +- [ ] Beacon present on all newly-produced blocks. +- [ ] Verifiable: `verify(message, beacon, producerPub) === true`. + +**Verification** +- Inspect `block.header.beacon`. + +--- + +### TASK-034 — State pruning for zero/dead accounts + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Accounts that have been emptied (balance=0, nonce=0, no code, no storage) bloat the state. Periodically prune them. + +**Files** +- new: `backend/src/blockchain/statePruner.ts` +- edit: `backend/src/blockchain/StateManager.ts` — invoke pruner every 1000 blocks. + +**Implementation sketch** +- `DELETE FROM accounts WHERE balance = '0' AND nonce = 0 AND (code IS NULL OR code = '') AND storage IS NULL OR storage = '{}'`. +- Log count pruned. +- Skip if it would touch active recent accounts (last 100 blocks of activity) — be conservative. + +**Acceptance** +- [ ] Empty accounts vanish after pruning sweep. +- [ ] Active accounts untouched. + +**Verification** +- Manually empty an account, run pruner, observe. + +--- + +### TASK-035 — State snapshot every 10k blocks + +**Section:** chain +**Effort:** M +**Depends on:** TASK-316 (state_snapshots table) +**Type:** edit + +**Goal** +Capture full account + contract_storage state every 10k blocks for fast-sync (TASK-036). + +**Files** +- new: `backend/src/blockchain/snapshotWriter.ts` +- edit: BlockProducer to invoke after `commitBlock` when `height % 10000 === 0`. + +**Implementation sketch** +- Snapshot blob = gzipped JSON of all rows in `accounts` + `contract_storage` at this height. +- Write to `state_snapshots` table. +- Skip if snapshot already exists for that height (idempotent). + +**Acceptance** +- [ ] After 10k blocks, exactly one row in `state_snapshots`. +- [ ] Blob inflates to a JSON object with `accounts[]` and `storage[]`. + +**Verification** +- Run to height 10000, query state_snapshots. + +--- + +### TASK-036 — /api/mesh/snapshot/:height fast-sync + +**Section:** chain +**Effort:** S +**Depends on:** TASK-035 +**Type:** edit + +**Goal** +Serve the snapshot blob for a given height so a fresh peer can hydrate without replaying 10k blocks. + +**Files** +- edit: `backend/src/network/api.ts` — `GET /api/mesh/snapshot/:height`. + +**API contract** +``` +GET /api/mesh/snapshot/10000 +→ 200 (Content-Type: application/octet-stream) + +→ 404 { error: 'snapshot not found' } +``` + +**Acceptance** +- [ ] Returns blob for known snapshot heights. +- [ ] 404 otherwise. + +**Verification** +- `curl /api/mesh/snapshot/10000 | gunzip | jq '.accounts | length'`. + +--- + +### TASK-037 — Tx fee distribution: 80% producer, 20% burned + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Currently `applyBlockReward` adds the fixed BLOCK_REWARD. Tx fees are not separately accounted. Split them: 80% credited to producer (along with reward), 20% burned (subtracted from supply). + +**Files** +- edit: `backend/src/blockchain/StateManager.ts:applyBlockReward` — accept additional `feeTotal: bigint`; credit producer with `reward + (fee * 80n / 100n)`; track burn separately in chainState. +- edit: `backend/src/blockchain/BlockProducer.ts` — accumulate per-tx fees (`tx.gasPrice * gasUsed`), pass into applyBlockReward. +- edit: `chainState` — add `incrementBurn(amount: bigint)`, `getTotalBurn(): bigint`. + +**Acceptance** +- [ ] Producer receives reward + 80% of fees. +- [ ] Total burn tracked. + +**Verification** +- After known txs, compare producer balance increment to expected. + +--- + +### TASK-038 — Burn counter on chain stats + +**Section:** chain +**Effort:** S +**Depends on:** TASK-037 +**Type:** edit + +**Goal** +Surface the cumulative burn in `/api/status` and a new `/api/chain/burn` for easy querying. + +**Files** +- edit: `backend/src/api/server.ts:/api/status` — add `totalBurn` field. +- new endpoint `GET /api/chain/burn` → `{ totalBurn: '', burnRatePerBlock: '' }`. + +**Acceptance** +- [ ] After fee-bearing blocks, totalBurn > 0. + +**Verification** +- `curl /api/chain/burn`. + +--- + +### TASK-039 — Per-validator block reward via env + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +[BlockProducer.BLOCK_REWARD](backend/src/blockchain/BlockProducer.ts#L20) is a hardcoded `10e18`. Allow override via env per environment (testnet vs mainnet). + +**Files** +- edit: `backend/src/blockchain/BlockProducer.ts:18-20` — read `HERMES_BLOCK_REWARD_WEI` env. + +**Implementation sketch** +- Default unchanged. +- Parse as bigint. Throw on non-numeric. + +**Acceptance** +- [ ] No env: 10e18. +- [ ] `HERMES_BLOCK_REWARD_WEI=5000000000000000000` → 5e18. + +**Verification** +- Boot with env, watch first block reward. + +--- + +### TASK-040 — Coinbase tx representation in receipts + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Block reward is a state credit but no tx represents it. Block explorers can't show "where did this 10 OPEN come from?" Add a synthetic coinbase tx with hash `coinbase:${blockHeight}` and a receipt. + +**Files** +- edit: `backend/src/blockchain/BlockProducer.ts` — after applyBlockReward, create + storeReceipt for a synthetic coinbase tx. + +**Implementation sketch** +- Synthetic tx: `from='0x0000...coinbase'`, `to=producer.address`, `value=BLOCK_REWARD + producer fee share`, `hash=sha256('coinbase:'+blockHeight)`, `data='coinbase'`. +- Receipt at index 0; shifts other tx indices. +- Don't add to TransactionPool (it's already executed). + +**Acceptance** +- [ ] Each block has a coinbase receipt. +- [ ] Sum of receipt values = BLOCK_REWARD + 80% fees. + +**Verification** +- Curl receipts for a recent block. + +--- + +### TASK-041 — Migration: index blocks(hash) + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** migration + +**Goal** +`getBlockByHash` performs a full table scan without an index on the hash column. Add it. + +**Files** +- new: `backend/src/database/migrations/0019_blocks_hash_idx.sql` + +**Migration SQL** +```sql +-- up: +CREATE INDEX IF NOT EXISTS idx_blocks_hash ON blocks(hash); + +-- down: +DROP INDEX IF EXISTS idx_blocks_hash; +``` + +**Acceptance** +- [ ] Index visible. + +**Verification** +- `EXPLAIN SELECT * FROM blocks WHERE hash = 'abc';` uses it. + +--- + +### TASK-042 — Tx (from_address, nonce) compound index + +**Section:** chain +**Effort:** S +**Depends on:** TASK-307 (already in section 08) +**Type:** migration + +**Goal** +This task is satisfied by TASK-307 in section 08 (`backend/src/database/migrations/0003_tx_from_nonce_idx.sql`). No additional migration here — note dependency only. + +**Files** +- (covered by TASK-307) + +**Acceptance** +- [ ] When section 08 lands, this task closes automatically. + +**Verification** +- See TASK-307. + +--- + +### TASK-043 — Account-history rebuild script + +**Section:** chain +**Effort:** M +**Depends on:** none +**Type:** script + +**Goal** +If state_change events are lost (e.g. fresh DB) but blocks/receipts survive, rebuild per-account history by replaying all txs. + +**Files** +- new: `backend/scripts/rebuild-account-history.ts` +- edit: `backend/package.json:scripts` — `"chain:rebuild-history": "..."`. + +**Implementation sketch** +- Iterate blocks 0..N. +- For each tx: apply to a fresh in-memory state, log state_change. +- After full replay, write summary per address: total_in, total_out, tx_count. + +**Acceptance** +- [ ] Output matches a from-scratch chain. +- [ ] Idempotent. + +**Verification** +- Run script, sample one account, compare to live state. + +--- + +### TASK-044 — CLI: npm run verify-chain + +**Section:** chain +**Effort:** M +**Depends on:** none +**Type:** script + +**Goal** +Sanity check: walk the chain from genesis, verify (parent linkage, block hashes, signatures, state-root reproducibility). Operators run before trusting a chain copy. + +**Files** +- new: `backend/scripts/verify-chain.ts` +- edit: `backend/package.json:scripts` — `"verify-chain": "..."`. + +**Implementation sketch** +- For each block: `Block.fromJSON(toJSON()).header.hash === current.hash`. +- For each tx: `verifyTransactionSignature` returns true. +- For each receipt: matches `calculateReceiptsRoot`. +- Log pass/fail per block; final summary. +- Exit 1 on any failure. + +**Acceptance** +- [ ] Healthy chain → all pass, exit 0. +- [ ] Tampered block → fail, exit 1. + +**Verification** +- Run on dev chain. + +--- + +### TASK-045 — State root mismatch alarm event + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +If `commitBlock`'s computed state root doesn't match the value the producer baked into the block header (e.g. desync between producer and validator), emit a loud alarm. + +**Files** +- edit: `backend/src/blockchain/Chain.ts:addBlock` — after applying tx to local state, compare `stateManager.calculateStateRoot()` vs `block.header.stateRoot`. On mismatch, emit `state_root_mismatch` event. + +**Implementation sketch** +- Don't reject the block (it may be ours); just emit the event for monitoring. +- Payload: `{ blockHeight, blockHash, expected, actual, producer }`. + +**Acceptance** +- [ ] Event fires on mismatch. +- [ ] Doesn't fire on match. + +**Verification** +- Inject a mismatched root manually, observe event. + +--- + +### TASK-046 — Receipt root verification at sync + +**Section:** chain +**Effort:** S +**Depends on:** TASK-007 +**Type:** edit + +**Goal** +On gossip-applied blocks, recompute receipts root from the block's receipts and compare to header. Reject mismatches as malformed. + +**Files** +- edit: `backend/src/network/api.ts:/api/mesh/block` — after fromJSON, before addBlock, recompute receipts root, compare. Reject if mismatch. + +**Acceptance** +- [ ] Block with valid receipts root → accepted. +- [ ] Tampered receipts → rejected. + +**Verification** +- Round-trip with intact, then tampered receipts. + +--- + +### TASK-047 — SSE /api/logs/subscribe?topic0= + +**Section:** chain +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Real-time log stream filtered by topic0. Clients (HUD, contracts, oracles) subscribe and receive new logs as blocks land. + +**Files** +- new: `backend/src/api/log-stream.ts` +- mount in server.ts at `/api/chain/logs/subscribe`. + +**Reuses** +- `eventBus.on('block_produced', cb)` — extract logs from each block's receipts. +- SSE pattern from [server.ts:971-1057](backend/src/api/server.ts#L971-L1057). + +**API contract** +``` +GET /api/chain/logs/subscribe?topic0=0xabc +→ Server-Sent Events + data: { address, topics, data, blockNumber, transactionHash } +``` + +**Acceptance** +- [ ] Filter by topic0 works. +- [ ] Reconnect resumes from `Last-Event-ID`. + +**Verification** +- `curl -N /api/chain/logs/subscribe`, produce a log-bearing block, observe. + +--- + +### TASK-048 — SSE /api/mempool/subscribe + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Stream `transaction_added` and `transaction_removed` events for live mempool view. + +**Files** +- new: `backend/src/api/mempool-stream.ts` +- mount at `/api/mempool/subscribe`. + +**Reuses** +- `eventBus.on('transaction_added')` and a new `transaction_removed` event emitted by `removeTransactions`. + +**Acceptance** +- [ ] Submit tx → event arrives. +- [ ] Tx mined → removal event. + +**Verification** +- `curl -N` while submitting txs. + +--- + +### TASK-049 — SSE /api/forks/subscribe + +**Section:** chain +**Effort:** S +**Depends on:** TASK-026 +**Type:** new-file + +**Goal** +Stream `chain_reorg` events with depth + payload. + +**Files** +- new: `backend/src/api/fork-stream.ts` +- mount at `/api/forks/subscribe`. + +**Acceptance** +- [ ] Reorg → event delivered. + +**Verification** +- Force reorg, watch stream. + +--- + +### TASK-050 — Per-block aggregate gas price stats + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Each block's receipts include `gasUsed` per tx and `gasPrice` per tx. Aggregate to `{ p50, p95, max, mean }` for charting (TASK-226). + +**Files** +- new: `backend/src/api/chain-stats.ts` — `GET /api/chain/gas-stats?height=N` or `?fromHeight=&toHeight=`. + +**Implementation sketch** +- For a single block: aggregate over receipts. +- For a range: same per-block, return array. +- Sort gasPrice values, pick percentiles. + +**Acceptance** +- [ ] Returns numeric stats. + +**Verification** +- Curl after a couple blocks. + +--- + +### TASK-051 — /api/chain/tps?window=60 + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +[chain.getRecentTps](backend/src/blockchain/Chain.ts#L344) already exists. Just expose it. + +**Files** +- edit: `backend/src/api/server.ts` or `chain-stats.ts` — `GET /api/chain/tps?window=60`. + +**Reuses** +- `chain.getRecentTps(windowSec)`. + +**API contract** +``` +GET /api/chain/tps?window=60 +→ 200 { tps: 3.4, window_sec: 60 } +``` + +**Acceptance** +- [ ] Default window 60. +- [ ] Range param honored. + +**Verification** +- Curl. + +--- + +### TASK-052 — /api/chain/block-times histogram + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Distribution of block times (parent.ts → child.ts) over last N blocks. + +**Files** +- new endpoint in `backend/src/api/chain-stats.ts`. + +**API contract** +``` +GET /api/chain/block-times?limit=1000 +→ 200 { buckets: [...], counts: [...], mean: 10.2, p95: 13.0 } +``` + +**Implementation sketch** +- Pull last N blocks, compute deltas, bucket. + +**Acceptance** +- [ ] Returns histogram. + +**Verification** +- Curl after 100+ blocks. + +--- + +### TASK-053 — Validator uptime metric + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Per-validator uptime = (blocks produced when scheduled) / (blocks scheduled). Surface via existing validators endpoint. + +**Files** +- edit: `backend/src/api/server.ts:/api/validators` — extend response with `uptime: number` (0..1). + +**Implementation sketch** +- Scheduled = floor(chainHeight / numValidators) per validator (rough; precise needs producer rotation accounting). +- Actual = `blocks_produced` from `validators` table. +- `uptime = actual / scheduled`. + +**Acceptance** +- [ ] Validator with 100% production → uptime = 1. + +**Verification** +- Curl. + +--- + +### TASK-054 — Mempool depth chart endpoint + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Time-series of mempool size for the HUD chart (TASK-219). + +**Files** +- new: `backend/src/blockchain/mempoolHistory.ts` — sample every 10s, keep ring buffer of last 360 samples (1h). +- new endpoint `GET /api/mempool/history`. + +**Acceptance** +- [ ] Returns last hour of mempool depth. + +**Verification** +- Curl. + +--- + +### TASK-055 — /api/tx/simulate (VM dry-run) + +**Section:** chain +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +Run a tx through the VM without committing. Returns the would-be receipt + state changes. + +**Files** +- new endpoint `POST /api/tx/simulate` in `backend/src/api/server.ts`. + +**Reuses** +- `interpreter.execute()` from [vm/Interpreter.ts](backend/src/vm/Interpreter.ts). +- `parseVmProgram`. + +**API contract** +``` +POST /api/tx/simulate +body: { from, to, value, gasPrice, gasLimit, nonce, data } +→ 200 { gasUsed: '...', logs: [...], status: 'success'|'revert', error?: string } +``` + +**Implementation sketch** +- For VM tx: parse program, run interpreter against a snapshot of current state. +- For plain transfer: just check balance/nonce, return predicted gasUsed = 21000. + +**Acceptance** +- [ ] Simulation matches actual on-chain execution for VM tx. + +**Verification** +- Submit same tx via simulate then via /api/transactions; compare. + +--- + +### TASK-056 — /api/tx/estimate-gas + +**Section:** chain +**Effort:** S +**Depends on:** TASK-055 +**Type:** edit + +**Goal** +Subset of simulate that returns just the gasUsed estimate. + +**Files** +- new endpoint `POST /api/tx/estimate-gas`. + +**Implementation sketch** +- Wrap TASK-055 internals; return only `{ gasEstimate: '' }`. + +**Acceptance** +- [ ] Returns numeric estimate. + +**Verification** +- Curl with a sample VM tx. + +--- + +### TASK-057 — /api/account/:addr/next-nonce + +**Section:** chain +**Effort:** S +**Depends on:** TASK-307 (compound index) +**Type:** edit + +**Goal** +`next_nonce = max(chain_nonce, max_pending_nonce) + 1`. Wallets need this to avoid nonce conflicts. + +**Files** +- new endpoint in `backend/src/api/server.ts`. + +**Reuses** +- `stateManager.getNonce(addr)`, query `MAX(nonce)` over pending. + +**API contract** +``` +GET /api/account/:addr/next-nonce +→ 200 { address, nextNonce: 42 } +``` + +**Acceptance** +- [ ] No pending → returns chain_nonce + 1. +- [ ] Pending tx → returns max(pending) + 1. + +**Verification** +- Curl after submitting a few pending. + +--- + +### TASK-058 — /api/account/:addr/history paginated + +**Section:** chain +**Effort:** M +**Depends on:** TASK-307 +**Type:** edit + +**Goal** +Per-account tx history with cursor pagination. + +**Files** +- new endpoint in `backend/src/api/server.ts`. + +**API contract** +``` +GET /api/account/:addr/history?cursor=&limit=50 +→ 200 { items: [...txs...], next_cursor: "..." | null } +``` + +**Implementation sketch** +- Cursor = base64-encoded `block_height:tx_index` of last item. +- Query `WHERE (from_address = $1 OR to_address = $1) AND (block_height, tx_index) < (cursor) ORDER BY block_height DESC, tx_index DESC LIMIT $2`. + +**Acceptance** +- [ ] First page returns latest. +- [ ] Cursor walks back. + +**Verification** +- Walk through pages. + +--- + +### TASK-059 — /api/validator/:addr/blocks paginated + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +List blocks produced by a specific validator, paginated. + +**Files** +- new endpoint. + +**API contract** +``` +GET /api/validator/:addr/blocks?cursor=&limit=50 +→ 200 { items: [{height, hash, timestamp, transactionCount}], next_cursor } +``` + +**Acceptance** +- [ ] Returns blocks where producer = addr. + +**Verification** +- Curl for known validator. + +--- + +### TASK-060 — /api/chain/reorgs (last 50) + +**Section:** chain +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Persist reorg events (currently only emitted on event bus) to a `reorg_log` table; expose recent ones. + +**Files** +- new: `backend/src/database/migrations/0020_reorg_log.sql` +- new: handler in `backend/src/api/server.ts` — `GET /api/chain/reorgs?limit=50`. +- edit: `backend/src/blockchain/Chain.ts:handleReorg` — INSERT a row per reorg. + +**Migration SQL** +```sql +-- up: +CREATE TABLE IF NOT EXISTS reorg_log ( + id BIGSERIAL PRIMARY KEY, + occurred_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + depth INTEGER NOT NULL, + orphaned_count INTEGER NOT NULL, + added_count INTEGER NOT NULL, + new_height BIGINT NOT NULL, + common_ancestor_height BIGINT NOT NULL, + metadata_json TEXT +); +CREATE INDEX IF NOT EXISTS idx_reorg_log_occurred + ON reorg_log(occurred_at DESC); + +-- down: +DROP INDEX IF EXISTS idx_reorg_log_occurred; +DROP TABLE IF EXISTS reorg_log; +``` + +**Acceptance** +- [ ] Reorg records inserted. +- [ ] Endpoint returns recent. + +**Verification** +- Force reorg, curl endpoint. + +--- + +## Summary + +60 tasks: 38 small, 18 medium, 4 large. Mix of new endpoints (~20), edits to existing chain code (~25), new modules (~10), and a handful of migrations + scripts. diff --git a/docs/backlog/queue/02-vm.md b/docs/backlog/queue/02-vm.md new file mode 100644 index 00000000..d6cf7555 --- /dev/null +++ b/docs/backlog/queue/02-vm.md @@ -0,0 +1,1230 @@ +# Section 02 — VM Specs (TASK-061..105) + +45 tasks. Extends the JSON-op interpreter from [Interpreter.ts](backend/src/vm/Interpreter.ts) into a usable contract VM: arithmetic + comparisons + control flow + storage + cross-contract calls + events + memory + deployment + tooling. Targets correctness over performance. + +**Preconditions used throughout this section:** +- VM core: [Interpreter.ts](backend/src/vm/Interpreter.ts) — `execute(ops, gasLimit, ctx)` returns `{status, gasUsed, logs, storage, error?}`. Op shape `{ op: 'NAME', args?: any }`. +- Gas: [GasMeter.ts](backend/src/vm/GasMeter.ts) — `GAS_COSTS` table, `meter.charge(amount): bool`, `logGasCost(bytes)`. +- Re-export: [vm/index.ts](backend/src/vm/index.ts). +- Block dispatch: [BlockProducer.ts:115-160](backend/src/blockchain/BlockProducer.ts#L115-L160) — VM tx detection via `parseVmProgram(tx.data)`. +- Receipts: [TransactionReceipt.ts:Log](backend/src/blockchain/TransactionReceipt.ts) interface. +- Storage table from TASK-314: `contract_storage(contract_address, storage_key, storage_value, updated_at_block)`. +- Code table from TASK-313: `contract_code(address, code_hash, bytecode, deployed_at_block, deployed_by)`. + +--- + +### TASK-061 — MUL opcode + cost + +**Section:** vm +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Multiply top two stack items, push result. Standard arithmetic. + +**Files** +- edit: [Interpreter.ts](backend/src/vm/Interpreter.ts) — add `MUL` case. +- edit: [GasMeter.ts](backend/src/vm/GasMeter.ts) — `MUL: 5n` in `GAS_COSTS`. + +**Implementation sketch** +- `case 'MUL': charge(GAS_COSTS.MUL); a = pop, b = pop; push(a * b);`. +- Type widening: cast both to `BigInt` so overflow doesn't lose precision; final push as Number if safe. + +**Acceptance** +- [ ] `[PUSH 3, PUSH 4, MUL]` → top of stack = 12. +- [ ] Out-of-gas at MUL → revert. + +**Verification** +- Add to VM unit fixtures (TASK-102). + +--- + +### TASK-062 — DIV opcode + +**Section:** vm +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Integer division. `b = pop; a = pop; push(a / b)`. Divide-by-zero → revert (not panic). + +**Files** +- edit: [Interpreter.ts](backend/src/vm/Interpreter.ts) — add `DIV` case. +- edit: [GasMeter.ts](backend/src/vm/GasMeter.ts) — `DIV: 5n`. + +**Implementation sketch** +- Charge gas before pop. +- If `b === 0`, return `{status:'revert', error: 'division by zero'}`. +- Floor-division semantics for negatives. + +**Acceptance** +- [ ] `[PUSH 10, PUSH 3, DIV]` → 3. +- [ ] `[PUSH 1, PUSH 0, DIV]` → revert with reason. + +**Verification** +- Unit fixture. + +--- + +### TASK-063 — MOD opcode + +**Section:** vm +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Modulo. Same divide-by-zero handling as DIV. + +**Files** +- edit: Interpreter.ts + GasMeter.ts. + +**Implementation sketch** +- `MOD: 5n`. `push(a % b)`. + +**Acceptance** +- [ ] `[PUSH 10, PUSH 3, MOD]` → 1. + +**Verification** +- Unit. + +--- + +### TASK-064 — EQ / LT / GT comparison ops + +**Section:** vm +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Three comparison opcodes. Push 1 for true, 0 for false. Standard for downstream JUMPI. + +**Files** +- edit: Interpreter.ts (3 cases) + GasMeter.ts (`EQ/LT/GT: 3n`). + +**Implementation sketch** +- For each: pop two, compare, push 0 or 1. +- LT and GT compare numerically (cast to bigint). + +**Acceptance** +- [ ] `[PUSH 5, PUSH 5, EQ]` → 1. +- [ ] `[PUSH 3, PUSH 5, LT]` → 1. + +**Verification** +- Unit. + +--- + +### TASK-065 — AND / OR / NOT bitwise ops + +**Section:** vm +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Bitwise booleans for flag manipulation. + +**Files** +- edit: Interpreter.ts + GasMeter.ts. + +**Implementation sketch** +- `AND: 3n`, `OR: 3n`, `NOT: 3n`. +- Operate on bigints via `&`, `|`, `~` semantics (NOT inverts low 256 bits). + +**Acceptance** +- [ ] Truth-table tests pass. + +**Verification** +- Unit. + +--- + +### TASK-066 — JUMP / JUMPI control flow + +**Section:** vm +**Effort:** M +**Depends on:** TASK-067 +**Type:** edit + +**Goal** +Unconditional and conditional jumps. Branch to op index N if condition is non-zero (JUMPI) or always (JUMP). + +**Files** +- edit: Interpreter.ts — add JUMP, JUMPI handlers; refactor execute() loop to use a `pc` (program counter) variable. + +**Reuses** +- JUMPDEST validation from TASK-067. + +**Implementation sketch** +- Replace the `for (i; ...)` with `let pc = 0; while (pc < ops.length)`. +- `JUMP { args: [target] }`: `pc = target` (must be a JUMPDEST per TASK-067). +- `JUMPI { args: [target] }`: pop condition; if non-zero, `pc = target`; else `pc++`. +- Out-of-bounds target → revert. +- Gas: `JUMP: 8n`, `JUMPI: 10n`. + +**Acceptance** +- [ ] Loop fixture: `[PUSH 0 PUSH 10 JUMPDEST DUP1 PUSH 1 ADD JUMPI 3 STOP]` (or similar) terminates correctly. + +**Verification** +- Unit. + +--- + +### TASK-067 — JUMPDEST validation pass + +**Section:** vm +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Compute the set of valid jump targets at execution start. JUMP/JUMPI to any other index reverts. + +**Files** +- edit: Interpreter.ts — at execute() start, scan for `op==='JUMPDEST'`, build `Set`. + +**Implementation sketch** +- Add `JUMPDEST` opcode (`1n` gas, no-op). +- Pre-scan: `const jumpdests = new Set(ops.map((o,i) => o.op==='JUMPDEST' ? i : -1).filter(i => i >= 0))`. +- Pass to JUMP/JUMPI handlers. + +**Acceptance** +- [ ] Jump to non-JUMPDEST → revert. +- [ ] Jump to JUMPDEST → succeeds. + +**Verification** +- Unit. + +--- + +### TASK-068 — SLOAD opcode (read storage) + +**Section:** vm +**Effort:** M +**Depends on:** TASK-314 (contract_storage table) +**Type:** edit + +**Goal** +Read contract storage by key. Pop key from stack, push value (or 0 if unset). + +**Files** +- edit: Interpreter.ts — add SLOAD; need DB read at execution time. + +**Implementation sketch** +- Async-ify `execute()` (becomes `Promise`). +- `SLOAD { args: [] }`: pop key. `SELECT storage_value FROM contract_storage WHERE contract_address=$1 AND storage_key=$2`. Push value or 0. +- Gas: `SLOAD_COLD: 800n`, `SLOAD_WARM: 100n`. Track warm set per execution. + +**Acceptance** +- [ ] Read after SSTORE returns the value. +- [ ] Read of unset key returns 0. +- [ ] Cold-then-warm pricing observed in gasUsed. + +**Verification** +- Integration: SSTORE then SLOAD in same program; receipt logs show expected value. + +--- + +### TASK-069 — Storage persistence to contract_storage table + +**Section:** vm +**Effort:** M +**Depends on:** TASK-314 +**Type:** edit + +**Goal** +Currently SSTORE writes to in-memory `storage` map and is discarded after execution. Persist to PG so SLOAD across blocks works. + +**Files** +- edit: Interpreter.ts — SSTORE handler upserts to `contract_storage`. +- edit: BlockProducer.ts — only commit storage writes if execution succeeded (revert → discard). + +**Implementation sketch** +- Buffer writes in execution result's `storage` object. +- After execute() returns, if status === 'success', `INSERT ... ON CONFLICT DO UPDATE` for each key. +- Updated_at_block = current block height. + +**Acceptance** +- [ ] After successful SSTORE block + restart, value still readable via SLOAD. +- [ ] Reverted execution doesn't persist. + +**Verification** +- Two-block test: write, restart, read. + +--- + +### TASK-070 — CALL opcode + +**Section:** vm +**Effort:** L +**Depends on:** TASK-068, TASK-082 +**Type:** edit + +**Goal** +Invoke another contract. Pop (gas, address, value, calldata) from stack; execute target's bytecode; push success flag. + +**Files** +- edit: Interpreter.ts — add CALL handler with nested execution. + +**Implementation sketch** +- `CALL { args: [] }`: pop `gas, addr, value, calldata`. +- Look up target contract code via `SELECT bytecode FROM contract_code WHERE address=$1`. +- If no code: treat as plain transfer (debit caller, credit target by `value`). +- Else: parse target program, recursively `execute(targetOps, gas, { ...ctx, contractAddress: addr })`. +- Track call depth (TASK-093 cap). +- Push 1 on success, 0 on revert. + +**Acceptance** +- [ ] CALL to contract that emits LOG → log appears in caller's receipt. +- [ ] CALL to non-existent contract → success-as-transfer. + +**Verification** +- Two-contract integration test. + +--- + +### TASK-071 — RETURN opcode + return-data buffer + +**Section:** vm +**Effort:** M +**Depends on:** TASK-070 +**Type:** edit + +**Goal** +Allow callee to return data to caller. RETURN pops a value and ends execution; CALL exposes it via RETURNDATA opcode (sub-task). + +**Files** +- edit: Interpreter.ts — add RETURN; track lastReturnData on execution context. + +**Implementation sketch** +- `RETURN { args: [] }`: pop value, set `result.returnData = value`, terminate. +- Caller's CALL handler reads `result.returnData` after sub-execution. +- Add `RETURNDATA` opcode that pushes the last returndata. + +**Acceptance** +- [ ] CALL → sub-RETURN with value → caller's RETURNDATA pushes that value. + +**Verification** +- Two-contract test. + +--- + +### TASK-072 — CALLDATA opcode + +**Section:** vm +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Push the calldata that this execution was invoked with. For top-level tx, calldata = whatever followed `vm:` in tx.data. + +**Files** +- edit: Interpreter.ts. + +**Implementation sketch** +- ExecutionContext gets `calldata: any`. +- BlockProducer passes the un-prefixed parsed calldata when present. +- `CALLDATA { args: [] }`: push ctx.calldata (as JSON-encoded if non-primitive). + +**Acceptance** +- [ ] Tx with `data: 'vm:[...ops]'` and program using CALLDATA gets the parsed program back. (More useful: define a calldata field separate from program — see TASK-079.) + +**Verification** +- Unit. + +--- + +### TASK-073 — CALLER / ORIGIN ops + +**Section:** vm +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Push the immediate caller's address (CALLER) or the originating tx's `from` address (ORIGIN). Distinct under CALL chains. + +**Files** +- edit: Interpreter.ts. + +**Implementation sketch** +- ExecutionContext gains `caller: string` and `origin: string`. +- For top-level tx: caller === origin === tx.from. +- For CALL: caller = the executing contract; origin unchanged. + +**Acceptance** +- [ ] Top-level: CALLER === ORIGIN === tx.from. +- [ ] Nested: CALLER differs from ORIGIN. + +**Verification** +- Unit. + +--- + +### TASK-074 — VALUE op (msg.value) + +**Section:** vm +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Push the value transferred to this execution. + +**Files** +- edit: Interpreter.ts. + +**Implementation sketch** +- ExecutionContext gains `value: bigint`. +- VALUE pushes `ctx.value`. + +**Acceptance** +- [ ] Tx with value 100 → VALUE returns 100. + +**Verification** +- Unit. + +--- + +### TASK-075 — BALANCE(addr) op + +**Section:** vm +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Pop address, push that account's balance. + +**Files** +- edit: Interpreter.ts — needs DB lookup via stateManager. + +**Reuses** +- `stateManager.getBalance(addr)`. + +**Implementation sketch** +- Gas: 700n. +- `BALANCE { args: [] }`: pop addr, `push(stateManager.getBalance(addr))`. + +**Acceptance** +- [ ] Returns correct balance. + +**Verification** +- Unit. + +--- + +### TASK-076 — BLOCKNUMBER / TIMESTAMP / DIFFICULTY ops + +**Section:** vm +**Effort:** S +**Depends on:** TASK-033 (beacon for randomness via DIFFICULTY) +**Type:** edit + +**Goal** +Standard block-context opcodes. + +**Files** +- edit: Interpreter.ts. + +**Implementation sketch** +- All 3 push from ctx (blockNumber, blockTimestamp, blockDifficulty). +- Provide values from BlockProducer's per-tx context construction. +- DIFFICULTY can be aliased to use the per-block beacon for randomness sources. + +**Acceptance** +- [ ] Each opcode returns the expected block field. + +**Verification** +- Unit + integration. + +--- + +### TASK-077 — SHA256 / KECCAK precompile + +**Section:** vm +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Hash precompiles. Pop input, push 32-byte hash. + +**Files** +- edit: Interpreter.ts — add SHA256, KECCAK opcodes. + +**Reuses** +- Node `crypto.createHash`. + +**Implementation sketch** +- Gas: 60n + 12n per word. +- KECCAK uses SHA3-256 (or actual keccak via `js-sha3` if Solidity-compat). + +**Acceptance** +- [ ] Hash of empty input matches expected. + +**Verification** +- Unit comparing to known hash values. + +--- + +### TASK-078 — ECRECOVER precompile + +**Section:** vm +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +Recover signer address from (msg, sig). Used by signature-based logic in contracts. + +**Files** +- edit: Interpreter.ts. + +**Reuses** +- Ed25519 verification from [Crypto.ts:119](backend/src/blockchain/Crypto.ts#L119) — note this is verify, not recover. For Ed25519, key recovery isn't direct; pop (msg, sig, pubkey) and push the verify result. Document the Ed25519-vs-secp256k1 deviation in spec. + +**Implementation sketch** +- `ECRECOVER` pops 3 args, verifies, pushes the pubkey if valid, 0 otherwise. + +**Acceptance** +- [ ] Valid sig → pubkey. +- [ ] Invalid → 0. + +**Verification** +- Unit. + +--- + +### TASK-079 — CREATE opcode (contract deployment) + +**Section:** vm +**Effort:** L +**Depends on:** TASK-313 (contract_code table) +**Type:** edit + +**Goal** +Deploy a new contract. Pop value + bytecode + salt; compute address; insert into contract_code; return new address. + +**Files** +- edit: Interpreter.ts. + +**Reuses** +- TASK-080's address derivation. + +**Implementation sketch** +- `CREATE { args: [] }`: pop value, bytecode, optional salt. +- Compute address from sender + nonce (or sender + salt for CREATE2-like determinism). +- INSERT into contract_code. +- Push new address. +- Gas: 32000n base + per-byte storage cost. + +**Acceptance** +- [ ] CREATE deploys a contract; subsequent CALL to that address executes its code. + +**Verification** +- Two-tx test: deploy then call. + +--- + +### TASK-080 — Contract address derivation keccak(sender + nonce) + +**Section:** vm +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Pure deterministic function used by CREATE. + +**Files** +- new: `backend/src/vm/contractAddress.ts` — exports `deriveContractAddress(sender: string, nonce: number): string`. + +**Implementation sketch** +- `sha256(`${sender}:${nonce}`).slice(0, 44)` (base58, address-shaped). +- Document: NOT EVM-compatible; documented as Hermes-specific. + +**Acceptance** +- [ ] Same inputs → same address. +- [ ] Different inputs → different addresses. + +**Verification** +- Unit. + +--- + +### TASK-081 — contract_code table storage helpers + +**Section:** vm +**Effort:** S +**Depends on:** TASK-313 +**Type:** new-file + +**Goal** +Wrap insert/lookup of contract bytecode in helpers. + +**Files** +- new: `backend/src/vm/codeStore.ts` — `storeCode(addr, hash, bytecode, deployedAtBlock, deployedBy)`, `loadCode(addr)`. + +**Reuses** +- `db.query`. + +**Implementation sketch** +- Compute code_hash via SHA256 of bytecode. +- Cache loadCode results in-memory (LRU, 1k entries). + +**Acceptance** +- [ ] Store + load round-trip. + +**Verification** +- Unit. + +--- + +### TASK-082 — Code-loaded execution path in BlockProducer + +**Section:** vm +**Effort:** M +**Depends on:** TASK-081 +**Type:** edit + +**Goal** +Currently BlockProducer dispatches to VM only when `tx.data.startsWith('vm:')`. Also need: if `tx.to` has deployed code, execute that code (calldata = tx.data). + +**Files** +- edit: BlockProducer.ts:115-160. + +**Reuses** +- `loadCode(addr)` from TASK-081. + +**Implementation sketch** +- After verifying signature: `const targetCode = await loadCode(tx.to);`. +- If targetCode exists: parse, execute with calldata = `tx.data`. +- Else if `tx.data.startsWith('vm:')`: existing path. +- Else: plain transfer (existing path). + +**Acceptance** +- [ ] Tx to contract address invokes contract code. + +**Verification** +- Deploy + call sequence. + +--- + +### TASK-083 — /api/tx/:hash/trace endpoint + +**Section:** vm +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +Re-execute a tx with full per-op tracing for debugging. Returns the op sequence with stack snapshots. + +**Files** +- new: `backend/src/api/vm-trace.ts`. + +**Reuses** +- TASK-055's tx simulation infrastructure. + +**API contract** +``` +GET /api/tx/:hash/trace +→ 200 { steps: [ { pc, op, gasBefore, gasAfter, stackBefore, stackAfter } ] } +``` + +**Implementation sketch** +- Look up tx + parent state. +- Run interpreter with `trace: true` flag that records each step. +- Return steps array. + +**Acceptance** +- [ ] Trace shape matches API contract. + +**Verification** +- Trace a known tx, walk steps. + +--- + +### TASK-084 — /api/tx/:hash/gas-profile per-op breakdown + +**Section:** vm +**Effort:** S +**Depends on:** TASK-083 +**Type:** edit + +**Goal** +Aggregate per-op gas usage from the trace. + +**Files** +- new endpoint or extend TASK-083 with `?profile=true`. + +**Implementation sketch** +- Walk trace, sum gas per opcode name. +- Return `{ MUL: 1500, SLOAD: 8000, ... }`. + +**Acceptance** +- [ ] Returns gas histogram by op. + +**Verification** +- Curl on a known tx. + +--- + +### TASK-085 — STATICCALL (read-only nested call) + +**Section:** vm +**Effort:** M +**Depends on:** TASK-070 +**Type:** edit + +**Goal** +Like CALL but disallows storage writes in the sub-execution. + +**Files** +- edit: Interpreter.ts. + +**Implementation sketch** +- Set `ctx.readOnly = true` during sub-execution. +- SSTORE / CREATE / SELFDESTRUCT in read-only ctx → revert. + +**Acceptance** +- [ ] STATICCALL into contract that tries SSTORE → revert. +- [ ] STATICCALL into pure-read contract → success. + +**Verification** +- Two-contract test. + +--- + +### TASK-086 — SELFDESTRUCT op + +**Section:** vm +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Mark contract for removal at end of block; refund balance to recipient address (popped). + +**Files** +- edit: Interpreter.ts. + +**Implementation sketch** +- Pop recipient address. +- `result.selfDestruct = { recipient }`. +- BlockProducer post-tx: if selfDestruct set, transfer balance to recipient and DELETE FROM contract_code. + +**Acceptance** +- [ ] After SELFDESTRUCT, contract code gone, balance moved. + +**Verification** +- Integration. + +--- + +### TASK-087 — SSTORE refund for zero-set + +**Section:** vm +**Effort:** S +**Depends on:** TASK-069 +**Type:** edit + +**Goal** +EVM-style: setting a non-zero slot to zero refunds gas (bounded). Encourages cleanup. + +**Files** +- edit: Interpreter.ts SSTORE handler. + +**Implementation sketch** +- If `currentValue !== 0` and `newValue === 0`: refund 15000n at end of execution (capped at 1/2 of gasUsed). + +**Acceptance** +- [ ] SSTORE 0 over a non-zero slot reduces final gasUsed. + +**Verification** +- Unit. + +--- + +### TASK-088 — Cold/warm SLOAD pricing + +**Section:** vm +**Effort:** S +**Depends on:** TASK-068 +**Type:** edit + +**Goal** +First read of a slot in a tx is "cold" (high gas); subsequent reads are "warm" (low). Already sketched in TASK-068; this task makes the pricing real. + +**Files** +- edit: Interpreter.ts. + +**Implementation sketch** +- Per-execution `Set warmSlots`. +- `cost = warmSlots.has(key) ? 100n : 800n; warmSlots.add(key);`. + +**Acceptance** +- [ ] First SLOAD cost 800; second cost 100. + +**Verification** +- Unit. + +--- + +### TASK-089 — Memory model: byte-addressable scratch + +**Section:** vm +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +Add a per-execution memory buffer (Uint8Array) addressable by byte offset. Used by hashing precompiles and contract internals. + +**Files** +- edit: Interpreter.ts. + +**Implementation sketch** +- `let memory = new Uint8Array(0)`. +- Grow on demand to nearest 32-byte boundary; track `memorySize` for gas accounting (TASK-091). + +**Acceptance** +- [ ] Memory accessible (after MLOAD/MSTORE in TASK-090). + +**Verification** +- Unit via TASK-090. + +--- + +### TASK-090 — MLOAD / MSTORE / MSTORE8 + +**Section:** vm +**Effort:** S +**Depends on:** TASK-089 +**Type:** edit + +**Goal** +Load/store memory. + +**Files** +- edit: Interpreter.ts. + +**Implementation sketch** +- MLOAD: pop offset, push 32-byte value as bigint. +- MSTORE: pop offset + value, write 32 bytes. +- MSTORE8: pop offset + value, write 1 byte (lowest byte of value). + +**Acceptance** +- [ ] Store + load round-trip preserves value. + +**Verification** +- Unit. + +--- + +### TASK-091 — Memory expansion gas + +**Section:** vm +**Effort:** S +**Depends on:** TASK-089 +**Type:** edit + +**Goal** +Charge for memory growth: cost = quadratic in size to discourage abuse. + +**Files** +- edit: Interpreter.ts memory grow path. + +**Implementation sketch** +- `cost(N) = 3n * N + N*N / 512n` where N = memory words. +- Charge difference between old and new cost on every grow. + +**Acceptance** +- [ ] Growing memory consumes gas. + +**Verification** +- Unit: write at high offset, observe gas spike. + +--- + +### TASK-092 — Stack depth limit 1024 + +**Section:** vm +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Prevent unbounded stack growth. + +**Files** +- edit: Interpreter.ts each push site. + +**Implementation sketch** +- Wrap stack push: `if (stack.length >= 1024) return revert('stack overflow')`. + +**Acceptance** +- [ ] Push 1025 items → revert at item 1025. + +**Verification** +- Unit. + +--- + +### TASK-093 — Call depth limit 1024 + +**Section:** vm +**Effort:** S +**Depends on:** TASK-070 +**Type:** edit + +**Goal** +Prevent infinite recursion via CALL. + +**Files** +- edit: Interpreter.ts CALL handler. + +**Implementation sketch** +- ExecutionContext tracks `depth: number`. +- CALL: `if (depth >= 1024) revert('call depth exceeded')`. +- Sub-execute with `depth + 1`. + +**Acceptance** +- [ ] 1025-deep CALL chain → revert at depth 1025. + +**Verification** +- Unit. + +--- + +### TASK-094 — REVERT with return data + +**Section:** vm +**Effort:** S +**Depends on:** TASK-071 +**Type:** edit + +**Goal** +Currently REVERT just sets an error string. Allow it to also pop return data so caller can read it. + +**Files** +- edit: Interpreter.ts REVERT handler. + +**Implementation sketch** +- `REVERT { args: [] }`: pop return value, set `result.returnData`, set status 'revert'. + +**Acceptance** +- [ ] CALL into reverting sub: caller's RETURNDATA returns the revert data. + +**Verification** +- Unit. + +--- + +### TASK-095 — Try-catch CALL semantics + +**Section:** vm +**Effort:** S +**Depends on:** TASK-070, TASK-094 +**Type:** edit + +**Goal** +CALL already returns success flag (TASK-070). Document try-catch as: caller checks the flag and branches; the language stub (TASK-104) emits the right code. + +**Files** +- new: `docs/vm/try-catch.md` (separate from /docs/vm spec) — convention doc. + +**Implementation sketch** +- No new opcode; convention only. + +**Acceptance** +- [ ] Doc explains pattern with example. + +**Verification** +- Doc renders. + +--- + +### TASK-096 — EVENT opcode separate from LOG + +**Section:** vm +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +LOG is EVM-style untyped. EVENT is named/typed (resolved against ABI registry from TASK-097). Cleaner DX. + +**Files** +- edit: Interpreter.ts. + +**Implementation sketch** +- `EVENT { args: { name: string, fields: Record } }`. +- Look up event signature in ABI registry; encode topics; emit as LOG under the hood. +- Gas: same as LOG. + +**Acceptance** +- [ ] EVENT 'Transfer' { from, to, value } → log row with correct topic0. + +**Verification** +- Unit + integration with TASK-097. + +--- + +### TASK-097 — Event ABI registry per contract + +**Section:** vm +**Effort:** M +**Depends on:** TASK-315 (contract_metadata table) +**Type:** new-file + +**Goal** +Per-contract registry of event names → topic0 hashes + field schemas. Used by EVENT (TASK-096) and decode endpoints. + +**Files** +- new: `backend/src/vm/abiRegistry.ts` — `registerAbi(contractAddress, abiJson)`, `lookupEvent(contractAddress, eventName)`. + +**Reuses** +- `contract_metadata.abi_json` from TASK-315. + +**Implementation sketch** +- ABI schema: `{ events: [ { name, fields: [{name, type}] } ] }`. +- topic0 = keccak(`${name}(${fields.map(f=>f.type).join(',')})`).slice(0, 32 bytes). +- Cache parsed ABI in-memory. + +**Acceptance** +- [ ] After register, lookup returns expected topic0. + +**Verification** +- Unit. + +--- + +### TASK-098 — /api/contract/:addr/source verifier + +**Section:** vm +**Effort:** M +**Depends on:** TASK-315 +**Type:** edit + +**Goal** +Allow uploading source for a deployed contract; recompile (via TASK-104 stub compiler), compare bytecode hash; mark verified. + +**Files** +- new: `backend/src/api/contract-verify.ts`. + +**API contract** +``` +POST /api/contract/:addr/source +body: { source: '...DSL source...' } +→ 200 { verified: true } +→ 409 { verified: false, reason: 'bytecode mismatch' } +``` + +**Implementation sketch** +- Compile source via TASK-104. +- Compare hash with `contract_code.code_hash`. +- On match: UPDATE contract_metadata SET source_verified = TRUE. + +**Acceptance** +- [ ] Matching source → verified. + +**Verification** +- Round-trip: deploy via known source, verify. + +--- + +### TASK-099 — /api/contract/:addr/disasm endpoint + +**Section:** vm +**Effort:** S +**Depends on:** TASK-313 +**Type:** edit + +**Goal** +Return the JSON-op program for a deployed contract. + +**Files** +- new endpoint. + +**API contract** +``` +GET /api/contract/:addr/disasm +→ 200 { ops: [ {op, args} ... ] } +``` + +**Acceptance** +- [ ] Returns parsed program. + +**Verification** +- Curl. + +--- + +### TASK-100 — /api/contract/:addr/storage browser + +**Section:** vm +**Effort:** S +**Depends on:** TASK-314 +**Type:** edit + +**Goal** +List a contract's stored slots with optional key prefix. + +**Files** +- new endpoint. + +**API contract** +``` +GET /api/contract/:addr/storage?prefix=&cursor=&limit=100 +→ 200 { items: [ {key, value, updated_at_block} ], next_cursor } +``` + +**Acceptance** +- [ ] Returns rows from `contract_storage`. + +**Verification** +- Curl. + +--- + +### TASK-101 — contract_metadata table is sufficient + +**Section:** vm +**Effort:** S +**Depends on:** TASK-315 +**Type:** docs + +**Goal** +This task overlaps with TASK-315; closed once that migration lands. Document the linkage. + +**Files** +- edit: queue.md index — annotate TASK-101 as covered by TASK-315. + +**Acceptance** +- [ ] Linkage documented. + +**Verification** +- N/A. + +--- + +### TASK-102 — VM unit test fixtures (30 sample programs) + +**Section:** vm +**Effort:** M +**Depends on:** none +**Type:** test + +**Goal** +30 small JSON-op programs exercising every opcode in golden-test fashion: input → expected (status, gasUsed, logs, storage). + +**Files** +- new: `backend/tests/vm-fixtures/*.json` (30 files). +- new: `backend/tests/vm-fixtures.test.ts` — runs interpreter against each fixture, asserts output. + +**Implementation sketch** +- Each fixture: `{ ops, gasLimit, ctx, expected: { status, gasUsed, logs, storage } }`. +- Cover: arithmetic (5), comparisons (5), control flow (5), storage (5), CALL/RETURN (5), edge cases (5). + +**Acceptance** +- [ ] All 30 pass. + +**Verification** +- `npm test`. + +--- + +### TASK-103 — /docs/vm spec page + +**Section:** vm +**Effort:** M +**Depends on:** TASK-061..102 +**Type:** docs + +**Goal** +Authoritative reference: every opcode, gas cost, semantics, edge cases. + +**Files** +- new: `docs/vm/spec.md`. + +**Implementation sketch** +- Table: opcode | gas | stack effect | description. +- Examples for non-trivial ops. +- Versioning note. + +**Acceptance** +- [ ] Doc covers every opcode in Interpreter.ts. + +**Verification** +- Manual review. + +--- + +### TASK-104 — Compiler stub: tiny DSL → JSON-op + +**Section:** vm +**Effort:** L +**Depends on:** TASK-061..094 +**Type:** new-file + +**Goal** +A toy higher-level language → JSON-op compiler so users can write `let x = 1 + 2; emit(x);` instead of raw stack manipulation. Just enough to demo: assignments, arithmetic, if/while, function call, emit. + +**Files** +- new: `backend/src/vm/compiler/` — lexer, parser, emitter. +- new: `backend/scripts/vm-compile.ts` — CLI wrapper. + +**Implementation sketch** +- Lexer: tokens (NUMBER, IDENT, OP, KEYWORD). +- Parser: recursive descent → AST. +- Emitter: AST → JSON-op array. +- Stack-machine register allocation (no SSA needed at this scale). + +**Acceptance** +- [ ] Counter contract compiles and runs. + +**Verification** +- `npm run vm:compile sample.hsm` → `sample.json` with valid program. + +--- + +### TASK-105 — Sample contracts: counter / erc20-like / multisig / vrf + +**Section:** vm +**Effort:** M +**Depends on:** TASK-104 +**Type:** docs + +**Goal** +Four worked examples in the new DSL with deployable JSON output and walkthrough docs. + +**Files** +- new: `examples/counter/{source.hsm,program.json,README.md}` +- new: `examples/erc20-like/...` +- new: `examples/multisig/...` +- new: `examples/vrf/...` + +**Implementation sketch** +- Counter: increment + read. +- ERC20-like: balances + transfer + approve. +- Multisig: 2-of-3 with on-chain proposals. +- VRF: consume per-block beacon from TASK-033. + +**Acceptance** +- [ ] All 4 deploy and execute on dev chain. + +**Verification** +- Walkthrough each example end-to-end. + +--- + +## Summary + +45 tasks: 24 small, 13 medium, 4 large, 1 docs-link. Heavy mid-cluster around storage + CALL semantics; lighter early (arithmetic) and late (sample contracts). diff --git a/docs/backlog/queue/03-wallet.md b/docs/backlog/queue/03-wallet.md new file mode 100644 index 00000000..e44ed025 --- /dev/null +++ b/docs/backlog/queue/03-wallet.md @@ -0,0 +1,877 @@ +# Section 03 — Wallet & Accounts Specs (TASK-106..140) + +35 tasks. HD derivation, mnemonic flows, multi-sig, ENS-like names, contact book, token primitives, faucet hardening, batch send, scheduling, hardware-key + session-key delegation, account-abstraction stub, social recovery, encrypted export/import, vanity gen, metrics. + +**Preconditions used throughout:** +- Wallet API: [backend/src/api/wallet.ts](backend/src/api/wallet.ts) — current send/balance/faucet handlers. +- Crypto: [backend/src/blockchain/Crypto.ts](backend/src/blockchain/Crypto.ts) — `generateKeypair`, `derivePublicKey`, `sign`, `verify`, `verifyTransactionSignature`. +- State: [StateManager.ts](backend/src/blockchain/StateManager.ts) — `getBalance`, `getNonce`. +- Tx pool: [TransactionPool.ts](backend/src/blockchain/TransactionPool.ts) — `addTransaction`. +- DB: standard `db.query`. + +--- + +### TASK-106 — HD wallet derivation BIP32-style + +**Section:** wallet +**Effort:** L +**Depends on:** none +**Type:** new-file + +**Goal** +One mnemonic → many addresses via deterministic derivation. Reduces user key-management burden; matches industry conventions. + +**Files** +- new: `backend/src/wallet/hd.ts` — `seedFromMnemonic(mnemonic): Buffer`, `deriveKeypair(seed, path): {pub, priv}`. +- add deps: `bip39`, `ed25519-hd-key`. + +**Implementation sketch** +- `bip39.mnemonicToSeedSync(mnemonic)` → 64-byte seed. +- Path format `m/44'/9999'/0'/0/N` (9999 = our coin type). +- `ed25519-hd-key.derivePath(path, seed.toString('hex')).key` → 32-byte priv. +- Public key via existing `derivePublicKey`. + +**Acceptance** +- [ ] Same mnemonic → same address sequence. +- [ ] Different paths → different addresses. + +**Verification** +- Unit: known mnemonic → known addresses. + +--- + +### TASK-107 — Mnemonic export endpoint + +**Section:** wallet +**Effort:** S +**Depends on:** TASK-106 +**Type:** new-file + +**Goal** +Authenticated wallets can retrieve their mnemonic for backup. Heavily rate-limited. + +**Files** +- new: `POST /api/wallet/:addr/mnemonic/export` (in wallet.ts). + +**Reuses** +- Mnemonic store side (assumes mnemonic was stored encrypted at create-time). +- TASK-351 rate limit (1/min). + +**API contract** +``` +POST /api/wallet/:addr/mnemonic/export +body: { signature: '' } +→ 200 { mnemonic: '...', warning: 'never share' } +→ 401 { error: 'invalid signature' } +``` + +**Acceptance** +- [ ] Valid sig → mnemonic returned. +- [ ] Invalid → 401. + +**Verification** +- Sign + curl. + +--- + +### TASK-108 — Mnemonic import + recovery + +**Section:** wallet +**Effort:** M +**Depends on:** TASK-106 +**Type:** new-file + +**Goal** +User pastes a mnemonic; we derive their addresses + scan for any with on-chain history. + +**Files** +- new: `POST /api/wallet/import` body: `{ mnemonic, scanCount: 20 }`. + +**Implementation sketch** +- Derive `scanCount` addresses. +- For each: query `/api/account/:addr` to get balance + nonce. +- Return list with non-zero or non-zero-nonce addresses flagged as active. + +**Acceptance** +- [ ] Existing mnemonic returns its known addresses. + +**Verification** +- Import a known mnemonic. + +--- + +### TASK-109 — Watch-only address mode + +**Section:** wallet +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Users can add an address to track without holding the key. UI shows balance + activity, no send button. + +**Files** +- new: `POST /api/wallet/watch` body: `{ address }`. +- new: `GET /api/wallet/watched/:userKey`. + +**Implementation sketch** +- Per-session list of watched addresses (cookie-keyed) or per-API-key. +- Just metadata; no key storage. + +**Acceptance** +- [ ] Watched address appears in list. + +**Verification** +- Add + read. + +--- + +### TASK-110 — Multi-sig wallet primitive + +**Section:** wallet +**Effort:** L +**Depends on:** TASK-070, TASK-079 +**Type:** new-file + +**Goal** +Deploy an m-of-n multi-sig contract; all signers must approve to send. + +**Files** +- new: `examples/multisig/{source.hsm,program.json,README.md}` (overlaps with TASK-105). +- new: `backend/src/wallet/multisig.ts` — helper to construct & deploy. + +**Implementation sketch** +- Multi-sig stores list of owners + threshold M. +- Tx submission via `propose(target, value, data)` → returns proposal id. +- Approve via `confirm(proposalId, signature)`. +- Execute when M confirmations collected. + +**Acceptance** +- [ ] 2-of-3 multisig: 1 approval insufficient, 2 sufficient. + +**Verification** +- E2E test. + +--- + +### TASK-111 — Wallet name aliases + +**Section:** wallet +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Per-user nickname for any address (off-chain, scoped to API key). + +**Files** +- new: `backend/src/database/migrations/0022_wallet_aliases.sql` — `wallet_aliases(api_key_hash, address, alias, PK(api_key_hash, address))`. +- new: CRUD endpoints `GET/POST/DELETE /api/wallet/aliases`. + +**Acceptance** +- [ ] Aliases scoped to user. + +**Verification** +- Add + read. + +--- + +### TASK-112 — ENS-like /api/names/:name resolver + +**Section:** wallet +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Global on-chain name → address registry. First-claim wins. + +**Files** +- new: `backend/src/database/migrations/0023_names.sql` — `names(name TEXT PK, address, owner, registered_at, expires_at)`. +- new: `backend/src/api/names.ts` — register, transfer, resolve. + +**API contract** +``` +GET /api/names/:name → 200 { address } +POST /api/names body: { name, signature } → 200 { ok: true } +``` + +**Acceptance** +- [ ] Names resolvable. + +**Verification** +- Register + resolve. + +--- + +### TASK-113 — Reverse name lookup + +**Section:** wallet +**Effort:** S +**Depends on:** TASK-112 +**Type:** new-file + +**Goal** +Address → registered name(s). + +**Files** +- new: `GET /api/names/reverse/:addr` → `{ names: [...] }`. + +**Acceptance** +- [ ] Returns names where address matches. + +**Verification** +- Curl. + +--- + +### TASK-114 — Wallet activity feed + +**Section:** wallet +**Effort:** M +**Depends on:** TASK-058 +**Type:** edit + +**Goal** +Combined send/receive/contract-event timeline for an address. + +**Files** +- edit: `backend/src/api/wallet.ts` — `GET /api/wallet/:addr/activity?cursor=&limit=`. + +**Reuses** +- TASK-058 history; TASK-161 events. + +**API contract** +``` +→ 200 { items: [{ type: 'send'|'receive'|'event', ts, ...details }], next_cursor } +``` + +**Acceptance** +- [ ] Mixed feed returned in time order. + +**Verification** +- Curl. + +--- + +### TASK-115 — CSV export of wallet history + +**Section:** wallet +**Effort:** S +**Depends on:** TASK-114 +**Type:** edit + +**Goal** +Same data as TASK-114 but CSV for spreadsheets/tax tools. + +**Files** +- new: `GET /api/wallet/:addr/activity.csv?from=&to=`. + +**Implementation sketch** +- Stream CSV rows with header. + +**Acceptance** +- [ ] Returns valid CSV. + +**Verification** +- `curl > out.csv && head out.csv`. + +--- + +### TASK-116 — /api/wallet/:addr/qr.png + +**Section:** wallet +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +QR code PNG for an address (for receive screens). + +**Files** +- new: `GET /api/wallet/:addr/qr.png?size=256`. +- add dep: `qrcode`. + +**Implementation sketch** +- `QRCode.toBuffer(addr, { width: size })` → res. + +**Acceptance** +- [ ] PNG returned with correct content-type. + +**Verification** +- Curl, view image. + +--- + +### TASK-117 — Wallet contact book + +**Section:** wallet +**Effort:** S +**Depends on:** TASK-111 +**Type:** new-file + +**Goal** +Per-user contact list (alias + address + notes). + +**Files** +- new: migration `wallet_contacts` table. +- new: CRUD endpoints. + +**Acceptance** +- [ ] Add + list contacts. + +**Verification** +- Curl. + +--- + +### TASK-118 — Token balance aggregation + +**Section:** wallet +**Effort:** M +**Depends on:** TASK-105 (erc20-like example) +**Type:** new-file + +**Goal** +Across all deployed token contracts, return user's holdings. + +**Files** +- new: `GET /api/wallet/:addr/tokens` → `{ tokens: [{ contractAddress, symbol, balance }] }`. + +**Implementation sketch** +- Query `contract_metadata` for contracts with `symbol` field. +- For each, run a balance read against `contract_storage`. + +**Acceptance** +- [ ] Lists all balances > 0. + +**Verification** +- Curl. + +--- + +### TASK-119 — Approve / transferFrom flow for tokens + +**Section:** wallet +**Effort:** S +**Depends on:** TASK-105 +**Type:** docs + helper + +**Goal** +Standard ERC20-like allowance flow. Document in /docs and provide helper endpoints that build the txs. + +**Files** +- new: `POST /api/wallet/token/:contract/approve` body: `{spender, amount, signature}` → constructed tx. + +**Acceptance** +- [ ] Approve + transferFrom round-trip works. + +**Verification** +- Two-tx test. + +--- + +### TASK-120 — Allowance lookup endpoint + +**Section:** wallet +**Effort:** S +**Depends on:** TASK-119 +**Type:** new-file + +**Goal** +Read `allowance(owner, spender)` from a token contract. + +**Files** +- new: `GET /api/wallet/token/:contract/allowance/:owner/:spender`. + +**Acceptance** +- [ ] Returns numeric allowance. + +**Verification** +- Curl. + +--- + +### TASK-121 — Token transfer history per-account + +**Section:** wallet +**Effort:** S +**Depends on:** TASK-114 +**Type:** new-file + +**Goal** +Filter activity feed to just token transfers (Transfer event topic). + +**Files** +- new: `GET /api/wallet/:addr/token-history?contract=`. + +**Acceptance** +- [ ] Returns Transfer events involving this address. + +**Verification** +- Curl. + +--- + +### TASK-122 — Faucet rate-limit by IP not just address + +**Section:** wallet +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Current faucet limits per address (24h cooldown). Also limit by IP to prevent address-rotation abuse. + +**Files** +- edit: `backend/src/api/wallet.ts` faucet handler. + +**Implementation sketch** +- Track `faucet_ip_drips(ip, last_drip_at, count_24h)` in Redis with TTL. +- Cap: 5 drips per IP per 24h. + +**Acceptance** +- [ ] Same IP across 6 different addresses → 6th rejected. + +**Verification** +- Loop curl from same IP. + +--- + +### TASK-123 — Faucet captcha hook + +**Section:** wallet +**Effort:** M +**Depends on:** TASK-122 +**Type:** edit + +**Goal** +Optional hCaptcha verification before faucet drip. + +**Files** +- edit: faucet handler. +- add dep: `hcaptcha` (or fetch directly). + +**Implementation sketch** +- If `HCAPTCHA_SECRET` env set: require `captchaToken` field, verify against hCaptcha. +- Else: skip (dev/staging). + +**Acceptance** +- [ ] Without token (when configured): rejected. +- [ ] Valid token: passes. + +**Verification** +- Test in staging. + +--- + +### TASK-124 — Faucet drip dynamic amount + +**Section:** wallet +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Adjust drip size based on demand (queue depth) and pool reserves. + +**Files** +- edit: faucet handler. + +**Implementation sketch** +- Base = 100 OPEN. +- If pool reserves < 1000 OPEN: drip /= 4. +- If queue length > 100/min: drip /= 2. + +**Acceptance** +- [ ] Drip amount reduces under low reserves. + +**Verification** +- Drain pool, check next drip. + +--- + +### TASK-125 — Faucet pool refill schedule + +**Section:** wallet +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Auto-refill faucet pool from a treasury address on a schedule. + +**Files** +- new: `backend/src/wallet/faucetRefiller.ts` — interval that monitors pool, sends from treasury when below threshold. + +**Implementation sketch** +- Threshold via env (default 5000 OPEN). +- Refill amount: 50000 OPEN at a time. +- Treasury private key from env (sealed). + +**Acceptance** +- [ ] Pool refills on schedule. + +**Verification** +- Drop pool below threshold, observe. + +--- + +### TASK-126 — Wallet send batch (many recipients) + +**Section:** wallet +**Effort:** M +**Depends on:** TASK-169 +**Type:** new-file + +**Goal** +Single signed payload sends to N recipients (one tx per recipient). + +**Files** +- new: `POST /api/wallet/send-batch` body: `{ from, recipients: [{to, amount}], signature }`. + +**Implementation sketch** +- Verify signature over canonical message. +- Construct N txs with sequential nonces. +- Use bulk submit (TASK-169). + +**Acceptance** +- [ ] All txs accepted with correct nonces. + +**Verification** +- Send to 3 recipients in one call. + +--- + +### TASK-127 — Tx scheduling (broadcast at future height) + +**Section:** wallet +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Submit a signed tx now to be broadcast when chain reaches height H. + +**Files** +- new: migration `scheduled_txs(hash PK, target_height, payload_json, status, scheduled_by)`. +- new: `POST /api/wallet/schedule` body: `{ tx, height }`. +- new: scheduler interval that submits when height matches. + +**Acceptance** +- [ ] Tx scheduled at height N executes at or after N. + +**Verification** +- Schedule + wait. + +--- + +### TASK-128 — Tx replacement UI flow (cancel-by-replace) + +**Section:** wallet +**Effort:** S +**Depends on:** TASK-168 +**Type:** docs + +**Goal** +Document the cancel flow + provide a one-call helper. + +**Files** +- new: `docs/wallet/cancel-tx.md`. + +**Acceptance** +- [ ] Doc walks through with curl examples. + +**Verification** +- Manual. + +--- + +### TASK-129 — Hardware-key signing protocol stub + +**Section:** wallet +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Stub protocol for Ledger-style flow: backend constructs unsigned tx, sends to client, client signs on hw device, returns sig, backend submits. + +**Files** +- new: `backend/src/wallet/hardwareSigning.ts`. +- new: docs `/docs/wallet/hardware-keys.md`. + +**Implementation sketch** +- `POST /api/wallet/sign-request` body: `{ from, to, value }` → `{ unsignedTx, message }`. +- Client side responsibility: sign on device. +- `POST /api/wallet/sign-submit` body: `{ unsignedTx, signature }`. + +**Acceptance** +- [ ] Stub round-trip works with software signer simulating hw. + +**Verification** +- Dev test. + +--- + +### TASK-130 — Session key delegation + +**Section:** wallet +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Sign once with master key to authorize a session key with scoped permissions (max value, expiry). + +**Files** +- new: migration `session_keys(id, master_address, session_pubkey, max_value, expires_at, signature)`. +- new: middleware that accepts session-key signatures for txs within scope. + +**Acceptance** +- [ ] Tx signed by session key within scope: accepted. +- [ ] Out-of-scope: rejected. + +**Verification** +- Dev test. + +--- + +### TASK-131 — Account abstraction stub: paymaster + +**Section:** wallet +**Effort:** M +**Depends on:** TASK-079 +**Type:** new-file + +**Goal** +Allow gasless tx: paymaster contract pays the gas on behalf of the sender. + +**Files** +- new: `examples/paymaster/{source.hsm,program.json}`. +- edit: BlockProducer to accept paymaster-stamped txs (charge fee to paymaster, not sender). + +**Acceptance** +- [ ] Tx with valid paymaster authorization: sender pays nothing. + +**Verification** +- E2E test. + +--- + +### TASK-132 — Wallet-side mempool view + +**Section:** wallet +**Effort:** S +**Depends on:** TASK-166 +**Type:** new-file + +**Goal** +Filter `/api/mempool` to txs from/to a specific address. + +**Files** +- new: `GET /api/wallet/:addr/pending`. + +**Acceptance** +- [ ] Returns only txs touching this address. + +**Verification** +- Curl after submit. + +--- + +### TASK-133 — Wallet recovery via social guardians + +**Section:** wallet +**Effort:** L +**Depends on:** TASK-110 +**Type:** new-file + +**Goal** +3-of-5 friends approve a recovery to swap the master key on an account. + +**Files** +- new: `examples/social-recovery/...`. +- new: `POST /api/wallet/recovery/setup` body: `{guardians: [...]}`. +- new: `POST /api/wallet/recovery/initiate` body: `{newKey, signatures}`. + +**Implementation sketch** +- Recovery contract holds the guardian set. +- M-of-N approve a `setKey(newPubKey)` call. +- Time-lock 24h before swap takes effect. + +**Acceptance** +- [ ] Recovery flow works. + +**Verification** +- E2E test. + +--- + +### TASK-134 — Per-account gas budget cap + +**Section:** wallet +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Account-level cap on gas burn per 24h. Prevents runaway scripts from draining. + +**Files** +- new: migration `account_gas_caps(address PK, max_24h, used_24h, period_started)`. +- edit: TransactionPool — reject if cap would exceed. + +**Acceptance** +- [ ] Tx beyond cap: rejected. + +**Verification** +- Set low cap, attempt tx. + +--- + +### TASK-135 — Wallet password-encrypted export + +**Section:** wallet +**Effort:** M +**Depends on:** TASK-107 +**Type:** new-file + +**Goal** +Export wallet (mnemonic + addresses) as JSON encrypted with PBKDF2 + AES-256-GCM keyed by user password. + +**Files** +- new: `backend/src/wallet/encryptedExport.ts` — `exportEncrypted(walletData, password)`, `decryptImport(blob, password)`. + +**Implementation sketch** +- PBKDF2-SHA256, 100k iterations, 32-byte key. +- AES-256-GCM with random 12-byte IV. +- JSON envelope: `{ kdf: 'pbkdf2', iterations, salt, iv, ciphertext, tag }`. + +**Acceptance** +- [ ] Round-trip with correct password. +- [ ] Wrong password → decrypt failure. + +**Verification** +- Unit. + +--- + +### TASK-136 — Wallet import from JSON + +**Section:** wallet +**Effort:** S +**Depends on:** TASK-135 +**Type:** edit + +**Goal** +Accept the encrypted JSON, decrypt, derive addresses. + +**Files** +- new: `POST /api/wallet/import-encrypted` body: `{ blob, password }`. + +**Acceptance** +- [ ] Successful decrypt restores wallet. + +**Verification** +- Round-trip. + +--- + +### TASK-137 — Address validity checker endpoint + +**Section:** wallet +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Pre-flight check: is this string a valid Hermes address? (base58 + length + checksum). + +**Files** +- new: `GET /api/wallet/validate/:input`. + +**API contract** +``` +→ 200 { valid: true|false, reason?: 'bad-base58'|'wrong-length'|'bad-checksum' } +``` + +**Acceptance** +- [ ] Valid address: true. +- [ ] Garbage: false with reason. + +**Verification** +- Curl. + +--- + +### TASK-138 — Vanity address generator script + +**Section:** wallet +**Effort:** S +**Depends on:** none +**Type:** script + +**Goal** +Given a prefix, brute-force keypairs until address starts with prefix. CLI. + +**Files** +- new: `backend/scripts/vanity-address.ts`. + +**Implementation sketch** +- Loop generateKeypair, check prefix, count attempts, print on match. +- `--prefix Hermes --max 1000000`. + +**Acceptance** +- [ ] Returns address with the prefix. + +**Verification** +- `npm run vanity -- --prefix abc`. + +--- + +### TASK-139 — Bulk address generator for testing + +**Section:** wallet +**Effort:** S +**Depends on:** none +**Type:** script + +**Goal** +Generate N keypairs at once, output JSON for test fixtures. + +**Files** +- new: `backend/scripts/bulk-keys.ts`. + +**Acceptance** +- [ ] Outputs N pairs. + +**Verification** +- Run with N=100. + +--- + +### TASK-140 — Wallet metrics endpoint + +**Section:** wallet +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Aggregate counts for ops dashboard. + +**Files** +- new: `GET /api/wallet/metrics` → `{ total, active_24h, new_24h, send_volume_24h }`. + +**Acceptance** +- [ ] Returns sensible numbers. + +**Verification** +- Curl. + +--- + +## Summary + +35 tasks: 21 small, 11 medium, 3 large. Heavier-cluster around HD/multi-sig/social recovery (106, 110, 133); rest mostly endpoints + helpers. diff --git a/docs/backlog/queue/04-api-explorer.md b/docs/backlog/queue/04-api-explorer.md new file mode 100644 index 00000000..19105796 --- /dev/null +++ b/docs/backlog/queue/04-api-explorer.md @@ -0,0 +1,1065 @@ +# Section 04 — API & Explorer Specs (TASK-141..180) + +40 tasks. OpenAPI/Swagger, versioning, observability middleware (rate-limit headers, request IDs, access log, slow-query log), health subroutes, build/flags/metrics endpoints, search + leaderboards, mempool/reorg/contract feeds, address tagging, WebSocket + Socket.io rooms, GraphQL/tRPC/JSON-RPC compatibility, Postman collection generator. + +**Preconditions used throughout:** +- Express server: [backend/src/api/server.ts](backend/src/api/server.ts) — current route mounts at lines 90-700. +- Auth: [backend/src/api/auth.ts](backend/src/api/auth.ts) — `requireApiKey('scope')` middleware, `ipRateLimit(perMin)`. +- Receipts: `loadReceipt()` from [TransactionReceipt.ts:255](backend/src/blockchain/TransactionReceipt.ts#L255). +- DB: `db.query`, `db.queryRead` (TASK-322). +- Mesh: [backend/src/network/api.ts](backend/src/network/api.ts). +- SSE pattern: [server.ts:971-1057](backend/src/api/server.ts#L971-L1057). + +--- + +### TASK-141 — /api/openapi.json generation + +**Section:** api +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Auto-generate OpenAPI 3.1 spec from route declarations so clients can codegen typed SDKs and we get free API docs. + +**Files** +- new: `backend/src/api/openapi.ts` — exports `buildOpenApiSpec(): object`. +- edit: `backend/src/api/server.ts` — register `GET /api/openapi.json`. + +**Reuses** +- Express's `app._router.stack` for route enumeration. + +**API contract** +``` +GET /api/openapi.json +→ 200 { openapi: "3.1.0", info, paths: {...}, components: {...} } +``` + +**Implementation sketch** +- Walk `app._router.stack` and registered routers; emit `paths[route][method]` entries. +- Per-route metadata supplied via a side annotation `routeDoc(route, { summary, params, responses })` collected at registration time. +- Components: shared schemas (Block, Transaction, Receipt) from a single source. + +**Acceptance** +- [ ] Returns valid OpenAPI 3.1 (passes `swagger-cli validate`). +- [ ] Every existing route appears. + +**Verification** +- `curl /api/openapi.json | swagger-cli validate -`. + +--- + +### TASK-142 — Swagger UI at /docs + +**Section:** api +**Effort:** S +**Depends on:** TASK-141 +**Type:** edit + +**Goal** +Mount swagger-ui-express at `/docs` reading from the openapi.json endpoint. + +**Files** +- edit: `backend/src/api/server.ts` — add `swaggerUi.serve` and `swaggerUi.setup`. +- add dep: `swagger-ui-express`. + +**Implementation sketch** +- `app.use('/docs', swaggerUi.serve, swaggerUi.setup(undefined, { swaggerOptions: { url: '/api/openapi.json' } }))`. + +**Acceptance** +- [ ] `/docs` renders the spec with try-it-out. + +**Verification** +- Open `/docs` in browser. + +--- + +### TASK-143 — /api/v1/* version prefix + deprecation headers + +**Section:** api +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +Stable version 1 prefix. Bare `/api/*` keeps working but returns `Deprecation: true` and `Sunset: ` headers. + +**Files** +- edit: `backend/src/api/server.ts` — wrap existing app.use calls in helper that mounts at both `/api/v1/...` and `/api/...`; the latter wraps with deprecation middleware. + +**Implementation sketch** +- `mountVersioned(app, '/auth', authRouter)` mounts twice; bare path adds the response header. + +**Acceptance** +- [ ] `/api/v1/status` works. +- [ ] `/api/status` works AND has `Deprecation: true` header. + +**Verification** +- `curl -I /api/status | grep Deprecation`. + +--- + +### TASK-144 — Rate-limit headers (X-RateLimit-*) + +**Section:** api +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +[ipRateLimit](backend/src/api/auth.ts) silently rejects with 429. Surface remaining quota in response headers so clients can self-pace. + +**Files** +- edit: `backend/src/api/auth.ts:ipRateLimit` — set `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` on every response. + +**Acceptance** +- [ ] Headers present on every rate-limited route's response. +- [ ] Remaining decrements per call. + +**Verification** +- `curl -i /api/personality/hermes ...`. + +--- + +### TASK-145 — CORS allowlist via env + +**Section:** api +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +[server.ts:80](backend/src/api/server.ts#L80) uses `cors()` (open). Restrict to `CORS_ORIGINS` env (comma-separated) when set. + +**Files** +- edit: `backend/src/api/server.ts:80`. + +**Implementation sketch** +- If env set: `cors({ origin: (origin, cb) => cb(null, allowed.includes(origin)) })`. +- Default unchanged for dev. + +**Acceptance** +- [ ] With env, disallowed origin → CORS error. + +**Verification** +- Test with curl Origin header. + +--- + +### TASK-146 — Request-ID middleware + +**Section:** api +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Echo or assign `X-Request-ID` header on every request. Used by access log + slow-query log + error reporting for correlation. + +**Files** +- new: `backend/src/api/middleware/requestId.ts`. +- edit: `backend/src/api/server.ts` — register before any other middleware. + +**Implementation sketch** +- `req.id = req.headers['x-request-id'] || crypto.randomUUID()`. +- `res.setHeader('X-Request-ID', req.id)`. + +**Acceptance** +- [ ] Header present on all responses. + +**Verification** +- `curl -i /api/status | grep X-Request-ID`. + +--- + +### TASK-147 — Structured access log NDJSON + +**Section:** api +**Effort:** S +**Depends on:** TASK-146 +**Type:** new-file + +**Goal** +One JSON-line per request to stdout: `{ts, method, path, status, durationMs, bytes, requestId, ip}`. Easy to feed into Logflare/Datadog. + +**Files** +- new: `backend/src/api/middleware/accessLog.ts`. +- edit: server.ts — register after requestId. + +**Implementation sketch** +- On `res.on('finish')`, write the JSON line. + +**Acceptance** +- [ ] Every request logged exactly once. + +**Verification** +- `curl /api/status` → stdout shows the line. + +--- + +### TASK-148 — Slow-request log >1s + +**Section:** api +**Effort:** S +**Depends on:** TASK-147 +**Type:** edit + +**Goal** +Tag requests over a threshold (default 1s) in the access log with `slow: true` and elevate to console.warn. + +**Files** +- edit: accessLog.ts. + +**Implementation sketch** +- Threshold env `SLOW_REQUEST_MS` (default 1000). +- If durationMs > threshold: also `console.warn(...)`. + +**Acceptance** +- [ ] Slow request appears in console.warn. + +**Verification** +- Inject pg_sleep route or call /api/agent/stream briefly. + +--- + +### TASK-149 — /health/live + /health/ready + /health/deep + +**Section:** api +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Three-tier health checks per the design doc. live = process up; ready = can serve traffic; deep = exercises full read paths. + +**Files** +- new: `backend/src/api/health.ts`. +- edit: server.ts — mount. + +**API contract** +``` +GET /health/live → 200 { status: 'live' } +GET /health/ready + → 200 { ready: true, checks: {...} } + → 503 { ready: false, failures: [{check, reason}] } +GET /health/deep + → 200 { ready: true, latencyMs: { db: 12, redis: 3, chainHead: 1 } } + → 503 { ready: false, failures: [...] } +``` + +**Implementation sketch** +- `live`: always 200 unless shutting down (graceful-shutdown handler flips a flag). +- `ready`: check `db.poolStats()`, `cache.isConnected()`, agent worker heartbeat (TASK-332), chain bootstrap height. +- `deep`: actually run a SELECT, GET, and chain head fetch with timing. + +**Acceptance** +- [ ] All three endpoints return appropriate codes. +- [ ] During shutdown, ready returns 503. + +**Verification** +- Curl each. + +--- + +### TASK-150 — /api/build endpoint + +**Section:** api +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Returns build info for debugging deploys: commit SHA, build timestamp, version. + +**Files** +- new: `backend/src/api/build.ts`. +- edit: build script — write `backend/build-info.json` with `{ commit: $(git rev-parse HEAD), buildTime: now }`. + +**API contract** +``` +GET /api/build → 200 { commit, buildTime, version } +``` + +**Acceptance** +- [ ] Returns commit SHA matching deployed code. + +**Verification** +- Compare with `git log` after build. + +--- + +### TASK-151 — /api/flags feature-flag endpoint + +**Section:** api +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Surface the typed feature flag registry already in the codebase. Clients use it for progressive rollout UI. + +**Files** +- new: `backend/src/api/flags.ts`. +- edit: server.ts — mount. + +**Reuses** +- Existing flag registry (was committed in `feat(ops): typed feature flag registry`). + +**API contract** +``` +GET /api/flags → 200 { flags: { vmEnabled: true, beaconRandomness: false, ... } } +``` + +**Acceptance** +- [ ] Lists all flags + current values. + +**Verification** +- Curl. + +--- + +### TASK-152 — /api/metrics Prometheus text format + +**Section:** api +**Effort:** M +**Depends on:** TASK-319, TASK-320 +**Type:** new-file + +**Goal** +Standard Prometheus exposition format. Counters + gauges + histograms. + +**Files** +- new: `backend/src/api/metrics.ts`. + +**Reuses** +- `db.poolStats()` (TASK-319). +- Query histogram from TASK-320. +- Chain stats: `chain.getChainLength()`, mempool size, etc. + +**API contract** +``` +GET /api/metrics → 200 (text/plain) + # HELP hermes_chain_height ... + # TYPE hermes_chain_height gauge + hermes_chain_height 1234 + ... +``` + +**Acceptance** +- [ ] Output parses by `prom2json` without errors. +- [ ] Includes pool, query histogram, chain height, mempool size, peer count. + +**Verification** +- `curl /api/metrics | prom2json -`. + +--- + +### TASK-153 — Block search by height range with filters + +**Section:** api +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Paginated block listing with height range + producer filter. + +**Files** +- new endpoint in server.ts. + +**API contract** +``` +GET /api/blocks/search?from=&to=&producer=&limit=50&cursor= +→ 200 { items: [...], next_cursor } +``` + +**Acceptance** +- [ ] Filters apply. +- [ ] Cursor pagination correct. + +**Verification** +- Curl with combinations. + +--- + +### TASK-154 — Tx search by from/to/value range + +**Section:** api +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Find txs by sender, recipient, or value range. + +**Files** +- new endpoint in server.ts. + +**API contract** +``` +GET /api/tx/search?from=&to=&minValue=&maxValue=&limit=50&cursor= +→ 200 { items: [...], next_cursor } +``` + +**Acceptance** +- [ ] Filters apply. + +**Verification** +- Curl. + +--- + +### TASK-155 — Top accounts by balance + +**Section:** api +**Effort:** S +**Depends on:** TASK-308 +**Type:** new-file + +**Goal** +Leaderboard endpoint. + +**Files** +- new: `GET /api/accounts/top?limit=100`. + +**Reuses** +- `idx_accounts_balance_desc` from TASK-308. + +**API contract** +``` +GET /api/accounts/top?limit=100 +→ 200 { items: [{ address, balance, rank }] } +``` + +**Acceptance** +- [ ] Sorted by balance DESC. + +**Verification** +- Curl. + +--- + +### TASK-156 — Top accounts by tx count + +**Section:** api +**Effort:** S +**Depends on:** TASK-307 +**Type:** new-file + +**Goal** +Same shape as TASK-155 but ranked by `(SELECT COUNT(*) FROM transactions WHERE from_address = a.address OR to_address = a.address)`. + +**Files** +- new: `GET /api/accounts/top-by-activity?limit=100`. + +**Acceptance** +- [ ] Returns count-ranked list. + +**Verification** +- Curl. + +--- + +### TASK-157 — Validator leaderboard + +**Section:** api +**Effort:** S +**Depends on:** TASK-053 +**Type:** new-file + +**Goal** +Validators ranked by blocks_produced and uptime. + +**Files** +- new: `GET /api/validators/leaderboard`. + +**API contract** +``` +→ 200 { items: [{ address, name, blocks_produced, uptime, rank }] } +``` + +**Acceptance** +- [ ] Sorted correctly. + +**Verification** +- Curl. + +--- + +### TASK-158 — Network stats dashboard endpoint + +**Section:** api +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +One endpoint that bundles peers/mempool/tps/finality-lag for the HUD. + +**Files** +- new: `GET /api/network/dashboard`. + +**API contract** +``` +→ 200 { + peers: { active: 5, total: 7 }, + mempool: { pending: 12 }, + tps: { window60: 3.4 }, + finality: { headHeight: 1234, finalizedHeight: 1222, lagBlocks: 12 } +} +``` + +**Acceptance** +- [ ] All fields populated. + +**Verification** +- Curl. + +--- + +### TASK-159 — Block detail with full receipts inline + +**Section:** api +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Existing `/api/blocks/:height` returns block JSON without receipts. Inline them. + +**Files** +- edit: `backend/src/api/server.ts:183`. + +**Reuses** +- `loadBlockReceipts(height)`. + +**API contract** +``` +GET /api/blocks/:height?include=receipts +→ 200 { ...block, receipts: [...] } +``` + +**Acceptance** +- [ ] `?include=receipts` returns receipts array. + +**Verification** +- Curl. + +--- + +### TASK-160 — Tx detail with decoded log events + +**Section:** api +**Effort:** M +**Depends on:** TASK-097 +**Type:** edit + +**Goal** +Existing `/api/tx/:hash` returns raw logs. Add ABI-decoded form when contract has registered ABI. + +**Files** +- edit: server.ts — extend tx handler. + +**Reuses** +- `lookupEvent` from TASK-097. + +**API contract** +``` +GET /api/tx/:hash?decodeLogs=true +→ 200 { ...tx, logs: [{ ...raw, decoded: { name, fields } | null }] } +``` + +**Acceptance** +- [ ] Logs from contracts with ABI get decoded form. + +**Verification** +- Curl. + +--- + +### TASK-161 — /api/contract/:addr/events feed + +**Section:** api +**Effort:** S +**Depends on:** TASK-310, TASK-097 +**Type:** new-file + +**Goal** +Per-contract event history. + +**Files** +- new: `GET /api/contract/:addr/events?limit=&cursor=`. + +**Acceptance** +- [ ] Returns logs filtered to that contract address. + +**Verification** +- Curl. + +--- + +### TASK-162 — Address tag system + +**Section:** api +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Operators can tag addresses ("exchange", "validator", "burn"). Tags surface alongside balances in account endpoints. + +**Files** +- new: `backend/src/database/migrations/0021_address_tags.sql` — `address_tags(address, tag, source, created_at, PK(address, tag))`. +- new: `backend/src/api/tags.ts` — CRUD endpoints. + +**API contract** +``` +GET /api/tags/:addr → 200 { address, tags: ['exchange', 'validator'] } +POST /api/tags (admin) body: {address, tag} → 200 { ok: true } +DELETE /api/tags/:addr/:tag (admin) → 200 { ok: true } +``` + +**Acceptance** +- [ ] Tags persist + appear in `/api/account/:addr`. + +**Verification** +- Add + read. + +--- + +### TASK-163 — Tag suggestion endpoint + +**Section:** api +**Effort:** S +**Depends on:** TASK-162 +**Type:** new-file + +**Goal** +Heuristic-based suggestions for untagged addresses (high tx count, large balance, validator). + +**Files** +- new: `GET /api/tags/suggest/:addr`. + +**Implementation sketch** +- If validator → suggest 'validator'. +- If receives many small tx daily → suggest 'exchange'. +- If sends many txs but receives ~0 → suggest 'distributor'. + +**Acceptance** +- [ ] Returns list of suggestions with confidence. + +**Verification** +- Curl on a known active address. + +--- + +### TASK-164 — Top gas spenders last 24h + +**Section:** api +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Leaderboard of who's burning the most gas. + +**Files** +- new: `GET /api/accounts/top-gas?windowHours=24`. + +**Acceptance** +- [ ] Returns sorted list. + +**Verification** +- Curl. + +--- + +### TASK-165 — /api/reorg/:id detail page + +**Section:** api +**Effort:** S +**Depends on:** TASK-060 +**Type:** new-file + +**Goal** +Drill into a specific reorg event from `reorg_log`. + +**Files** +- new: `GET /api/reorg/:id`. + +**Acceptance** +- [ ] Returns row + linked orphaned blocks. + +**Verification** +- Curl. + +--- + +### TASK-166 — /api/mempool snapshot + +**Section:** api +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +List pending txs (capped). + +**Files** +- new: `GET /api/mempool?limit=200`. + +**Acceptance** +- [ ] Returns array of pending tx JSON. + +**Verification** +- Curl. + +--- + +### TASK-167 — /api/mempool/:hash pending tx by hash + +**Section:** api +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Lookup a specific pending tx (404 if mined or unknown). + +**Files** +- new: `GET /api/mempool/:hash`. + +**Acceptance** +- [ ] 200 with tx if pending; 404 if mined. + +**Verification** +- Curl. + +--- + +### TASK-168 — Cancel pending tx endpoint + +**Section:** api +**Effort:** M +**Depends on:** TASK-019 +**Type:** new-file + +**Goal** +Sender-signed cancel: submit a no-op tx with same nonce + 11% higher gas (RBF). + +**Files** +- new: `POST /api/mempool/:hash/cancel`. + +**Implementation sketch** +- Body: `{ signature }` over message `cancel:${hash}:${timestamp}`. +- Verify signature against tx.from. +- Construct a self-transfer (from→from, value=0) with same nonce + bumped gasPrice. +- Submit via TransactionPool. + +**Acceptance** +- [ ] Pending tx cancelled (replaced by no-op). + +**Verification** +- Submit, cancel, observe replacement. + +--- + +### TASK-169 — Bulk tx submit endpoint + +**Section:** api +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Single POST with multiple txs. + +**Files** +- new: `POST /api/transactions/bulk` body: `{ transactions: [...] }` → `{ accepted: [hashes], rejected: [{tx, reason}] }`. + +**Acceptance** +- [ ] Accepts up to 100 per call. + +**Verification** +- Bulk curl. + +--- + +### TASK-170 — Idempotent tx submit (dedup on hash) + +**Section:** api +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Re-submitting the same hash should return the existing acceptance, not error. + +**Files** +- edit: `backend/src/api/server.ts:319` — check for existing pending or confirmed tx with same hash, short-circuit. + +**Acceptance** +- [ ] Same tx submitted twice → 200 both times. + +**Verification** +- Submit twice. + +--- + +### TASK-171 — WebSocket equivalents of all SSE channels + +**Section:** api +**Effort:** L +**Depends on:** TASK-047, TASK-048, TASK-049 +**Type:** new-file + +**Goal** +Every SSE endpoint also exposed via WebSocket (for clients that prefer ws). + +**Files** +- new: `backend/src/api/ws.ts` — uses `ws` package. +- edit: server.ts — attach ws server to httpServer at `/ws`. + +**API contract** +``` +ws://host/ws/agent +ws://host/ws/logs +ws://host/ws/mempool +ws://host/ws/forks +``` + +**Acceptance** +- [ ] Each ws path delivers same events as the SSE counterpart. + +**Verification** +- `wscat -c ws://...`. + +--- + +### TASK-172 — Socket.io rooms per address + +**Section:** api +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +Allow clients to subscribe to `/socket.io` and join a room per address; receive only events touching that address. + +**Files** +- edit: server.ts socket.io setup. + +**Implementation sketch** +- On `connection`: `socket.on('subscribe', addr => socket.join(`addr:${addr}`))`. +- When a tx is mined or balance changes for addr, emit to `addr:${addr}` room. + +**Acceptance** +- [ ] Subscriber gets events only for their address. + +**Verification** +- Two browsers, one subscribed; receive only own. + +--- + +### TASK-173 — SSE event replay since cursor + +**Section:** api +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +SSE clients that reconnect with `Last-Event-ID` should receive missed events. + +**Files** +- edit: SSE handlers. + +**Implementation sketch** +- Buffer last 1000 events per channel. +- On reconnect, replay events with id > Last-Event-ID, then resume live. + +**Acceptance** +- [ ] Reconnect after disconnect → no missed events (within buffer). + +**Verification** +- Drop SSE, produce events, reconnect, observe. + +--- + +### TASK-174 — GraphQL gateway over REST + +**Section:** api +**Effort:** L +**Depends on:** none +**Type:** new-file + +**Goal** +Single GraphQL endpoint at `/graphql` exposing typed queries over the existing REST surface. + +**Files** +- new: `backend/src/api/graphql/{schema,resolvers,server}.ts`. +- add deps: `graphql`, `graphql-yoga`. + +**Implementation sketch** +- Schema covers: block, tx, account, validator, mempool, logs. +- Resolvers call existing REST handlers internally. + +**Acceptance** +- [ ] Sample query returns expected shape. + +**Verification** +- GraphiQL. + +--- + +### TASK-175 — tRPC endpoint mirror + +**Section:** api +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +tRPC router mirroring REST so TS clients get end-to-end types. + +**Files** +- new: `backend/src/api/trpc/router.ts`. +- add dep: `@trpc/server`. + +**Implementation sketch** +- Define procedures matching key REST handlers. +- Mount at `/trpc`. + +**Acceptance** +- [ ] TypeScript client gets typed responses. + +**Verification** +- Sample client script. + +--- + +### TASK-176 — JSON-RPC eth_blockNumber + eth_getBalance + +**Section:** api +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Minimal Ethereum JSON-RPC compat layer so MetaMask + other tools can connect. + +**Files** +- new: `backend/src/api/jsonrpc.ts` — POST `/rpc` handler dispatching by `method` field. + +**Implementation sketch** +- `eth_blockNumber` → hex of `chain.getChainLength()`. +- `eth_getBalance` → hex of `stateManager.getBalance(addr)`. +- Other methods → `{ error: { code: -32601, message: 'method not found' } }`. + +**Acceptance** +- [ ] MetaMask can connect and read balance. + +**Verification** +- Configure MetaMask custom RPC. + +--- + +### TASK-177 — JSON-RPC eth_call via VM + +**Section:** api +**Effort:** M +**Depends on:** TASK-176, TASK-055 +**Type:** edit + +**Goal** +Implement `eth_call` for read-only contract execution. + +**Files** +- edit: jsonrpc.ts. + +**Implementation sketch** +- Dispatch eth_call → run interpreter against current state with read-only flag. +- Return hex of returndata. + +**Acceptance** +- [ ] eth_call against deployed contract returns expected result. + +**Verification** +- Sample contract call. + +--- + +### TASK-178 — JSON-RPC eth_sendRawTransaction + +**Section:** api +**Effort:** M +**Depends on:** TASK-176 +**Type:** edit + +**Goal** +Accept a signed raw tx and submit to mempool. + +**Files** +- edit: jsonrpc.ts. + +**Implementation sketch** +- Decode raw tx (RLP if EVM-compat, else our format). +- Insert via `txPool.addTransaction`. +- Return tx hash hex. + +**Acceptance** +- [ ] MetaMask can send tx. + +**Verification** +- MetaMask sample tx. + +--- + +### TASK-179 — JSON-RPC subscriptions + +**Section:** api +**Effort:** L +**Depends on:** TASK-176 +**Type:** edit + +**Goal** +`eth_subscribe` and `eth_unsubscribe` over WebSocket for newHeads, logs, newPendingTransactions. + +**Files** +- edit: jsonrpc.ts + ws.ts. + +**Acceptance** +- [ ] Subscribe to newHeads → receives block on each produced. + +**Verification** +- wscat with eth_subscribe. + +--- + +### TASK-180 — Postman/Bruno collection generator + +**Section:** api +**Effort:** S +**Depends on:** TASK-141 +**Type:** script + +**Goal** +Generate a Postman / Bruno collection from openapi.json so non-dev users can poke the API. + +**Files** +- new: `backend/scripts/gen-postman.ts`. + +**Implementation sketch** +- Read openapi.json, walk paths, emit Postman v2.1 collection format. + +**Acceptance** +- [ ] Generated collection imports cleanly into Postman. + +**Verification** +- Import. + +--- + +## Summary + +40 tasks: 23 small, 13 medium, 4 large. Heavy-cluster on JSON-RPC compat (176-179) and observability middleware (146-152). diff --git a/docs/backlog/queue/05-agent-worker.md b/docs/backlog/queue/05-agent-worker.md new file mode 100644 index 00000000..3b99b9e4 --- /dev/null +++ b/docs/backlog/queue/05-agent-worker.md @@ -0,0 +1,801 @@ +# Section 05 — Agent Worker Specs (TASK-181..215) + +35 tasks. Task priority + dependency + effort tracking, retry/timeout/budget, skill registry + versioning + rate-limit, verification gates (typecheck/test/prettier) + auto-rollback, PR-mode workflow + branch-per-task + auto-rebase + commitlint + co-author + DCO, agent identity rotation + per-area expertise, cross-task memory + per-task post-mortem + learning corpus. + +**Preconditions used throughout:** +- Agent worker: [backend/src/agent/AgentWorker.ts](backend/src/agent/AgentWorker.ts). +- Task backlog: [TaskBacklog.ts](backend/src/agent/TaskBacklog.ts), [TaskSources.ts](backend/src/agent/TaskSources.ts). +- Git: [GitIntegration.ts](backend/src/agent/GitIntegration.ts). +- CI monitor: [CIMonitor.ts](backend/src/agent/CIMonitor.ts). +- Skills: [SkillManager.ts](backend/src/agent/SkillManager.ts). +- Executor: [AgentExecutor.ts](backend/src/agent/AgentExecutor.ts). +- LLM: [hermesClient.ts](backend/src/llm/hermesClient.ts). + +--- + +### TASK-181 — Task priority queue (urgent / normal / chore) + +**Section:** agent +**Effort:** S +**Depends on:** none +**Type:** edit + migration + +**Goal** +Three-tier priority. Urgent always first; normal next; chore when nothing else. + +**Files** +- new: `backend/src/database/migrations/0024_agent_tasks_priority.sql` — `ALTER TABLE agent_tasks ADD COLUMN priority TEXT NOT NULL DEFAULT 'normal' CHECK (priority IN ('urgent','normal','chore'));` +- edit: `TaskBacklog.ts` — order pending by priority CASE then created_at. + +**Acceptance** +- [ ] Urgent task picked before normal regardless of age. + +**Verification** +- Insert mixed-priority queue, observe order. + +--- + +### TASK-182 — Task dependency graph + +**Section:** agent +**Effort:** M +**Depends on:** TASK-181 +**Type:** edit + migration + +**Goal** +A task can declare `depends_on: [taskId, ...]`. Worker skips it until all deps are status='completed'. + +**Files** +- new: migration `0025_agent_task_deps.sql` — `agent_task_deps(task_id, depends_on, PK(task_id, depends_on))`. +- edit: TaskBacklog.getNextTask query — `WHERE NOT EXISTS (SELECT 1 FROM agent_task_deps d JOIN agent_tasks t ON t.id = d.depends_on WHERE d.task_id = at.id AND t.status != 'completed')`. + +**Acceptance** +- [ ] Task with unmet dep: skipped. +- [ ] After dep completes: picked up. + +**Verification** +- Two-task chain. + +--- + +### TASK-183 — Task estimated + actual effort tracking + +**Section:** agent +**Effort:** S +**Depends on:** none +**Type:** edit + migration + +**Goal** +Track planned vs actual minutes per task for retrospective. + +**Files** +- new: migration adding `estimated_minutes`, `actual_minutes` columns. +- edit: AgentWorker — record actual_minutes on completion. + +**Acceptance** +- [ ] Both columns populate. + +**Verification** +- Check after task. + +--- + +### TASK-184 — Task retry on transient failure + +**Section:** agent +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Distinguish transient (network, rate-limit) from permanent (bug, missing dep). Retry transient up to 3 times with exponential backoff. + +**Files** +- edit: AgentWorker.ts — on caught error, classify, increment retry count. + +**Implementation sketch** +- Transient: retry; permanent: fail. +- Backoff: 1m, 5m, 15m. + +**Acceptance** +- [ ] Network error retried. +- [ ] Logic error fails immediately. + +**Verification** +- Inject errors of each kind. + +--- + +### TASK-185 — Task timeout 30min + +**Section:** agent +**Effort:** S +**Depends on:** TASK-333 +**Type:** edit + +**Goal** +Hard cap on per-task wall time. Kill execution + recovery sweep handles cleanup (TASK-333). + +**Files** +- edit: AgentWorker.ts — wrap task execution in `Promise.race` with 30min timeout. + +**Acceptance** +- [ ] Task exceeding 30min: terminated. + +**Verification** +- Inject sleep task. + +--- + +### TASK-186 — Per-task token budget + +**Section:** agent +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Cap LLM tokens per task. Default 50k. + +**Files** +- edit: AgentExecutor.ts — track running token count via response usage; abort if cap exceeded. + +**Acceptance** +- [ ] 50k+ token task aborts. + +**Verification** +- Force a giant task. + +--- + +### TASK-187 — Cumulative daily token budget + +**Section:** agent +**Effort:** S +**Depends on:** TASK-186 +**Type:** edit + +**Goal** +Global cap (e.g. 5M tokens/day) so a runaway can't drain Anthropic credits. + +**Files** +- edit: hermesClient.ts — refuse calls when daily cap hit. + +**Implementation sketch** +- Redis counter `tokens:daily:${date}`. +- Read before each call; abort if over. + +**Acceptance** +- [ ] At cap: subsequent calls return error. + +**Verification** +- Set low cap, exhaust, observe. + +--- + +### TASK-188 — /api/agent/tokens/stream SSE + +**Section:** agent +**Effort:** S +**Depends on:** TASK-186 +**Type:** new-file + +**Goal** +Real-time stream of token usage for the HUD cost ticker (TASK-244). + +**Files** +- new SSE endpoint. + +**API contract** +``` +GET /api/agent/tokens/stream +event: usage +data: { taskId, inputTokens, outputTokens, costUsd } +``` + +**Acceptance** +- [ ] Each LLM call emits an event. + +**Verification** +- Curl, observe. + +--- + +### TASK-189 — Cost estimate per task + +**Section:** agent +**Effort:** S +**Depends on:** TASK-186 +**Type:** edit + +**Goal** +After completion, write `estimated_cost_usd` to agent_tasks based on token usage × model price. + +**Files** +- new column + price table in code. +- edit: AgentWorker on completion. + +**Acceptance** +- [ ] Cost field populated. + +**Verification** +- Check post-task. + +--- + +### TASK-190 — Task rejection reason logging + +**Section:** agent +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +When a task is skipped (deps unmet, gates failed), log to a `task_rejections` table with reason. + +**Files** +- new migration + INSERT in TaskBacklog. + +**Acceptance** +- [ ] Rejections traceable. + +**Verification** +- Force a rejection. + +--- + +### TASK-191 — Skill registry persistence + +**Section:** agent +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +[SkillManager](backend/src/agent/SkillManager.ts) currently holds skills in-memory. Persist set of loaded + enabled skills. + +**Files** +- new migration `agent_skills(id PK, name, version, enabled, loaded_at)`. +- edit: SkillManager to read/write. + +**Acceptance** +- [ ] Restart preserves enable/disable state. + +**Verification** +- Disable, restart, check. + +--- + +### TASK-192 — Skill versioning + hot-reload + +**Section:** agent +**Effort:** M +**Depends on:** TASK-191 +**Type:** edit + +**Goal** +Each skill has semver. Reload picks up new code without restart. + +**Files** +- edit: SkillManager. + +**Implementation sketch** +- File watcher on skills directory. +- Reload + bump version on change. + +**Acceptance** +- [ ] Edit + save: skill rebuilds in seconds. + +**Verification** +- Edit a skill, observe. + +--- + +### TASK-193 — Per-skill rate limit + +**Section:** agent +**Effort:** S +**Depends on:** TASK-191 +**Type:** edit + +**Goal** +Some skills are expensive. Rate-limit per skill (e.g. browser automation: 10/min). + +**Files** +- edit: SkillManager invoke path — Redis counter per skill. + +**Acceptance** +- [ ] Over-rate skill call: rejected with 429-style error. + +**Verification** +- Loop invocations. + +--- + +### TASK-194 — /api/skills discovery + +**Section:** agent +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Already exists at server.ts:744. Verify completeness; add `description`, `version`, `rateLimit`, `lastUsedAt` fields. + +**Files** +- edit: existing handler. + +**Acceptance** +- [ ] All fields present. + +**Verification** +- Curl. + +--- + +### TASK-195 — Skill source viewer endpoint + +**Section:** agent +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Show skill source code for inspection. + +**Files** +- new: `GET /api/skills/:id/source` (admin-gated). + +**Acceptance** +- [ ] Returns source file content. + +**Verification** +- Curl. + +--- + +### TASK-196 — /api/agent/tools log dump + +**Section:** agent +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Recent tool invocations with args + results for debugging. + +**Files** +- new endpoint. + +**API contract** +``` +GET /api/agent/tools?since=&limit=100 +→ 200 { items: [{ tool, args, result, durationMs, ts }] } +``` + +**Acceptance** +- [ ] Returns recent tool calls. + +**Verification** +- Curl. + +--- + +### TASK-197 — Verification: typecheck after every code edit + +**Section:** agent +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +After each agent file edit, run `tsc --noEmit -p .` on the affected package. + +**Files** +- edit: AgentExecutor file-write path — invoke typecheck. + +**Acceptance** +- [ ] Typecheck failure blocks task completion. + +**Verification** +- Force a type error, observe. + +--- + +### TASK-198 — Verification: tests on changed packages + +**Section:** agent +**Effort:** S +**Depends on:** TASK-197 +**Type:** edit + +**Goal** +Run package tests of any package whose files were modified. + +**Files** +- edit: AgentExecutor. + +**Acceptance** +- [ ] Test failures block. + +**Verification** +- Force a test failure. + +--- + +### TASK-199 — Verification: prettier + +**Section:** agent +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +After each edit, run `prettier --write` on changed files. + +**Files** +- edit: AgentExecutor. + +**Acceptance** +- [ ] Files formatted post-edit. + +**Verification** +- Inspect agent commits. + +--- + +### TASK-200 — Auto-rollback on verification failure + +**Section:** agent +**Effort:** M +**Depends on:** TASK-197, TASK-198 +**Type:** edit + +**Goal** +If verification fails, `git reset --hard HEAD` to discard the bad changes. + +**Files** +- edit: AgentExecutor catch path. + +**Acceptance** +- [ ] Failed task: working tree restored. + +**Verification** +- Force failure, check git status. + +--- + +### TASK-201 — PR-mode toggle + +**Section:** agent +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Env `AGENT_PR_MODE=true` switches commits → branch + PR instead of direct main. + +**Files** +- edit: GitIntegration.ts. + +**Acceptance** +- [ ] PR mode opens PR via gh CLI. + +**Verification** +- Toggle env, run task. + +--- + +### TASK-202 — PR template generator from task + +**Section:** agent +**Effort:** S +**Depends on:** TASK-201 +**Type:** edit + +**Goal** +Generate PR title + body from task title + description + verification steps. + +**Files** +- edit: GitIntegration createPR path. + +**Acceptance** +- [ ] PR has structured body. + +**Verification** +- Open PR. + +--- + +### TASK-203 — PR auto-link to task ID + +**Section:** agent +**Effort:** S +**Depends on:** TASK-202 +**Type:** edit + +**Goal** +PR title includes `(TASK-NNN)` and body links to a tracker URL. + +**Files** +- edit: PR template. + +**Acceptance** +- [ ] PR title format: `feat: ... (TASK-NNN)`. + +**Verification** +- Open PR. + +--- + +### TASK-204 — Squash-merge agent for stacked commits + +**Section:** agent +**Effort:** S +**Depends on:** TASK-201 +**Type:** edit + +**Goal** +When merging via gh, use `--squash`. + +**Files** +- edit: GitIntegration mergePR. + +**Acceptance** +- [ ] Merged PR has 1 commit on main. + +**Verification** +- Merge. + +--- + +### TASK-205 — Branch-per-task workflow + +**Section:** agent +**Effort:** S +**Depends on:** TASK-201 +**Type:** edit + +**Goal** +Branch name `agent/TASK-NNN/` per task. + +**Files** +- edit: GitIntegration branch create. + +**Acceptance** +- [ ] Each task creates its own branch. + +**Verification** +- List branches. + +--- + +### TASK-206 — Auto-rebase on main before push + +**Section:** agent +**Effort:** S +**Depends on:** TASK-205 +**Type:** edit + +**Goal** +Before pushing PR branch, rebase on latest main to keep linear history. + +**Files** +- edit: GitIntegration push path. + +**Acceptance** +- [ ] Pushed branches are rebased. + +**Verification** +- Push, inspect. + +--- + +### TASK-207 — Conflict-resolution prompt to LLM + +**Section:** agent +**Effort:** M +**Depends on:** TASK-206 +**Type:** edit + +**Goal** +On rebase conflict, send conflict region + base + ours + theirs to LLM for resolution suggestion. + +**Files** +- edit: GitIntegration. + +**Implementation sketch** +- Parse conflict markers. +- Construct prompt; get suggested resolution; apply if confident. +- Otherwise leave for human. + +**Acceptance** +- [ ] Simple conflict resolved. + +**Verification** +- Synthetic conflict. + +--- + +### TASK-208 — Commitlint enforcement + +**Section:** agent +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Enforce conventional-commits (`feat: ...`, `fix: ...`, etc.) on every agent commit. + +**Files** +- new: `commitlint.config.js`. +- edit: GitIntegration commit path — validate before write. + +**Acceptance** +- [ ] Non-conventional message: rejected. + +**Verification** +- Manual test. + +--- + +### TASK-209 — Co-author tag on every agent commit + +**Section:** agent +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Already present per recent commits; codify in GitIntegration. + +**Files** +- edit: GitIntegration commit message builder. + +**Acceptance** +- [ ] All commits include Co-Authored-By trailer. + +**Verification** +- `git log | grep Co-Authored`. + +--- + +### TASK-210 — Sign-off (DCO) tag + +**Section:** agent +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Add `Signed-off-by: Hermes Agent ` for DCO compliance. + +**Files** +- edit: GitIntegration. + +**Acceptance** +- [ ] Trailer present. + +**Verification** +- Log inspection. + +--- + +### TASK-211 — Agent identity rotation per file area + +**Section:** agent +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +Different "personas" for backend/frontend/docs commits; each persona has its own author identity. + +**Files** +- edit: GitIntegration — pick author by file path. + +**Implementation sketch** +- Mapping `backend/* → Hermes Backend Agent`, `frontend/* → Hermes Frontend Agent`, `docs/* → Hermes Scribe`. + +**Acceptance** +- [ ] Commits authored by appropriate persona. + +**Verification** +- Inspect log. + +--- + +### TASK-212 — Per-area expertise routing + +**Section:** agent +**Effort:** M +**Depends on:** TASK-211 +**Type:** edit + +**Goal** +Route task to the persona whose area best matches; load area-specific system prompt. + +**Files** +- edit: AgentWorker getNextTask + executor system prompt selection. + +**Acceptance** +- [ ] Frontend tasks use frontend-tuned prompt. + +**Verification** +- Inspect prompt logs. + +--- + +### TASK-213 — Cross-task memory + +**Section:** agent +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +Already exists in agent_memory; ensure recent insights flow into new task's context. + +**Files** +- edit: AgentExecutor system prompt assembly. + +**Implementation sketch** +- Pull top 10 most-relevant memories (by tag match against task title) and embed in prompt. + +**Acceptance** +- [ ] Memories appear in prompt. + +**Verification** +- Inspect prompt. + +--- + +### TASK-214 — Per-task post-mortem 1-line + +**Section:** agent +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +After every task, ask LLM for a 1-sentence post-mortem; save to agent_memory. + +**Files** +- edit: AgentWorker completion hook. + +**Acceptance** +- [ ] post-mortem present per task. + +**Verification** +- Query agent_memory. + +--- + +### TASK-215 — Learning corpus index + +**Section:** agent +**Effort:** M +**Depends on:** TASK-214 +**Type:** new-file + +**Goal** +Vector-store index over post-mortems for retrieval-augmented prompting. + +**Files** +- new: `backend/src/agent/learningCorpus.ts`. +- add dep: a local embedding model or use Anthropic embeddings (via api). + +**Implementation sketch** +- On save, embed; on retrieve, k-NN. + +**Acceptance** +- [ ] Relevant past insights surfaced for new tasks. + +**Verification** +- Manual. + +--- + +## Summary + +35 tasks: 22 small, 11 medium. Heavy infra around verification gates + git workflow. diff --git a/docs/backlog/queue/06-frontend-hud.md b/docs/backlog/queue/06-frontend-hud.md new file mode 100644 index 00000000..caf8de58 --- /dev/null +++ b/docs/backlog/queue/06-frontend-hud.md @@ -0,0 +1,1080 @@ +# Section 06 — Frontend / HUD Specs (TASK-216..265) + +50 tasks. New panels (tx detail, block detail, account, mempool live tail, reorg history, validator board, quorum ticker, VM trace viewer, contract storage, event log, peer map, wallet/HD address tree, send form, token grid, faucet button, address book, name registry browse), theme variants + picker, agent reasoning panel, cost ticker, SSE robustness (reconnect, channel selector, replay), keyboard shortcuts + cmd-k palette, search bars + autocomplete + deep-link, prefs storage, notifications, embed/PWA/mobile, charts/a11y polish. + +**Preconditions used throughout:** +- Frontend root: [frontend/src/App.tsx](frontend/src/App.tsx). +- HUD state: [frontend/src/useHermesDockState.ts](frontend/src/useHermesDockState.ts). +- Theme: [frontend/src/useTheme.ts](frontend/src/useTheme.ts). +- Styles: [frontend/src/index.css](frontend/src/index.css). + +--- + +### TASK-216 — Tx detail modal + +**Section:** hud +**Effort:** M +**Depends on:** TASK-160 +**Type:** new-file + +**Goal** +Click any tx hash → modal showing full tx data, status, gas, decoded logs. + +**Files** +- new: `frontend/src/components/TxDetailModal.tsx`. +- edit: App.tsx — render conditionally on `selectedTxHash`. + +**Implementation sketch** +- Fetch from `/api/tx/:hash?decodeLogs=true`. +- Show: from, to, value, gas, status, logs table. + +**Acceptance** +- [ ] Modal opens on click; data loads. + +**Verification** +- Click in HUD. + +--- + +### TASK-217 — Block detail drawer + +**Section:** hud +**Effort:** M +**Depends on:** TASK-159 +**Type:** new-file + +**Goal** +Side drawer with full block info + receipts list. + +**Files** +- new: `frontend/src/components/BlockDetailDrawer.tsx`. + +**Implementation sketch** +- Slide-in from right. +- Fetch `/api/blocks/:height?include=receipts`. + +**Acceptance** +- [ ] Drawer renders correctly. + +**Verification** +- Click block height in HUD. + +--- + +### TASK-218 — Account detail panel + +**Section:** hud +**Effort:** S +**Depends on:** TASK-058 +**Type:** new-file + +**Goal** +Panel with balance, nonce, history, name (if registered), tags. + +**Files** +- new: `frontend/src/components/AccountDetail.tsx`. + +**Acceptance** +- [ ] Panel renders. + +**Verification** +- Navigate to /account/:addr. + +--- + +### TASK-219 — Mempool live-tail panel + +**Section:** hud +**Effort:** M +**Depends on:** TASK-048 +**Type:** new-file + +**Goal** +SSE-backed live list of pending txs with auto-scroll. + +**Files** +- new: `frontend/src/components/MempoolPanel.tsx`. + +**Acceptance** +- [ ] New tx appears in real time. + +**Verification** +- Submit tx, observe. + +--- + +### TASK-220 — Reorg history widget + +**Section:** hud +**Effort:** S +**Depends on:** TASK-060 +**Type:** new-file + +**Goal** +Sidebar widget listing recent reorg events with depth + age. + +**Files** +- new: `frontend/src/components/ReorgWidget.tsx`. + +**Acceptance** +- [ ] Lists last 10. + +**Verification** +- Trigger reorg. + +--- + +### TASK-221 — Validator board with rotating producer indicator + +**Section:** hud +**Effort:** M +**Depends on:** TASK-157 +**Type:** new-file + +**Goal** +Grid of validators with avatar, stake, blocks_produced, uptime; current producer pulses. + +**Files** +- new: `frontend/src/components/ValidatorBoard.tsx`. + +**Acceptance** +- [ ] Producer pulses on each new block. + +**Verification** +- Watch over multiple blocks. + +--- + +### TASK-222 — Quorum-vote live ticker + +**Section:** hud +**Effort:** S +**Depends on:** TASK-014 +**Type:** new-file + +**Goal** +Per-block list of who approved / rejected, animated as votes land. + +**Files** +- new: `frontend/src/components/QuorumTicker.tsx`. + +**Acceptance** +- [ ] Votes appear in order. + +**Verification** +- Multi-validator sim. + +--- + +### TASK-223 — VM execution trace viewer + +**Section:** hud +**Effort:** M +**Depends on:** TASK-083 +**Type:** new-file + +**Goal** +Tx detail modal extension: toggle "Show trace" → step-by-step opcode + stack viewer. + +**Files** +- new: `frontend/src/components/VmTraceView.tsx`. + +**Acceptance** +- [ ] Renders trace from /api/tx/:hash/trace. + +**Verification** +- Pick a VM tx. + +--- + +### TASK-224 — Contract storage browser + +**Section:** hud +**Effort:** S +**Depends on:** TASK-100 +**Type:** new-file + +**Goal** +Read /api/contract/:addr/storage; show key/value table with prefix filter. + +**Files** +- new: component. + +**Acceptance** +- [ ] Filter works. + +**Verification** +- Navigate to deployed contract. + +--- + +### TASK-225 — Event log viewer with topic filter + +**Section:** hud +**Effort:** S +**Depends on:** TASK-024 +**Type:** new-file + +**Goal** +Filtered view of /api/chain/logs. + +**Files** +- new: component. + +**Acceptance** +- [ ] Filter applies. + +**Verification** +- Trigger logs, filter. + +--- + +### TASK-226 — Gas-price chart last 1000 blocks + +**Section:** hud +**Effort:** M +**Depends on:** TASK-050 +**Type:** new-file + +**Goal** +Line chart from /api/chain/gas-stats. + +**Files** +- new: `frontend/src/charts/GasPriceChart.tsx`. +- add dep: `recharts` or similar. + +**Acceptance** +- [ ] Chart renders. + +**Verification** +- Visual. + +--- + +### TASK-227 — Block-time chart + +**Section:** hud +**Effort:** S +**Depends on:** TASK-052 +**Type:** new-file + +**Goal** +Histogram from /api/chain/block-times. + +**Files** +- new: chart. + +**Acceptance** +- [ ] Renders. + +**Verification** +- Visual. + +--- + +### TASK-228 — TPS sparkline in header + +**Section:** hud +**Effort:** S +**Depends on:** TASK-051 +**Type:** edit + +**Goal** +Tiny sparkline next to TPS chip. + +**Files** +- edit: App.tsx header chips. + +**Acceptance** +- [ ] Sparkline updates. + +**Verification** +- Visual. + +--- + +### TASK-229 — Network peer map (graph view) + +**Section:** hud +**Effort:** L +**Depends on:** TASK-005 +**Type:** new-file + +**Goal** +Force-directed graph of known peers with edges showing chainHeight delta. + +**Files** +- new: component. +- add dep: `react-force-graph`. + +**Acceptance** +- [ ] Graph renders with peers. + +**Verification** +- Visual. + +--- + +### TASK-230 — Peer latency table + +**Section:** hud +**Effort:** S +**Depends on:** TASK-005 +**Type:** new-file + +**Goal** +Sortable table of peers + last-seen + RTT. + +**Files** +- new: component. + +**Acceptance** +- [ ] Sort works. + +**Verification** +- Visual. + +--- + +### TASK-231 — Wallet panel: HD address tree + +**Section:** hud +**Effort:** M +**Depends on:** TASK-106 +**Type:** new-file + +**Goal** +Tree of derived addresses (m/44'/9999'/0'/0/0..N) with balance per leaf. + +**Files** +- new: component. + +**Acceptance** +- [ ] Tree expands; balances shown. + +**Verification** +- Visual. + +--- + +### TASK-232 — Send-tx form with gas estimator + +**Section:** hud +**Effort:** M +**Depends on:** TASK-056 +**Type:** new-file + +**Goal** +To-address + amount + auto gas estimate via /api/tx/estimate-gas. + +**Files** +- new: component. + +**Acceptance** +- [ ] Estimate appears as user types. + +**Verification** +- UI test. + +--- + +### TASK-233 — Token balance grid + +**Section:** hud +**Effort:** S +**Depends on:** TASK-118 +**Type:** new-file + +**Goal** +Grid of token holdings per active wallet. + +**Files** +- new: component. + +**Acceptance** +- [ ] Grid populated. + +**Verification** +- Visual. + +--- + +### TASK-234 — Approve / transfer modal + +**Section:** hud +**Effort:** S +**Depends on:** TASK-119 +**Type:** new-file + +**Goal** +Modal for token approve + transferFrom flow. + +**Files** +- new: component. + +**Acceptance** +- [ ] Both actions work. + +**Verification** +- E2E. + +--- + +### TASK-235 — Faucet button + cooldown timer + +**Section:** hud +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +One-click drip with countdown if cooldown active. + +**Files** +- new: component. + +**Acceptance** +- [ ] Disabled during cooldown. + +**Verification** +- Click + wait. + +--- + +### TASK-236 — Address book panel + +**Section:** hud +**Effort:** S +**Depends on:** TASK-117 +**Type:** new-file + +**Goal** +List + add/edit/delete contacts. + +**Files** +- new: component. + +**Acceptance** +- [ ] CRUD works. + +**Verification** +- UI test. + +--- + +### TASK-237 — Name registry browse + +**Section:** hud +**Effort:** S +**Depends on:** TASK-112 +**Type:** new-file + +**Goal** +Searchable list of registered names. + +**Files** +- new: component. + +**Acceptance** +- [ ] Search works. + +**Verification** +- UI test. + +--- + +### TASK-238 — Theme: high-contrast variant + +**Section:** hud +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +A11y theme: black bg, white text, no antialiasing softening. + +**Files** +- edit: index.css — add `[data-theme='high-contrast']` block. +- edit: useTheme.ts — accept this theme. + +**Acceptance** +- [ ] Toggling shows variant. + +**Verification** +- Visual. + +--- + +### TASK-239 — Theme: amber CRT variant + +**Section:** hud +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Retro amber phosphor look. + +**Files** +- edit: same as TASK-238. + +**Acceptance** +- [ ] Variant applied. + +**Verification** +- Visual. + +--- + +### TASK-240 — Theme picker dropdown + +**Section:** hud +**Effort:** S +**Depends on:** TASK-238, TASK-239 +**Type:** edit + +**Goal** +Replace the binary toggle with a dropdown listing all themes. + +**Files** +- edit: header. + +**Acceptance** +- [ ] Picker switches themes. + +**Verification** +- UI. + +--- + +### TASK-241 — Compact mode + +**Section:** hud +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Toggle that reduces padding, font size, row height for dense displays. + +**Files** +- edit: index.css + useTheme. + +**Acceptance** +- [ ] Compact applied. + +**Verification** +- Visual. + +--- + +### TASK-242 — Stage agent reasoning panel + +**Section:** hud +**Effort:** M +**Depends on:** TASK-188 +**Type:** new-file + +**Goal** +Real-time tool-call chain with collapsible nesting. + +**Files** +- new: component. + +**Acceptance** +- [ ] Tool calls render hierarchically. + +**Verification** +- Visual during agent run. + +--- + +### TASK-243 — Per-task progress bar in HUD + +**Section:** hud +**Effort:** S +**Depends on:** TASK-183 +**Type:** edit + +**Goal** +Bar showing actual vs estimated minutes. + +**Files** +- edit: existing task card. + +**Acceptance** +- [ ] Bar updates. + +**Verification** +- Visual. + +--- + +### TASK-244 — Cost ticker (today's spend) + +**Section:** hud +**Effort:** S +**Depends on:** TASK-189 +**Type:** edit + +**Goal** +Header chip showing $ today. + +**Files** +- edit: header. + +**Acceptance** +- [ ] Updates as tasks complete. + +**Verification** +- Visual. + +--- + +### TASK-245 — SSE reconnect with exponential backoff + +**Section:** hud +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +On disconnect, retry: 1s, 2s, 4s, 8s, capped at 30s. + +**Files** +- edit: SSE wiring in useHermesDockState. + +**Acceptance** +- [ ] Reconnects after drop. + +**Verification** +- Kill backend briefly. + +--- + +### TASK-246 — SSE channel selector + +**Section:** hud +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +UI to subscribe only to channels of interest. + +**Files** +- new: component. + +**Acceptance** +- [ ] Toggling reduces traffic. + +**Verification** +- Network tab. + +--- + +### TASK-247 — Keyboard shortcuts overlay (?) + +**Section:** hud +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Press `?` → modal listing all shortcuts. + +**Files** +- new: component. + +**Acceptance** +- [ ] Modal shows on press. + +**Verification** +- Press. + +--- + +### TASK-248 — Command palette cmd-k + +**Section:** hud +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Cmd-K opens fuzzy search over all routes + actions. + +**Files** +- new: component. +- add dep: `cmdk`. + +**Acceptance** +- [ ] Palette opens; search works. + +**Verification** +- Press. + +--- + +### TASK-249 — Block search bar + +**Section:** hud +**Effort:** S +**Depends on:** TASK-153 +**Type:** new-file + +**Goal** +Header bar to jump to any block. + +**Files** +- new: component. + +**Acceptance** +- [ ] Search jumps to block. + +**Verification** +- Type + enter. + +--- + +### TASK-250 — Address search with autocomplete + +**Section:** hud +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Header search; autocompletes from address book + recent. + +**Files** +- new: component. + +**Acceptance** +- [ ] Suggestions appear. + +**Verification** +- Type. + +--- + +### TASK-251 — Tx hash paste auto-route + +**Section:** hud +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Paste tx hash anywhere → auto-open detail modal. + +**Files** +- edit: global paste handler. + +**Acceptance** +- [ ] Paste opens modal. + +**Verification** +- Paste known hash. + +--- + +### TASK-252 — URL deep-link state restore + +**Section:** hud +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +Selected block / tx / account encoded in URL hash so refresh preserves view. + +**Files** +- edit: state hooks to read/write URL. + +**Acceptance** +- [ ] Refresh keeps view. + +**Verification** +- Open, refresh. + +--- + +### TASK-253 — Shareable view URLs preserve filters + +**Section:** hud +**Effort:** S +**Depends on:** TASK-252 +**Type:** edit + +**Goal** +Filters (e.g. log topic, block range) included in URL. + +**Files** +- edit: filter components. + +**Acceptance** +- [ ] Sharing URL reproduces filtered view. + +**Verification** +- Share + open in fresh window. + +--- + +### TASK-254 — Local hud preferences in localStorage + +**Section:** hud +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Theme, compact mode, channel selections persist. + +**Files** +- edit: useTheme + state hooks. + +**Acceptance** +- [ ] Refresh keeps prefs. + +**Verification** +- Refresh. + +--- + +### TASK-255 — Server-synced HUD prefs per API key + +**Section:** hud +**Effort:** M +**Depends on:** TASK-254 +**Type:** new-file + +**Goal** +For logged-in users, sync prefs to server (covered by API key). + +**Files** +- new: `GET/PUT /api/prefs`. +- new migration `user_prefs(api_key_hash PK, prefs_json)`. + +**Acceptance** +- [ ] Cross-device sync. + +**Verification** +- Two browsers. + +--- + +### TASK-256 — Toast notifications for own-wallet activity + +**Section:** hud +**Effort:** S +**Depends on:** TASK-172 +**Type:** new-file + +**Goal** +When a tx involving your wallet lands, show toast. + +**Files** +- new: component. + +**Acceptance** +- [ ] Toast appears. + +**Verification** +- Self-tx. + +--- + +### TASK-257 — Browser push notification opt-in + +**Section:** hud +**Effort:** S +**Depends on:** TASK-256 +**Type:** new-file + +**Goal** +Same trigger but via Notification API. + +**Files** +- edit: same component, add Notification.requestPermission flow. + +**Acceptance** +- [ ] Push notification fires. + +**Verification** +- Permit + trigger. + +--- + +### TASK-258 — Embed-mode iframe + +**Section:** hud +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +`?embed=true` query strips chrome for iframe embedding. + +**Files** +- edit: App.tsx. + +**Acceptance** +- [ ] Embed view = no chrome. + +**Verification** +- Visit with param. + +--- + +### TASK-259 — Mobile-responsive collapse + +**Section:** hud +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +Below 768px: collapse sidebar, stack panels. + +**Files** +- edit: index.css. + +**Acceptance** +- [ ] Layout adapts. + +**Verification** +- Resize browser. + +--- + +### TASK-260 — PWA manifest + offline shell + +**Section:** hud +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Installable PWA. + +**Files** +- new: `frontend/public/manifest.webmanifest`. +- edit: index.html. + +**Acceptance** +- [ ] Installable. + +**Verification** +- Chrome install icon. + +--- + +### TASK-261 — Service worker for static assets + +**Section:** hud +**Effort:** S +**Depends on:** TASK-260 +**Type:** new-file + +**Goal** +Cache static assets for offline shell. + +**Files** +- new: `frontend/public/sw.js`. + +**Acceptance** +- [ ] Offline → shell still loads. + +**Verification** +- DevTools offline mode. + +--- + +### TASK-262 — Reusable chart component + +**Section:** hud +**Effort:** S +**Depends on:** TASK-226 +**Type:** new-file + +**Goal** +Single `` component used by all chart panels. + +**Files** +- new: component. + +**Acceptance** +- [ ] Used in 3+ panels. + +**Verification** +- Inspect. + +--- + +### TASK-263 — A11y pass: aria-labels everywhere + +**Section:** hud +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +Every button/link gets descriptive aria-label. + +**Files** +- edit: across components. + +**Acceptance** +- [ ] axe-core scan: 0 errors. + +**Verification** +- Run axe. + +--- + +### TASK-264 — Keyboard-trap audit on modals + +**Section:** hud +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Tab key cycles within modal; Escape closes. + +**Files** +- edit: modal components. + +**Acceptance** +- [ ] Trap correct. + +**Verification** +- Tab through. + +--- + +### TASK-265 — Focus-visible styling pass + +**Section:** hud +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +:focus-visible ring on all interactives. + +**Files** +- edit: index.css. + +**Acceptance** +- [ ] Tab navigation shows rings. + +**Verification** +- Tab around. + +--- + +## Summary + +50 tasks: 32 small, 17 medium, 1 large. Visible-progress cluster (most produce immediate UI value). diff --git a/docs/backlog/queue/07-docs-site.md b/docs/backlog/queue/07-docs-site.md new file mode 100644 index 00000000..980565a9 --- /dev/null +++ b/docs/backlog/queue/07-docs-site.md @@ -0,0 +1,849 @@ +# Section 07 — Docs & Site Specs (TASK-266..305) + +40 tasks. Landing tagline, /docs pages (architecture, consensus, vm, api, sdk), SDK packages (TS, Python, Rust) + CLI, examples, tutorials, whitepaper, glossary, FAQ, roadmap, contributing, style guide, code of conduct, threat model, security.txt, bug bounty, status page, press kit. + +--- + +### TASK-266 — Landing page tagline rewrite + +**Section:** docs +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Refresh the landing tagline to reflect current state (not aspirational). + +**Files** +- edit: `frontend/src/App.tsx` landing section. + +**Acceptance** +- [ ] New tagline reflects shipped tier-3 features. + +**Verification** +- Visual review. + +--- + +### TASK-267 — /docs/architecture rewrite + +**Section:** docs +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Comprehensive architecture overview with diagrams. + +**Files** +- new: `docs/architecture/overview.md`. + +**Acceptance** +- [ ] Page covers all major subsystems. + +**Verification** +- Review. + +--- + +### TASK-268 — /docs/consensus page + +**Section:** docs +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Consensus algorithm reference: producer rotation, quorum, finality, slashing. + +**Files** +- new: `docs/consensus.md`. + +**Acceptance** +- [ ] Page complete. + +**Verification** +- Review. + +--- + +### TASK-269 — /docs/vm spec page + +**Section:** docs +**Effort:** S +**Depends on:** TASK-103 +**Type:** docs + +**Goal** +Already covered by TASK-103. Linkage doc. + +**Files** +- (covered) + +**Acceptance** +- [ ] Linked from /docs index. + +**Verification** +- Click. + +--- + +### TASK-270 — /docs/api reference + +**Section:** docs +**Effort:** S +**Depends on:** TASK-141, TASK-142 +**Type:** edit + +**Goal** +Markdown wrapper that embeds Swagger UI. + +**Files** +- new: `docs/api.md` linking to /docs (Swagger). + +**Acceptance** +- [ ] Page links to live Swagger. + +**Verification** +- Visit. + +--- + +### TASK-271 — /docs/sdk page + +**Section:** docs +**Effort:** S +**Depends on:** TASK-273 +**Type:** new-file + +**Goal** +SDK overview with install + quickstart. + +**Files** +- new: `docs/sdk.md`. + +**Acceptance** +- [ ] Page complete. + +**Verification** +- Review. + +--- + +### TASK-272 — SDK quickstart code samples (TS + Python) + +**Section:** docs +**Effort:** S +**Depends on:** TASK-273, TASK-278 +**Type:** new-file + +**Goal** +Five-line quickstart per language: install, init client, get balance. + +**Files** +- new: `docs/sdk/quickstart-ts.md`, `docs/sdk/quickstart-py.md`. + +**Acceptance** +- [ ] Samples runnable. + +**Verification** +- Run. + +--- + +### TASK-273 — SDK npm package skeleton + +**Section:** docs +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Publishable `@hermeschain/sdk` npm package. + +**Files** +- new: `sdk/typescript/package.json`, `index.ts`, etc. + +**Acceptance** +- [ ] `npm pack` produces tarball. + +**Verification** +- npm pack. + +--- + +### TASK-274 — SDK chain client class + +**Section:** docs +**Effort:** M +**Depends on:** TASK-273 +**Type:** edit + +**Goal** +`HermesClient` class with methods for blocks/txs/accounts. + +**Files** +- new: `sdk/typescript/src/client.ts`. + +**Acceptance** +- [ ] Methods covered by sample. + +**Verification** +- Run sample. + +--- + +### TASK-275 — SDK wallet helper + +**Section:** docs +**Effort:** S +**Depends on:** TASK-273 +**Type:** edit + +**Goal** +Keypair creation + signing helpers. + +**Files** +- new: `sdk/typescript/src/wallet.ts`. + +**Acceptance** +- [ ] Helpers callable. + +**Verification** +- Sample. + +--- + +### TASK-276 — SDK VM helper for op programs + +**Section:** docs +**Effort:** S +**Depends on:** TASK-273 +**Type:** edit + +**Goal** +Builder API: `program().push(1).push(2).add().log({...}).stop().build()`. + +**Files** +- new: `sdk/typescript/src/vm.ts`. + +**Acceptance** +- [ ] Builder produces valid programs. + +**Verification** +- Run sample. + +--- + +### TASK-277 — SDK signed-tx builder + +**Section:** docs +**Effort:** S +**Depends on:** TASK-275 +**Type:** edit + +**Goal** +`buildAndSignTx({ from, to, value, ...}, privateKey)` → signed payload. + +**Files** +- new: `sdk/typescript/src/tx.ts`. + +**Acceptance** +- [ ] Output accepted by API. + +**Verification** +- Submit + observe. + +--- + +### TASK-278 — Python SDK skeleton + +**Section:** docs +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Publishable `hermeschain` PyPI package. + +**Files** +- new: `sdk/python/setup.cfg`, `hermeschain/`. + +**Acceptance** +- [ ] `pip install -e .` works. + +**Verification** +- pip. + +--- + +### TASK-279 — Rust SDK skeleton + +**Section:** docs +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Publishable crate. + +**Files** +- new: `sdk/rust/Cargo.toml`, `src/lib.rs`. + +**Acceptance** +- [ ] `cargo build` works. + +**Verification** +- cargo. + +--- + +### TASK-280 — CLI tool `hermes` npm bin + +**Section:** docs +**Effort:** S +**Depends on:** TASK-273 +**Type:** new-file + +**Goal** +`@hermeschain/cli` package providing `hermes` command. + +**Files** +- new: `cli/package.json`, `bin/hermes.ts`. + +**Acceptance** +- [ ] `npx hermes --help` lists commands. + +**Verification** +- npx. + +--- + +### TASK-281 — CLI: hermes balance + +**Section:** docs +**Effort:** S +**Depends on:** TASK-280 +**Type:** edit + +**Goal** +Print balance. + +**Files** +- edit: cli. + +**Acceptance** +- [ ] Returns numeric. + +**Verification** +- Run. + +--- + +### TASK-282 — CLI: hermes send + +**Section:** docs +**Effort:** S +**Depends on:** TASK-280 +**Type:** edit + +**Goal** +Submit a tx. + +**Files** +- edit: cli. + +**Acceptance** +- [ ] Returns tx hash. + +**Verification** +- Run + observe. + +--- + +### TASK-283 — CLI: hermes call + +**Section:** docs +**Effort:** S +**Depends on:** TASK-280 +**Type:** edit + +**Goal** +Read-only contract call. + +**Files** +- edit: cli. + +**Acceptance** +- [ ] Returns result. + +**Verification** +- Run. + +--- + +### TASK-284 — CLI: hermes deploy + +**Section:** docs +**Effort:** S +**Depends on:** TASK-280 +**Type:** edit + +**Goal** +Deploy contract from JSON-op file. + +**Files** +- edit: cli. + +**Acceptance** +- [ ] Returns address. + +**Verification** +- Run. + +--- + +### TASK-285 — CLI: hermes node start (local node) + +**Section:** docs +**Effort:** M +**Depends on:** TASK-280 +**Type:** edit + +**Goal** +Start a local Hermeschain node for dev. + +**Files** +- edit: cli — invoke backend in dev mode with sqlite/in-memory PG. + +**Acceptance** +- [ ] Node boots locally. + +**Verification** +- Run. + +--- + +### TASK-286 — /examples/counter walkthrough + +**Section:** docs +**Effort:** S +**Depends on:** TASK-105 +**Type:** docs + +**Goal** +Already covered. Linkage. + +**Files** +- (covered) + +**Acceptance** +- [ ] Linked. + +**Verification** +- Click. + +--- + +### TASK-287 — /examples/erc20-like walkthrough + +**Section:** docs +**Effort:** S +**Depends on:** TASK-105 +**Type:** docs + +**Goal** +Same as 286. + +**Files** +- (covered) + +**Acceptance** +- [ ] Linked. + +**Verification** +- Click. + +--- + +### TASK-288 — /examples/multisig walkthrough + +**Section:** docs +**Effort:** S +**Depends on:** TASK-105 +**Type:** docs + +**Goal** +Same. + +**Files** +- (covered) + +**Acceptance** +- [ ] Linked. + +**Verification** +- Click. + +--- + +### TASK-289 — /examples/oracle walkthrough + +**Section:** docs +**Effort:** M +**Depends on:** TASK-105 +**Type:** new-file + +**Goal** +Worked oracle (push price feed) example. + +**Files** +- new: `examples/oracle/`. + +**Acceptance** +- [ ] Deploys + works. + +**Verification** +- Run. + +--- + +### TASK-290 — Tutorial: build your first contract + +**Section:** docs +**Effort:** S +**Depends on:** TASK-104, TASK-280 +**Type:** new-file + +**Goal** +Step-by-step from blank to deployed counter. + +**Files** +- new: `docs/tutorials/first-contract.md`. + +**Acceptance** +- [ ] Tutorial complete. + +**Verification** +- Walk through. + +--- + +### TASK-291 — Tutorial: run your own validator + +**Section:** docs +**Effort:** S +**Depends on:** TASK-013 +**Type:** new-file + +**Goal** +Setup guide. + +**Files** +- new: `docs/tutorials/run-validator.md`. + +**Acceptance** +- [ ] Tutorial complete. + +**Verification** +- Walk through. + +--- + +### TASK-292 — Tutorial: query the chain + +**Section:** docs +**Effort:** S +**Depends on:** TASK-273 +**Type:** new-file + +**Goal** +SDK + API examples. + +**Files** +- new: `docs/tutorials/query.md`. + +**Acceptance** +- [ ] Tutorial complete. + +**Verification** +- Walk. + +--- + +### TASK-293 — Tutorial: submit a tx from a script + +**Section:** docs +**Effort:** S +**Depends on:** TASK-277 +**Type:** new-file + +**Goal** +Build + sign + submit walkthrough. + +**Files** +- new: `docs/tutorials/submit-tx.md`. + +**Acceptance** +- [ ] Tutorial complete. + +**Verification** +- Walk. + +--- + +### TASK-294 — Whitepaper draft v0.1 + +**Section:** docs +**Effort:** L +**Depends on:** TASK-267, TASK-268, TASK-269 +**Type:** new-file + +**Goal** +PDF/MD whitepaper covering motivation, design, security, roadmap. + +**Files** +- new: `docs/whitepaper/v0.1.md`. + +**Acceptance** +- [ ] Draft complete. + +**Verification** +- Review. + +--- + +### TASK-295 — Glossary page + +**Section:** docs +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Define every term used. + +**Files** +- new: `docs/glossary.md`. + +**Acceptance** +- [ ] Comprehensive. + +**Verification** +- Review. + +--- + +### TASK-296 — FAQ page + +**Section:** docs +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Answer common questions. + +**Files** +- new: `docs/faq.md`. + +**Acceptance** +- [ ] 20+ Qs. + +**Verification** +- Review. + +--- + +### TASK-297 — Roadmap page + +**Section:** docs +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Public roadmap. + +**Files** +- new: `docs/roadmap.md`. + +**Acceptance** +- [ ] Tier 1-3 visible. + +**Verification** +- Review. + +--- + +### TASK-298 — Contributing guide + +**Section:** docs +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +How to contribute. + +**Files** +- new: `CONTRIBUTING.md`. + +**Acceptance** +- [ ] Covers dev setup, PR flow. + +**Verification** +- Review. + +--- + +### TASK-299 — Style guide for agent-authored code + +**Section:** docs +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Conventions for files written by the agent. + +**Files** +- new: `docs/style-guide.md`. + +**Acceptance** +- [ ] Covers naming, imports, comments. + +**Verification** +- Review. + +--- + +### TASK-300 — Code of conduct + +**Section:** docs +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Standard COC. + +**Files** +- new: `CODE_OF_CONDUCT.md`. + +**Acceptance** +- [ ] CoC present. + +**Verification** +- Review. + +--- + +### TASK-301 — Threat model document + +**Section:** docs +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +STRIDE-style threat model. + +**Files** +- new: `docs/security/threat-model.md`. + +**Acceptance** +- [ ] All trust boundaries covered. + +**Verification** +- Review. + +--- + +### TASK-302 — Disclosure / security.txt + +**Section:** docs +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +RFC 9116 security.txt. + +**Files** +- new: `frontend/public/.well-known/security.txt`. + +**Acceptance** +- [ ] Path returns text. + +**Verification** +- Curl. + +--- + +### TASK-303 — Bug bounty page + +**Section:** docs +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Scope, severity tiers, payouts. + +**Files** +- new: `docs/security/bug-bounty.md`. + +**Acceptance** +- [ ] Page complete. + +**Verification** +- Review. + +--- + +### TASK-304 — Status page stub + +**Section:** docs +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Static status page (manually updated for now). + +**Files** +- new: `frontend/src/pages/Status.tsx`. + +**Acceptance** +- [ ] Page renders. + +**Verification** +- Visit. + +--- + +### TASK-305 — Press kit (logos, screenshots) + +**Section:** docs +**Effort:** S +**Depends on:** TASK-434 +**Type:** new-file + +**Goal** +Public press-kit zip. + +**Files** +- new: `frontend/public/press-kit.zip` + `docs/press.md`. + +**Acceptance** +- [ ] Downloadable. + +**Verification** +- Curl. + +--- + +## Summary + +40 tasks: 32 small, 7 medium, 1 large. Heavy docs cluster. diff --git a/docs/backlog/queue/08-database-ops.md b/docs/backlog/queue/08-database-ops.md new file mode 100644 index 00000000..9f05e2b5 --- /dev/null +++ b/docs/backlog/queue/08-database-ops.md @@ -0,0 +1,1226 @@ +# Section 08 — Database & Ops Specs (TASK-306..335) + +30 tasks. Migrations, query observability, replica/cache infrastructure, backup/restore, job-queue hardening. Authored first because later sections cite these migration filenames and helper functions by name. + +**Preconditions used throughout this section:** +- Migration runner contract at [backend/src/database/migrations.ts:25-60](backend/src/database/migrations.ts#L25-L60). Files named `NNNN_slug.sql`, split on literal `-- down:` line, `-- up:` half is what runs in production. +- Existing migrations: [backend/src/database/migrations/0001_receipts.sql](backend/src/database/migrations/0001_receipts.sql). +- Schema source: [backend/src/database/schema.ts](backend/src/database/schema.ts) — every CREATE TABLE has `IF NOT EXISTS`. New tables go in NEW migration files, not edits to schema.ts. +- DB wrapper: [backend/src/database/db.ts:81-94](backend/src/database/db.ts#L81-L94) exposes `db.query(sql, params) → {rows, rowCount}` and `db.exec(sql)` for multi-statement. +- Redis wrapper: [backend/src/database/db.ts:123-222](backend/src/database/db.ts#L123-L222) exposes `cache.get/set/getJSON/setJSON/incr/hget/hset/hgetall`. + +--- + +### TASK-306 — Migration 0002: index transactions(block_height) + +**Section:** db +**Effort:** S +**Depends on:** none +**Type:** migration + +**Goal** +The `transactions` table from [schema.ts:24-56](backend/src/database/schema.ts#L24-L56) has no index on `block_height`. Block-detail endpoints currently scan the full table to load a block's txs. Add a btree index so block lookups stay O(log n) as the chain grows. + +**Files** +- new: `backend/src/database/migrations/0002_tx_block_height_idx.sql` + +**Reuses** +- Pattern from [backend/src/database/migrations/0001_receipts.sql:17-19](backend/src/database/migrations/0001_receipts.sql#L17-L19) — `CREATE INDEX IF NOT EXISTS ... ON ... (...);` + +**Migration SQL** +```sql +-- up: +CREATE INDEX IF NOT EXISTS idx_transactions_block_height + ON transactions(block_height); + +-- down: +DROP INDEX IF EXISTS idx_transactions_block_height; +``` + +**Implementation sketch** +- Drop file in `backend/src/database/migrations/` with the exact name above. +- Verify lexical sort places it after 0001. +- No code changes; runner picks it up on next boot via `applyPendingMigrations()`. + +**Acceptance** +- [ ] `backend/src/database/migrations/0002_tx_block_height_idx.sql` exists with both `-- up:` and `-- down:` blocks. +- [ ] Boot logs `[MIGRATIONS] 0002_tx_block_height_idx applied in ms`. +- [ ] `SELECT * FROM pg_indexes WHERE indexname='idx_transactions_block_height'` returns one row. + +**Verification** +- Cold boot against an empty PG: `npm run dev` (backend), inspect logs for migration line. +- Hot boot (migration already applied): boot still succeeds, logs `All N migration(s) already applied`. +- `EXPLAIN ANALYZE SELECT * FROM transactions WHERE block_height = 100;` shows `Index Scan` not `Seq Scan`. + +--- + +### TASK-307 — Migration 0003: compound index transactions(from_address, nonce) + +**Section:** db +**Effort:** S +**Depends on:** none +**Type:** migration + +**Goal** +Mempool admission and `/api/account/:addr/next-nonce` (TASK-057) both compute "max nonce seen from this sender." Without a compound index, that's a full table scan per call. Add `(from_address, nonce DESC)` to make it an index-only lookup. + +**Files** +- new: `backend/src/database/migrations/0003_tx_from_nonce_idx.sql` + +**Reuses** +- Same `CREATE INDEX IF NOT EXISTS` pattern from 0001. + +**Migration SQL** +```sql +-- up: +CREATE INDEX IF NOT EXISTS idx_transactions_from_nonce + ON transactions(from_address, nonce DESC); + +-- down: +DROP INDEX IF EXISTS idx_transactions_from_nonce; +``` + +**Implementation sketch** +- Drop the SQL file. +- No code changes. + +**Acceptance** +- [ ] Index visible via `\d transactions` (psql) under `Indexes:`. +- [ ] `EXPLAIN SELECT MAX(nonce) FROM transactions WHERE from_address = '0xabc'` shows `Index Scan using idx_transactions_from_nonce`. + +**Verification** +- Cold boot, scan logs for migration line. + +--- + +### TASK-308 — Migration 0004: index accounts(balance DESC) for top-balances + +**Section:** db +**Effort:** S +**Depends on:** none +**Type:** migration + +**Goal** +The "Top accounts by balance" endpoint (TASK-155) ranks every account by balance. Without an index, that's a full sort over the accounts table on every call. Add a descending btree so the top-N is an index-range scan. + +**Files** +- new: `backend/src/database/migrations/0004_accounts_balance_idx.sql` + +**Reuses** +- `CREATE INDEX` pattern from 0001. + +**Migration SQL** +```sql +-- up: +CREATE INDEX IF NOT EXISTS idx_accounts_balance_desc + ON accounts(balance DESC); + +-- down: +DROP INDEX IF EXISTS idx_accounts_balance_desc; +``` + +**Implementation sketch** +- The `accounts` table from [schema.ts:58-66](backend/src/database/schema.ts#L58-L66) stores balance as TEXT (decimal-as-string). PostgreSQL btree sorts TEXT lexicographically — incorrect for numeric ordering. Two options: + 1. **Recommended:** add an additional column `balance_numeric NUMERIC` populated by trigger or ALTER, index that. + 2. Cast index: `CREATE INDEX ... ON accounts ((balance::numeric) DESC)` — works on PG. +- Pick option 2 (zero schema change). Document the cast in spec. + +**Migration SQL (revised, option 2)** +```sql +-- up: +CREATE INDEX IF NOT EXISTS idx_accounts_balance_desc + ON accounts ((balance::numeric) DESC); + +-- down: +DROP INDEX IF EXISTS idx_accounts_balance_desc; +``` + +**Acceptance** +- [ ] Index exists. +- [ ] `EXPLAIN SELECT address, balance FROM accounts ORDER BY balance::numeric DESC LIMIT 100;` uses the index. + +**Verification** +- After a few thousand tx, top-100 query returns in <50ms. + +--- + +### TASK-309 — Migration 0005: index receipts(status) for failure-rate queries + +**Section:** db +**Effort:** S +**Depends on:** TASK-306 implicitly (migrations are sequential) +**Type:** migration + +**Goal** +The `receipts` table from 0001 has indexes on block_number / from / to but not on `status`. "Failure rate over last N blocks" and "latest reverts" queries scan the full table. Add an index. + +**Files** +- new: `backend/src/database/migrations/0005_receipts_status_idx.sql` + +**Migration SQL** +```sql +-- up: +CREATE INDEX IF NOT EXISTS idx_receipts_status + ON receipts(status, block_number DESC); + +-- down: +DROP INDEX IF EXISTS idx_receipts_status; +``` + +**Implementation sketch** +- Compound `(status, block_number DESC)` so "latest 100 failed" = single range scan. +- No code changes. + +**Acceptance** +- [ ] Index applied. +- [ ] `EXPLAIN SELECT * FROM receipts WHERE status != 1 ORDER BY block_number DESC LIMIT 100;` uses it. + +**Verification** +- Inspect `pg_indexes` after boot. + +--- + +### TASK-310 — Migration 0006: GIN index on receipts.logs_json + +**Section:** db +**Effort:** S +**Depends on:** none +**Type:** migration + +**Goal** +The `/api/logs` endpoint (TASK-024) filters by topic / address inside the `logs_json` text column. Without a GIN index over the parsed JSON, every log query is a full receipts scan. Convert query path to JSONB and add a GIN index for membership queries. + +**Files** +- new: `backend/src/database/migrations/0006_receipts_logs_gin.sql` + +**Migration SQL** +```sql +-- up: +ALTER TABLE receipts + ADD COLUMN IF NOT EXISTS logs_jsonb JSONB + GENERATED ALWAYS AS (logs_json::jsonb) STORED; +CREATE INDEX IF NOT EXISTS idx_receipts_logs_gin + ON receipts USING GIN (logs_jsonb); + +-- down: +DROP INDEX IF EXISTS idx_receipts_logs_gin; +ALTER TABLE receipts DROP COLUMN IF EXISTS logs_jsonb; +``` + +**Implementation sketch** +- Use a generated column so existing writes through `storeReceipt()` in [TransactionReceipt.ts:205-245](backend/src/blockchain/TransactionReceipt.ts#L205-L245) keep going to `logs_json` (text) and PG materializes `logs_jsonb` automatically. +- TASK-024 will query `logs_jsonb @> '[{"address":"0x..."}]'` style. + +**Acceptance** +- [ ] Generated column populated for all existing rows. +- [ ] GIN index visible in `\d receipts`. +- [ ] `EXPLAIN SELECT * FROM receipts WHERE logs_jsonb @> '[{"address":"0xabc"}]'` uses index. + +**Verification** +- Run migration on a DB with existing receipts, count `WHERE logs_jsonb IS NOT NULL` matches `COUNT(*)`. + +--- + +### TASK-311 — Migration 0007: validators.stake column for weighted quorum + +**Section:** db +**Effort:** S +**Depends on:** none +**Type:** migration + +**Goal** +TASK-013 (weighted producer selection) and TASK-014 (stake-weighted quorum) need a `stake` column on the `validators` table from [schema.ts:68-87](backend/src/database/schema.ts#L68-L87). Add it with a default of 1 so existing rows keep current head-count quorum behavior. + +**Files** +- new: `backend/src/database/migrations/0007_validators_stake.sql` + +**Migration SQL** +```sql +-- up: +ALTER TABLE validators + ADD COLUMN IF NOT EXISTS stake NUMERIC NOT NULL DEFAULT 1; + +-- down: +ALTER TABLE validators DROP COLUMN IF EXISTS stake; +``` + +**Implementation sketch** +- NUMERIC chosen for arbitrary-precision (matches existing balance/value text-as-numeric pattern). +- DEFAULT 1 means TASK-014's `Math.ceil(2 * sumStake / 3)` reduces to the current `Math.ceil(2n/3)` until stakes are explicitly set. + +**Acceptance** +- [ ] Column visible in `\d validators`. +- [ ] All existing rows have `stake = 1`. + +**Verification** +- `SELECT address, stake FROM validators` shows the new column. + +--- + +### TASK-312 — Migration 0008: validator_slashes table + +**Section:** db +**Effort:** S +**Depends on:** TASK-311 (validators.stake referenced) +**Type:** migration + +**Goal** +TASK-011 (slashing record) and TASK-012 (slash on equivocation) need a persistent table of slashing events. One row per offense with the offending validator, height, evidence pointer, and stake decrement applied. + +**Files** +- new: `backend/src/database/migrations/0008_validator_slashes.sql` + +**Migration SQL** +```sql +-- up: +CREATE TABLE IF NOT EXISTS validator_slashes ( + id BIGSERIAL PRIMARY KEY, + validator_address TEXT NOT NULL, + block_height BIGINT NOT NULL, + reason TEXT NOT NULL, + evidence_json TEXT NOT NULL DEFAULT '{}', + stake_before NUMERIC NOT NULL, + stake_after NUMERIC NOT NULL, + slashed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_slashes_validator + ON validator_slashes(validator_address); +CREATE INDEX IF NOT EXISTS idx_slashes_height + ON validator_slashes(block_height); + +-- down: +DROP INDEX IF EXISTS idx_slashes_height; +DROP INDEX IF EXISTS idx_slashes_validator; +DROP TABLE IF EXISTS validator_slashes; +``` + +**Implementation sketch** +- TASK-011 will expose `GET /api/validator/:addr/slashes` reading from this table. +- TASK-012 will INSERT into this table when equivocation is detected, then `UPDATE validators SET stake = stake - `. + +**Acceptance** +- [ ] Table + 2 indexes present. +- [ ] FK pattern (informal — no hard FK to validators.address since that's not unique-indexed in the original schema; document the soft join). + +**Verification** +- `INSERT INTO validator_slashes (validator_address, block_height, reason, stake_before, stake_after) VALUES ('0xabc', 100, 'equivocation', 100, 90);` succeeds. + +--- + +### TASK-313 — Migration 0009: contract_code table + +**Section:** db +**Effort:** S +**Depends on:** none +**Type:** migration + +**Goal** +TASK-079 (CREATE opcode), TASK-081 (contract code storage), and TASK-082 (code-loaded execution) need persistent contract bytecode keyed by contract address. The `accounts` table has a `code` TEXT column already but it's never populated — moving code to its own table avoids bloating account rows and lets us add code-specific metadata later. + +**Files** +- new: `backend/src/database/migrations/0009_contract_code.sql` + +**Migration SQL** +```sql +-- up: +CREATE TABLE IF NOT EXISTS contract_code ( + address TEXT PRIMARY KEY, + code_hash TEXT NOT NULL, + bytecode TEXT NOT NULL, + deployed_at_block BIGINT NOT NULL, + deployed_by TEXT NOT NULL, + deployed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_contract_code_hash + ON contract_code(code_hash); +CREATE INDEX IF NOT EXISTS idx_contract_code_deployer + ON contract_code(deployed_by); + +-- down: +DROP INDEX IF EXISTS idx_contract_code_deployer; +DROP INDEX IF EXISTS idx_contract_code_hash; +DROP TABLE IF EXISTS contract_code; +``` + +**Implementation sketch** +- `bytecode` stored as JSON-op string (matches the `vm:` prefix format from [Interpreter.ts](backend/src/vm/Interpreter.ts)) — TEXT not BYTEA. +- `code_hash` enables dedup detection for identical contracts. +- TASK-082 will look up code via `SELECT bytecode FROM contract_code WHERE address = $1` before treating a tx as a plain transfer. + +**Acceptance** +- [ ] Table + 2 indexes present. +- [ ] Migration runs after 0008 (sequential). + +**Verification** +- `\d contract_code` shows expected schema. + +--- + +### TASK-314 — Migration 0010: contract_storage table + +**Section:** db +**Effort:** S +**Depends on:** TASK-313 +**Type:** migration + +**Goal** +TASK-068 (SLOAD) and TASK-069 (storage persistence) need durable per-contract key/value storage. The Interpreter currently keeps storage in-memory inside the execution result and discards it. Persist it. + +**Files** +- new: `backend/src/database/migrations/0010_contract_storage.sql` + +**Migration SQL** +```sql +-- up: +CREATE TABLE IF NOT EXISTS contract_storage ( + contract_address TEXT NOT NULL, + storage_key TEXT NOT NULL, + storage_value TEXT NOT NULL, + updated_at_block BIGINT NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (contract_address, storage_key) +); +CREATE INDEX IF NOT EXISTS idx_contract_storage_block + ON contract_storage(updated_at_block); + +-- down: +DROP INDEX IF EXISTS idx_contract_storage_block; +DROP TABLE IF EXISTS contract_storage; +``` + +**Implementation sketch** +- Composite PK lets SLOAD do `WHERE contract_address = $1 AND storage_key = $2` as an index-only lookup. +- `updated_at_block` index supports state-snapshot logic from TASK-035. +- TASK-069 will `INSERT ... ON CONFLICT (contract_address, storage_key) DO UPDATE SET storage_value = EXCLUDED.storage_value`. + +**Acceptance** +- [ ] Table with composite PK present. +- [ ] Block-index in place. + +**Verification** +- Insert two rows with same address+key but different values, second is upsert not duplicate-key error. + +--- + +### TASK-315 — Migration 0011: contract_metadata table + +**Section:** db +**Effort:** S +**Depends on:** TASK-313 +**Type:** migration + +**Goal** +TASK-097 (event ABI registry), TASK-098 (source verifier), TASK-101 (contract metadata) need per-contract metadata: human-readable name, JSON ABI, source URL, verifier status. Separate from `contract_code` so verification can be done lazily without touching the bytecode row. + +**Files** +- new: `backend/src/database/migrations/0011_contract_metadata.sql` + +**Migration SQL** +```sql +-- up: +CREATE TABLE IF NOT EXISTS contract_metadata ( + address TEXT PRIMARY KEY, + name TEXT, + abi_json TEXT, + source_url TEXT, + source_verified BOOLEAN NOT NULL DEFAULT FALSE, + verifier_notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_contract_metadata_verified + ON contract_metadata(source_verified) WHERE source_verified = TRUE; + +-- down: +DROP INDEX IF EXISTS idx_contract_metadata_verified; +DROP TABLE IF EXISTS contract_metadata; +``` + +**Implementation sketch** +- Partial index on `source_verified = TRUE` keeps "list verified contracts" query cheap. +- ABI stored as JSON text; clients parse. +- Soft-link to `contract_code.address` (no hard FK so metadata can be created speculatively). + +**Acceptance** +- [ ] Table + partial index present. + +**Verification** +- `INSERT INTO contract_metadata (address, name) VALUES ('0xabc', 'MyContract')` works. + +--- + +### TASK-316 — Migration 0012: state_snapshots table + +**Section:** db +**Effort:** S +**Depends on:** none +**Type:** migration + +**Goal** +TASK-035 (state snapshot every 10k blocks) and TASK-036 (`/api/mesh/snapshot/:height` fast-sync) need a place to store periodic state checkpoints — full state-root + serialized state diff blob, keyed by height. + +**Files** +- new: `backend/src/database/migrations/0012_state_snapshots.sql` + +**Migration SQL** +```sql +-- up: +CREATE TABLE IF NOT EXISTS state_snapshots ( + height BIGINT PRIMARY KEY, + state_root TEXT NOT NULL, + account_count INTEGER NOT NULL, + storage_count INTEGER NOT NULL, + snapshot_blob BYTEA NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- down: +DROP TABLE IF EXISTS state_snapshots; +``` + +**Implementation sketch** +- BYTEA blob holds gzipped JSON of (accounts + contract_storage) at that height. +- TASK-035 will run as a cron during BlockProducer post-commit when `height % 10000 === 0`. +- TASK-036 will stream the blob over HTTP for peer fast-sync. + +**Acceptance** +- [ ] Table present, height as PK. + +**Verification** +- `INSERT` a 1MB blob, `SELECT octet_length(snapshot_blob) FROM state_snapshots` returns expected size. + +--- + +### TASK-317 — Migration 0013: peers table mirror of peers.json + +**Section:** db +**Effort:** S +**Depends on:** none +**Type:** migration + +**Goal** +The current peer registry persists to `data/peers.json` (file-backed) per [PeerRegistry.ts:60-79](backend/src/network/PeerRegistry.ts#L60-L79). For multi-replica deploys, the JSON file is local-only. Mirror to a `peers` table so all replicas see the same peer set. Keep the JSON as a write-through cache for fast cold-boot. + +**Files** +- new: `backend/src/database/migrations/0013_peers.sql` + +**Migration SQL** +```sql +-- up: +CREATE TABLE IF NOT EXISTS peers ( + peer_id TEXT PRIMARY KEY, + url TEXT NOT NULL, + chain_height BIGINT NOT NULL DEFAULT 0, + public_key TEXT NOT NULL DEFAULT '', + last_seen_ms BIGINT NOT NULL, + first_seen_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_peers_last_seen + ON peers(last_seen_ms DESC); + +-- down: +DROP INDEX IF EXISTS idx_peers_last_seen; +DROP TABLE IF EXISTS peers; +``` + +**Implementation sketch** +- After this migration, [PeerRegistry.registerPeer()](backend/src/network/PeerRegistry.ts) gets a follow-up commit (separate task in section 04) to UPSERT to PG too. +- Eviction loop will `DELETE FROM peers WHERE last_seen_ms < NOW() - 180000`. + +**Acceptance** +- [ ] Table + index present. + +**Verification** +- After commit lands, manual `INSERT` works. + +--- + +### TASK-318 — Migration 0014: rename chat_logs → agent_chat_logs + +**Section:** db +**Effort:** S +**Depends on:** none +**Type:** migration + +**Goal** +The `chat_logs` table from [schema.ts:115-128](backend/src/database/schema.ts#L115-L128) is misnamed — it stores agent conversations, not generic chat. Rename for consistency with `agent_memory`, `agent_completed_tasks`, etc. Use a view to keep old reads working during the rollout window. + +**Files** +- new: `backend/src/database/migrations/0014_rename_chat_logs.sql` + +**Migration SQL** +```sql +-- up: +ALTER TABLE chat_logs RENAME TO agent_chat_logs; +CREATE OR REPLACE VIEW chat_logs AS SELECT * FROM agent_chat_logs; + +-- down: +DROP VIEW IF EXISTS chat_logs; +ALTER TABLE agent_chat_logs RENAME TO chat_logs; +``` + +**Implementation sketch** +- View preserves backwards-compat for any forgotten callsite. Dropping the view is a separate later task. +- Indexes from the original (chat_logs_validator_address, chat_logs_created_at) get auto-renamed by ALTER TABLE … RENAME. + +**Acceptance** +- [ ] `\d agent_chat_logs` shows the table. +- [ ] `\d chat_logs` shows it as a VIEW. +- [ ] `SELECT * FROM chat_logs LIMIT 1` and `SELECT * FROM agent_chat_logs LIMIT 1` both work. + +**Verification** +- Boot succeeds (no callsites broken). +- Audit query `grep -rn "chat_logs" backend/src` returns hits for view-friendly read paths only. + +--- + +### TASK-319 — DB connection-pool tuning + monitoring + +**Section:** db +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +The PG pool from [db.ts:15-21](backend/src/database/db.ts#L15-L21) hardcodes `max: 20`. For Railway production with multiple replicas + worker, that's both under- and over-provisioned for different loads. Make pool size configurable via env, expose pool metrics (`waiting`, `idle`, `total`) for `/api/metrics`. + +**Files** +- edit: `backend/src/database/db.ts:15-21` — read `PG_POOL_MAX` (default 20), `PG_POOL_IDLE_MS` (default 30000), `PG_POOL_CONNECT_MS` (default 2000) from env. +- edit: same file — add `db.poolStats(): { total: number, idle: number, waiting: number }` reading from `pool.totalCount`, `pool.idleCount`, `pool.waitingCount`. + +**Reuses** +- node-postgres `Pool` instance fields (totalCount/idleCount/waitingCount). + +**API contract** +```ts +db.poolStats() → { total: 12, idle: 8, waiting: 0 } +``` + +**Implementation sketch** +- Add 3 env reads at module top. +- Add public method `poolStats` that returns 0/0/0 when no pool (in-memory mode). +- TASK-152 (`/api/metrics`) will export these as Prometheus gauges `hermes_pg_pool_{total,idle,waiting}`. + +**Acceptance** +- [ ] Env override works (`PG_POOL_MAX=50 npm run dev` boots with max=50). +- [ ] `db.poolStats()` returns sensible numbers under load. +- [ ] No breaking change for callers. + +**Verification** +- Hit a load test (TASK-400), watch `db.poolStats().waiting` rise then fall. + +--- + +### TASK-320 — DB query-time histogram metric + +**Section:** db +**Effort:** M +**Depends on:** TASK-319 +**Type:** edit + +**Goal** +We have no visibility into PG query latency. Wrap `db.query()` and `db.exec()` to record per-call duration into a histogram exposed at `/api/metrics`. Bucket boundaries: 1ms, 5ms, 10ms, 50ms, 100ms, 500ms, 1s, 5s. + +**Files** +- new: `backend/src/database/queryMetrics.ts` — exports `recordQuery(durationMs: number)`, `getHistogram(): { buckets: number[], counts: number[], sum: number, count: number }`. +- edit: `backend/src/database/db.ts:81-94` — wrap `query()` and `exec()` to call `recordQuery(Date.now() - start)`. + +**Reuses** +- Prometheus histogram convention (cumulative buckets). + +**API contract** +```ts +recordQuery(12) // ms +getHistogram() +→ { buckets: [1,5,10,50,100,500,1000,5000], counts: [0,0,12,40,8,1,0,0], sum: 23456, count: 61 } +``` + +**Implementation sketch** +- Use a flat array of bucket counts; increment the smallest bucket where `durationMs <= boundary`. +- Cumulative form computed at read time for Prometheus output (TASK-152). +- Sum + count accumulators for mean derivation. + +**Acceptance** +- [ ] Every successful PG query lands in the histogram. +- [ ] Failed queries also recorded (under separate counter `pg_query_errors_total`). + +**Verification** +- After 100 queries, `getHistogram().count === 100`. +- Slow query intentionally injected (`SELECT pg_sleep(2)`) lands in 1s+ bucket. + +--- + +### TASK-321 — DB slow-query log + +**Section:** db +**Effort:** S +**Depends on:** TASK-320 +**Type:** edit + +**Goal** +Queries above a threshold (default 1s) should be logged with the SQL + parameter shape (not values, to avoid leaking PII) for diagnosis. Use the wrapper from TASK-320. + +**Files** +- edit: `backend/src/database/db.ts` — extend the wrapper to `console.warn('[PG SLOW]', { sql, paramCount, durationMs })` when over threshold. + +**Reuses** +- Wrapper added in TASK-320. + +**Implementation sketch** +- Threshold via env `PG_SLOW_QUERY_MS` (default 1000). +- SQL truncated to 200 chars in log. +- Param values NEVER logged — only `params.length`. + +**Acceptance** +- [ ] Slow query logs include `[PG SLOW]` prefix. +- [ ] Param values absent from log line. + +**Verification** +- Inject `SELECT pg_sleep(2)`, observe log. + +--- + +### TASK-322 — Read-replica routing for heavy reads + +**Section:** db +**Effort:** L +**Depends on:** TASK-319 +**Type:** edit + +**Goal** +Single PG instance handles all reads + writes. As traffic grows, heavy reads (block listings, account history) should hit a read replica when one is available, so writes aren't latency-impacted. Add an optional `READ_DATABASE_URL` env; if set, route SELECT-only queries through a second pool. + +**Files** +- edit: `backend/src/database/db.ts` — add `readPool` second Pool when env present. +- edit: same — add `db.queryRead(sql, params)` method that prefers `readPool`, falls back to primary. + +**Reuses** +- Same Pool config conventions from TASK-319. + +**API contract** +```ts +db.queryRead('SELECT * FROM blocks WHERE height = $1', [100]) → {rows, rowCount} +``` + +**Implementation sketch** +- Detect `READ_DATABASE_URL`; if present, instantiate second Pool with same settings. +- `queryRead` tries readPool first, on connection error falls back to primary pool. +- Existing `db.query()` always hits primary (safe default). +- Audit endpoints (TASK-153, TASK-154, TASK-155) will be updated to use `queryRead` in their respective sections. + +**Acceptance** +- [ ] Without env, behavior unchanged. +- [ ] With env, `queryRead` hits replica. +- [ ] Replica unreachable → falls back to primary, logs warning. + +**Verification** +- Local: spin up two PG containers, point `READ_DATABASE_URL` at the second, observe `pg_stat_activity` on each. + +--- + +### TASK-323 — pg_dump backup script to S3 + +**Section:** db +**Effort:** M +**Depends on:** none +**Type:** new-file +**Type:** script + +**Goal** +We have no automated DB backup. Add a script that runs `pg_dump`, gzips, uploads to S3 with date-stamped key. Designed to be invoked from a cron (Railway, GH Action, or local). + +**Files** +- new: `backend/scripts/backup-db.ts` — entry point. +- new: `backend/scripts/README.md` — operator docs (or appended to existing README). + +**Reuses** +- Node `child_process.spawn` for `pg_dump`. +- AWS SDK v3 `@aws-sdk/client-s3` (add to deps if not present). + +**Env** +- `DATABASE_URL` — source. +- `S3_BACKUP_BUCKET` — target bucket. +- `S3_BACKUP_PREFIX` — key prefix (default `hermes-backups/`). +- `AWS_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` — AWS auth. + +**Implementation sketch** +- Spawn `pg_dump --no-owner --no-acl ` → pipe through `gzip` → `PutObjectCommand`. +- Key = `${prefix}${YYYY-MM-DD}/hermes-${HH:MM:SS}.sql.gz`. +- Log size, duration, key on success; non-zero exit on error. +- Optionally enforce retention: list keys older than 30d, delete (behind `--prune` flag). + +**Acceptance** +- [ ] `npm run backup` (script alias added in package.json) uploads a fresh dump. +- [ ] Key format matches spec. +- [ ] Script exits 1 on failure. + +**Verification** +- Run against a local PG + Localstack S3. + +--- + +### TASK-324 — Restore script + smoke test + +**Section:** db +**Effort:** M +**Depends on:** TASK-323 +**Type:** script + +**Goal** +A backup is only useful if restore works. Add a script that downloads the latest (or a specified) backup, restores into a target DB, and runs a smoke query (`SELECT COUNT(*) FROM blocks`, `SELECT COUNT(*) FROM transactions`). + +**Files** +- new: `backend/scripts/restore-db.ts` + +**Reuses** +- `@aws-sdk/client-s3` from TASK-323. +- `psql --dbname=$URL -f -` via spawn. + +**Env** +- `RESTORE_DATABASE_URL` — target (REFUSED if equals `DATABASE_URL` and `--force` not set, to prevent accidental prod restore). +- Other S3 env from TASK-323. + +**Implementation sketch** +- `npm run restore -- --key ` — restore that specific backup. +- `npm run restore -- --latest` — restore most recent. +- After restore, run smoke queries; print row counts. +- Refuse to overwrite a non-empty target unless `--force`. + +**Acceptance** +- [ ] Round-trip works: backup, drop target, restore, counts match. +- [ ] Smoke output printed. +- [ ] Safety check rejects identical source/target. + +**Verification** +- Backup local DB, create empty `hermes_restored`, restore into it, compare row counts. + +--- + +### TASK-325 — CLI: npm run migrate:down NNNN + +**Section:** db +**Effort:** S +**Depends on:** none +**Type:** edit + script + +**Goal** +The migration runner from [migrations.ts:110-154](backend/src/database/migrations.ts#L110-L154) only runs `up`. The `down:` half is parsed and ignored. Add a CLI for operators to roll back a specific migration locally. + +**Files** +- new: `backend/scripts/migrate-down.ts` — parse argv, find matching migration file, run its down half, delete row from `schema_migrations`. +- edit: `backend/package.json:scripts` — add `"migrate:down": "ts-node backend/scripts/migrate-down.ts"`. + +**Reuses** +- Migration loader logic from `loadMigrationsFrom()` in [migrations.ts:42-61](backend/src/database/migrations.ts#L42-L61). Export it (currently file-internal). + +**Implementation sketch** +- Usage: `npm run migrate:down 0007`. +- Refuses to run in production unless `FORCE_PROD_DOWN=1` env set. +- Wraps the down-block in a transaction so partial failure rolls back. +- Removes corresponding row from `schema_migrations`. + +**Acceptance** +- [ ] Running with valid NNNN executes the down block. +- [ ] `schema_migrations` row removed. +- [ ] Re-running `npm run dev` re-applies the up. + +**Verification** +- Apply 0007, run down, verify column dropped, verify row gone, restart, verify column added again. + +--- + +### TASK-326 — Migration dry-run mode + +**Section:** db +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +`applyPendingMigrations()` always writes. Operators can't preview what would change. Add a dry-run mode that prints the SQL each pending migration would execute, then exits without writing. + +**Files** +- edit: `backend/src/database/migrations.ts:110` — add an optional `{ dryRun?: boolean }` arg. +- new: `backend/scripts/migrate-status.ts` — prints applied + pending, with `--dry-run` flag invoking `applyPendingMigrations({ dryRun: true })`. +- edit: `backend/package.json:scripts` — add `"migrate:status": "ts-node backend/scripts/migrate-status.ts"`. + +**Reuses** +- Existing `migrationStatus()` from [migrations.ts:156-167](backend/src/database/migrations.ts#L156-L167). + +**Implementation sketch** +- In dry-run, replace `db.exec(migration.up)` with `console.log('[DRY-RUN]', migration.name, '\n', migration.up)`. +- Skip the INSERT into schema_migrations. +- Lock still acquired/released so behavior matches real run. + +**Acceptance** +- [ ] `npm run migrate:status -- --dry-run` prints SQL without applying. +- [ ] Real boot still applies migrations as normal. + +**Verification** +- Add a stub migration, dry-run, observe SQL printed, observe nothing in `schema_migrations`. + +--- + +### TASK-327 — Schema-diff against prod CLI + +**Section:** db +**Effort:** M +**Depends on:** none +**Type:** script + +**Goal** +Drift between dev schema and prod is invisible. Add a script that connects to two DB URLs (dev + prod), introspects every table + index + column, and prints a diff. Read-only; never writes. + +**Files** +- new: `backend/scripts/schema-diff.ts` +- edit: `backend/package.json:scripts` — add `"schema:diff": "ts-node backend/scripts/schema-diff.ts"`. + +**Reuses** +- `pg` Client (not Pool — short-lived, two distinct URLs). + +**Implementation sketch** +- Args: `npm run schema:diff -- --left $DEV_URL --right $PROD_URL`. +- For each side, query `information_schema.tables`, `.columns`, `pg_indexes`. +- Build a normalized fingerprint per table (sorted columns, sorted indexes). +- Diff fingerprints; print added/removed/changed. +- Exit 1 if differences found (CI-friendly). + +**Acceptance** +- [ ] Reports identical schemas as "no diff". +- [ ] Reports an extra column on one side as a single line `+ accounts.foo TEXT`. + +**Verification** +- Diff against itself → no output, exit 0. +- Apply a migration on one only → diff shows the change. + +--- + +### TASK-328 — Redis cache warmer at boot + +**Section:** db +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Cold boot leaves Redis empty; the first ~30s of traffic causes burst PG reads as the cache populates. Pre-warm with the things `chainState` (line 225 of db.ts) is going to read anyway: latest 100 blocks, top 50 accounts by balance, last block height. + +**Files** +- new: `backend/src/database/cacheWarmer.ts` — exports `warmCache(): Promise`. +- edit: `backend/src/api/server.ts` — invoke after `applyPendingMigrations()` and before route mounting. + +**Reuses** +- `cache.setJSON()` from [db.ts:148-170](backend/src/database/db.ts#L148-L170). +- `chainState` getters from [db.ts:225-280](backend/src/database/db.ts#L225-L280). + +**Implementation sketch** +- Warmer runs once at boot, behind `CACHE_WARMER_ENABLED=true` env (default off in dev, on in production). +- Reads: + - Last 100 blocks → `cache.setJSON('block:height:N', ..., 300)`. + - Top 50 accounts → `cache.setJSON('top_accounts', ..., 60)`. +- All warming work runs in parallel via `Promise.all`. +- Logs total entries warmed + duration. + +**Acceptance** +- [ ] Warmer boots cleanly even with empty PG. +- [ ] Logs count + duration. +- [ ] Doesn't block the server from starting (parallel-with-listen). + +**Verification** +- Boot with empty cache, confirm `cache.get('block:height:1')` returns the block right after boot completes. + +--- + +### TASK-329 — Redis key TTL audit + +**Section:** db +**Effort:** S +**Depends on:** none +**Type:** script + +**Goal** +The `cache` wrapper accepts an optional TTL but many callsites pass none, leaking memory long-term. Add a script that scans every `cache.set/setJSON/hset` callsite in `backend/src/` and reports the ones missing a TTL. + +**Files** +- new: `backend/scripts/audit-redis-ttl.ts` + +**Reuses** +- Plain `fs.readdirSync` recursion + regex matching. + +**Implementation sketch** +- Walk `backend/src/`, read every .ts. +- Match `cache.set(`, `cache.setJSON(`, `cache.hset(` invocations. +- Inspect arg list: third arg present = TTL set; absent = leak risk. +- Print one line per leak: `path:line — cache.set('foo', ...)` (no TTL). +- Exit 1 if any leaks found (CI-friendly). + +**Acceptance** +- [ ] Script runs, prints exactly one line per missing-TTL callsite. +- [ ] Recognizes inline string keys + variable keys. + +**Verification** +- Add a test fixture with one missing-TTL call, run script, see it. + +--- + +### TASK-330 — Redis pub/sub channel for cross-replica events + +**Section:** db +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Today's [EventBus](backend/src/events/EventBus.ts) is in-process. SSE clients on replica A miss events emitted on replica B. Add a Redis pub/sub bridge: emit local → republish to channel; subscribe channel → emit local. Replicas converge on the same event stream. + +**Files** +- new: `backend/src/events/RedisBridge.ts` — exports `attachRedisBridge(eventBus: EventBus, redis: Redis): { detach(): void }`. +- edit: `backend/src/api/server.ts` — invoke after eventBus + redis are constructed. + +**Reuses** +- ioredis duplicated client pattern (one publisher + one subscriber, since subscriber blocks). +- Existing event names from grep over `eventBus.emit(`. + +**Implementation sketch** +- Whitelist of event types that are safe to bridge (block_produced, network_message, ci_results, consensus_quorum) — others stay local. +- Loop-prevention: tag bridged messages with `_origin = REPLICA_ID`; ignore if echoed back. +- Channel name: `hermes:events:v1`. + +**Acceptance** +- [ ] Two-replica test: emit on A, observe on B (within 100ms). +- [ ] No infinite loop. +- [ ] Non-whitelisted events stay local. + +**Verification** +- Local: run two backend instances pointing at same Redis, watch B's logs when A produces a block. + +--- + +### TASK-331 — Two-replica web service: pin SSE to one replica + +**Section:** db +**Effort:** M +**Depends on:** TASK-330 +**Type:** edit + +**Goal** +With two web replicas behind a load balancer, SSE clients can land on either. Without sticky sessions, a reconnect lands on the other replica with no event history. Either: (a) sticky sessions via cookie, or (b) only one replica accepts SSE. Pick (b) since LB config may not be in our hands. + +**Files** +- edit: `backend/src/api/server.ts` — `/api/agent/stream` and other SSE routes return 503 if `process.env.SSE_REPLICA !== 'true'`. + +**Reuses** +- Existing SSE route handlers. + +**Implementation sketch** +- Env `SSE_REPLICA=true` set on exactly one replica via Railway service config. +- 503 response includes header `X-SSE-Failover: true` so clients can retry against a different host (when we add multiple). +- HUD reconnect logic (TASK-245) handles the 503. + +**Acceptance** +- [ ] Default replica returns 503 on `/api/agent/stream`. +- [ ] Replica with env=true serves SSE normally. + +**Verification** +- Two-replica Railway deploy: only the one with env serves the HUD ticker. + +--- + +### TASK-332 — Worker leader election + +**Section:** db +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Two worker replicas would both run the AgentWorker loop simultaneously and produce duplicate commits. Add a leader-election lease via Redis SETNX with TTL. Only the leader runs the agent loop; followers no-op. + +**Files** +- new: `backend/src/agent/leaderElection.ts` — exports `acquireLeadership(redis: Redis, leaseId: string): Promise` and `renewLease(): Promise`. +- edit: `backend/src/worker.ts` — wrap the agent loop start with leadership check; renew every 10s; release on shutdown. + +**Reuses** +- ioredis `set('key', val, 'PX', ttlMs, 'NX')` semantics. + +**Implementation sketch** +- Key: `hermes:worker:leader`. +- Value: this replica's hostname + pid. +- TTL: 30s; renewal every 10s with `EXPIRE` (only if value matches our id, via Lua script for atomicity). +- On lease loss (renew returns false), kill the agent loop, fall back to "follower" state. + +**Acceptance** +- [ ] Single worker: gets leader, agent runs. +- [ ] Two workers: only one gets leader; the other logs `[LEADER] follower mode`. +- [ ] Kill the leader: the follower picks up within ~30s. + +**Verification** +- Local: run worker.ts twice against same Redis, observe. + +--- + +### TASK-333 — Stuck-job recovery + +**Section:** db +**Effort:** M +**Depends on:** TASK-332 +**Type:** edit + +**Goal** +If the agent worker crashes mid-task, the task stays in "in_progress" forever and never gets retried. Add a recovery sweep that, on worker startup (and once every 5 min), finds tasks in `agent_tasks` with `status='in_progress'` and `started_at < NOW() - 1 hour`, resets them to `status='pending'` and increments a `recovery_count`. + +**Files** +- new: `backend/src/database/migrations/0015_agent_tasks_recovery.sql` — `ALTER TABLE agent_tasks ADD COLUMN IF NOT EXISTS recovery_count INTEGER NOT NULL DEFAULT 0;` +- edit: `backend/src/agent/AgentWorker.ts` — add `recoverStuckTasks()` method, call from constructor + interval. + +**Reuses** +- Existing PG access via `db.query`. +- Pattern from existing AgentWorker query callsites. + +**Implementation sketch** +- Recovery query: `UPDATE agent_tasks SET status='pending', recovery_count=recovery_count+1 WHERE status='in_progress' AND started_at < NOW() - INTERVAL '1 hour' RETURNING id`. +- Log each recovered task ID. +- After 5 recoveries on the same task, mark it `status='abandoned'` (separate hard-stop to avoid infinite retry loops). + +**Acceptance** +- [ ] Stuck tasks return to pending. +- [ ] `recovery_count` increments. +- [ ] After 5 recoveries, task marked abandoned and logged. + +**Verification** +- Insert a fake stuck row, run AgentWorker, observe. + +--- + +### TASK-334 — Dead-letter queue for failed agent tasks + +**Section:** db +**Effort:** M +**Depends on:** TASK-333 +**Type:** edit + migration + +**Goal** +Tasks that fail repeatedly get marked `failed` but stay in the same table, cluttering the active queue. Move them to a dead-letter table with the failure reason for triage. + +**Files** +- new: `backend/src/database/migrations/0016_dead_letter_tasks.sql` +- edit: `backend/src/agent/AgentWorker.ts` — when `failure_count >= 3`, move row to `dead_letter_tasks` and delete from `agent_tasks`. + +**Migration SQL** +```sql +-- up: +CREATE TABLE IF NOT EXISTS dead_letter_tasks ( + id TEXT PRIMARY KEY, + original_task_json TEXT NOT NULL, + last_error TEXT, + failure_count INTEGER NOT NULL, + moved_to_dlq_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_dlq_moved_at + ON dead_letter_tasks(moved_to_dlq_at DESC); + +-- down: +DROP INDEX IF EXISTS idx_dlq_moved_at; +DROP TABLE IF EXISTS dead_letter_tasks; +``` + +**Implementation sketch** +- Move = `INSERT INTO dead_letter_tasks (id, original_task_json, last_error, failure_count) SELECT id, row_to_json(t.*)::text, last_error, failure_count FROM agent_tasks t WHERE id = $1; DELETE FROM agent_tasks WHERE id = $1;` in one transaction. + +**Acceptance** +- [ ] Task that fails 3x lands in DLQ. +- [ ] Active queue no longer contains it. + +**Verification** +- Insert a synthetic always-failing task, run agent, watch it move. + +--- + +### TASK-335 — Job retry dashboard + +**Section:** db +**Effort:** M +**Depends on:** TASK-334 +**Type:** new-file + +**Goal** +Surface DLQ + recovery stats in the HUD. Endpoint that returns counts of pending / in_progress / failed / abandoned / dead-lettered, plus the 20 most recent DLQ entries. + +**Files** +- new: `backend/src/api/jobs.ts` — Express router. +- edit: `backend/src/api/server.ts` — mount at `/api/jobs`. + +**Reuses** +- Express router pattern from [backend/src/api/wallet.ts](backend/src/api/wallet.ts). + +**API contract** +``` +GET /api/jobs/stats +→ 200 { + pending: 10, + in_progress: 1, + failed: 2, + abandoned: 0, + dead_lettered: 5 +} + +GET /api/jobs/dlq?limit=20 +→ 200 { items: [ { id, last_error, failure_count, moved_to_dlq_at } ] } + +POST /api/jobs/dlq/:id/retry +→ 200 { restored: true } +→ 404 { error: 'not in dlq' } +``` + +**Implementation sketch** +- `/stats` is one query against `agent_tasks` group-by status + one count from `dead_letter_tasks`. +- `/dlq` reads recent rows. +- `/dlq/:id/retry` moves row back to `agent_tasks` with `status='pending'`, `failure_count=0`. +- Retry endpoint is admin-gated via `requireApiKey('jobs:write')` from [auth.ts](backend/src/api/auth.ts). + +**Acceptance** +- [ ] `/stats` returns counts matching DB ground truth. +- [ ] `/dlq/:id/retry` moves row, returns 200. +- [ ] Auth on retry rejected without admin scope. + +**Verification** +- `curl /api/jobs/stats` against running backend. + +--- + +## Summary + +| TASK | Title | Effort | +|---|---|---| +| 306 | Index transactions(block_height) | S | +| 307 | Index transactions(from_address, nonce) | S | +| 308 | Index accounts(balance DESC) | S | +| 309 | Index receipts(status) | S | +| 310 | GIN index on receipts.logs_json | S | +| 311 | validators.stake column | S | +| 312 | validator_slashes table | S | +| 313 | contract_code table | S | +| 314 | contract_storage table | S | +| 315 | contract_metadata table | S | +| 316 | state_snapshots table | S | +| 317 | peers table | S | +| 318 | Rename chat_logs | S | +| 319 | Pool tuning + monitoring | M | +| 320 | Query-time histogram | M | +| 321 | Slow-query log | S | +| 322 | Read-replica routing | L | +| 323 | pg_dump → S3 | M | +| 324 | Restore script + smoke test | M | +| 325 | migrate:down CLI | S | +| 326 | Migration dry-run mode | S | +| 327 | Schema-diff CLI | M | +| 328 | Redis cache warmer | S | +| 329 | Redis TTL audit script | S | +| 330 | Redis pub/sub bridge | M | +| 331 | SSE replica pinning | M | +| 332 | Worker leader election | M | +| 333 | Stuck-job recovery | M | +| 334 | Dead-letter queue | M | +| 335 | Job retry dashboard | M | + +13 small, 14 medium, 1 large. Net effort ≈ 30 commits over ~2 days at the 60/day cadence. diff --git a/docs/backlog/queue/09-security.md b/docs/backlog/queue/09-security.md new file mode 100644 index 00000000..7ce1e69e --- /dev/null +++ b/docs/backlog/queue/09-security.md @@ -0,0 +1,791 @@ +# Section 09 — Security Specs (TASK-336..370) + +35 tasks. CSRF + Helmet + body-size + rate-limit overrides, API-key scoping/expiry/rotation/audit, admin-token rotation, lockout + suspicious feed, replay protection (chainId + nonce), wallet-export rate limit + obscured display, log redaction, secrets scan + dep audit + CodeQL + Snyk, CSP nonces + SRI, HTTPS-only + cookie flags, mnemonic encryption at rest, KMS stub, Tor/VPN flag, ip2geo, threat-feed, cert-pinning notes, disclosure response template. + +**Preconditions used throughout:** +- Auth: [backend/src/api/auth.ts](backend/src/api/auth.ts) — `requireApiKey`, `ipRateLimit`. +- Crypto helpers in [Crypto.ts](backend/src/blockchain/Crypto.ts). +- Server middleware stack in [server.ts](backend/src/api/server.ts). + +--- + +### TASK-336 — CSRF protection on POST endpoints + +**Section:** security +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +State-changing routes need CSRF protection. Issue per-session token; reject mismatched. + +**Files** +- new: `backend/src/api/middleware/csrf.ts`. +- edit: server.ts — apply to all non-API-key POST routes. + +**Implementation sketch** +- Double-submit cookie pattern: server sets `csrf-token` cookie + expects `X-CSRF-Token` header to match. +- API-key authenticated routes exempt (token-based already). + +**Acceptance** +- [ ] POST without token: 403. +- [ ] With matching token: passes. + +**Verification** +- Curl with/without header. + +--- + +### TASK-337 — Helmet middleware (CSP, HSTS, frameguard) + +**Section:** security +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Standard security headers via `helmet`. + +**Files** +- edit: server.ts — `app.use(helmet({...}))`. +- add dep: `helmet`. + +**Implementation sketch** +- Default config + custom CSP allowing self + Sentry + analytics. +- HSTS max-age 1 year, includeSubDomains. + +**Acceptance** +- [ ] `curl -I /` shows X-Frame-Options, Strict-Transport-Security, Content-Security-Policy. + +**Verification** +- Inspect headers. + +--- + +### TASK-338 — SQL-injection audit pass + +**Section:** security +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +Audit every `db.query` callsite for string concatenation; convert any to parameterized. + +**Files** +- new: `backend/scripts/audit-sql-injection.ts` — grep `db.query` callsites for `+ req.` or template string with `${req.`. +- edit: any flagged callsites. + +**Acceptance** +- [ ] Audit script returns 0 hits. + +**Verification** +- Run script. + +--- + +### TASK-339 — Input length caps on every endpoint + +**Section:** security +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Per-field max length to block oversized inputs. + +**Files** +- new: `backend/src/api/middleware/inputCaps.ts`. + +**Implementation sketch** +- Generic middleware checking `req.body` field by field against schema (use zod or hand-rolled). +- Reject 413 with field name on overflow. + +**Acceptance** +- [ ] 10MB string in any field → 413. + +**Verification** +- Curl with huge field. + +--- + +### TASK-340 — JSON body size limit + +**Section:** security +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Cap total JSON body to 1MB via express.json options. + +**Files** +- edit: server.ts:81 — `express.json({ limit: '1mb' })`. + +**Acceptance** +- [ ] 2MB body: 413. + +**Verification** +- Curl. + +--- + +### TASK-341 — Per-endpoint rate limit overrides + +**Section:** security +**Effort:** S +**Depends on:** TASK-144 +**Type:** edit + +**Goal** +Different routes need different rates. `ipRateLimit(60)` for chat vs `ipRateLimit(600)` for /api/blocks. + +**Files** +- edit: auth.ts — already supports per-call rate; document + apply selectively in server.ts. + +**Acceptance** +- [ ] Chat limited to 60/min; reads to 600/min. + +**Verification** +- Curl loops. + +--- + +### TASK-342 — API-key scope chain:write vs chain:read + +**Section:** security +**Effort:** M +**Depends on:** none +**Type:** edit + +**Goal** +Currently API keys have boolean permissions. Add granular scopes: `chain:read`, `chain:write`, `wallet:send`, `keys:create`, `jobs:write`, `admin`. + +**Files** +- new: migration adding `permissions` JSONB column to `api_keys`. +- edit: `requireApiKey(scope)` middleware to check scope inclusion. + +**Acceptance** +- [ ] Read-scope key: blocked from write routes. + +**Verification** +- Mint key with read-only, attempt write. + +--- + +### TASK-343 — API-key expiry default 90d + +**Section:** security +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +New keys expire 90 days from creation unless overridden. + +**Files** +- edit: `POST /auth/keys` to set `expires_at`. + +**Acceptance** +- [ ] Expired keys: rejected with 401. + +**Verification** +- Backdate, attempt use. + +--- + +### TASK-344 — API-key rotation endpoint + +**Section:** security +**Effort:** S +**Depends on:** TASK-343 +**Type:** new-file + +**Goal** +Rotate a key in place: same permissions, new secret, old gracefully expires in 24h. + +**Files** +- new: `POST /auth/keys/:id/rotate` → returns new secret. + +**Acceptance** +- [ ] Old key still valid for 24h; new key works immediately. + +**Verification** +- Rotate + use both for 24h. + +--- + +### TASK-345 — API-key audit log + +**Section:** security +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Log every key creation, rotation, deletion to a tamper-evident audit table. + +**Files** +- new: migration `api_key_audit(id, key_id, action, actor, metadata, occurred_at)`. +- edit: auth router to write audit row on every key change. + +**Acceptance** +- [ ] All key changes appear in audit log. + +**Verification** +- Mint + delete key, query audit. + +--- + +### TASK-346 — Admin-token rotation flow + +**Section:** security +**Effort:** S +**Depends on:** none +**Type:** docs + +**Goal** +Document procedure for rotating `ADMIN_TOKEN` env without downtime. + +**Files** +- new: `docs/security/admin-token-rotation.md`. + +**Implementation sketch** +- Set `ADMIN_TOKEN_SECONDARY` to new value; both work. +- Restart with new as primary; remove old after deploy. + +**Acceptance** +- [ ] Doc exists with step-by-step. + +**Verification** +- Manual. + +--- + +### TASK-347 — Failed-auth lockout (5 in 1min → 15min block) + +**Section:** security +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Brute-force defense: 5 failures in 1 min from same IP → 15 min block. + +**Files** +- edit: auth.ts. + +**Implementation sketch** +- Redis counter `auth:fail:${ip}` with 60s TTL. +- On count > 5: set `auth:block:${ip}` with 900s TTL; reject with 429. + +**Acceptance** +- [ ] 6th failure within minute: blocked for 15 min. + +**Verification** +- Loop bad creds. + +--- + +### TASK-348 — Suspicious-activity feed + +**Section:** security +**Effort:** S +**Depends on:** TASK-347 +**Type:** new-file + +**Goal** +Stream auth failures, rate-limit hits, blocked IPs to ops channel. + +**Files** +- new: `GET /api/security/suspicious` (admin-gated). + +**API contract** +``` +→ 200 { items: [{ type, ip, reason, ts }] } +``` + +**Acceptance** +- [ ] Returns recent suspicious events. + +**Verification** +- Curl after triggering events. + +--- + +### TASK-349 — Tx replay protection: chainId + +**Section:** security +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Tx must include chainId; reject mismatched. Prevents cross-chain replay. + +**Files** +- edit: TransactionPool.validateTransaction — check `tx.chainId === HERMES_CHAIN_ID`. + +**Acceptance** +- [ ] Wrong chainId: rejected. + +**Verification** +- Submit with wrong id. + +--- + +### TASK-350 — Tx replay protection: per-key nonce window tightening + +**Section:** security +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +Currently nonce window is +10 from current. Tighten to +5; reject txs outside. + +**Files** +- edit: TransactionPool.validateTransaction. + +**Acceptance** +- [ ] Nonce > current+5: rejected. + +**Verification** +- Submit far-future nonce. + +--- + +### TASK-351 — Wallet-export rate limit 1/min + +**Section:** security +**Effort:** S +**Depends on:** TASK-107 +**Type:** edit + +**Goal** +Mnemonic export at most once per minute per address. + +**Files** +- edit: TASK-107 endpoint. + +**Acceptance** +- [ ] Second export within 60s: 429. + +**Verification** +- Two exports in row. + +--- + +### TASK-352 — Mnemonic display obscured + reveal button + +**Section:** security +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +HUD shows mnemonic as `••••••• ••••••• ...`; click-to-reveal. + +**Files** +- edit: relevant frontend component (TASK-231 area). + +**Acceptance** +- [ ] Mnemonic hidden by default in UI. + +**Verification** +- Visual. + +--- + +### TASK-353 — Server log redaction + +**Section:** security +**Effort:** M +**Depends on:** none +**Type:** new-file + +**Goal** +Wrap `console.log/warn/error` to scrub: API keys, signatures, mnemonics, private keys, JWT. + +**Files** +- new: `backend/src/utils/safeLog.ts` — exports `safeLog.{info,warn,error}` that scrubs. + +**Implementation sketch** +- Regex set: `/sk_[a-zA-Z0-9]{32,}/`, `/[A-Za-z0-9+/]{86}=/` (base58 sig length), `/(?:\b\w+\s){11}\w+/` (potential mnemonic). +- Replace matches with `[REDACTED]`. + +**Acceptance** +- [ ] Log line containing API key emerges with `[REDACTED]`. + +**Verification** +- Unit. + +--- + +### TASK-354 — Secrets scan in CI (gitleaks) + +**Section:** security +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Add gitleaks to CI workflow; block PRs that introduce secrets. + +**Files** +- new: `.github/workflows/secrets-scan.yml`. + +**Acceptance** +- [ ] PR with synthetic secret: scan fails. + +**Verification** +- Test PR. + +--- + +### TASK-355 — Dependency audit on PR (npm audit) + +**Section:** security +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +CI step `npm audit --audit-level=high` blocking on high+ vulnerabilities. + +**Files** +- new: `.github/workflows/npm-audit.yml`. + +**Acceptance** +- [ ] PR introducing vulnerable dep: fails. + +**Verification** +- Manual. + +--- + +### TASK-356 — CodeQL workflow on push + +**Section:** security +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +GitHub CodeQL static analysis weekly + on push. + +**Files** +- new: `.github/workflows/codeql.yml`. + +**Acceptance** +- [ ] CodeQL runs on push. + +**Verification** +- Push, view Actions. + +--- + +### TASK-357 — Snyk integration + +**Section:** security +**Effort:** S +**Depends on:** none +**Type:** new-file + +**Goal** +Snyk dependency + container scan on PR. + +**Files** +- new: `.github/workflows/snyk.yml`. + +**Acceptance** +- [ ] Workflow runs. + +**Verification** +- View Actions. + +--- + +### TASK-358 — CSP nonce per request + +**Section:** security +**Effort:** M +**Depends on:** TASK-337 +**Type:** edit + +**Goal** +Inline scripts must carry per-request nonce matching CSP header. + +**Files** +- edit: server.ts middleware to set `res.locals.cspNonce` and pass into HTML responses. +- edit: helmet config to use the nonce. + +**Acceptance** +- [ ] No unsafe-inline in CSP. + +**Verification** +- Inspect headers + source. + +--- + +### TASK-359 — Subresource integrity on CDN + +**Section:** security +**Effort:** S +**Depends on:** none +**Type:** edit + +**Goal** +External script tags include `integrity=` SRI hashes. + +**Files** +- edit: HTML templates. + +**Acceptance** +- [ ] All external `