From 2103649dbb8319ea1c118c1ba64dd5dec26bc657 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 09:35:20 +0100 Subject: [PATCH 001/107] feat: rebuild Concord MVP foundation --- .dockerignore | 19 + .env.example | 73 ++ .gitignore | 67 ++ README.md | 228 ++-- config/.gitkeep | 0 config/coston2/deployed-addresses.json | 517 ++++++++ config/networks/coston2.json | 20 + ...xtension_proxy.coston2.docker.toml.example | 33 + .../extension_proxy.coston2.toml.example | 36 + config/proxy/extension_proxy.docker.toml | 34 + config/proxy/extension_proxy.toml | 37 + contracts/AccordRegistry.sol | 143 +++ contracts/CapitalFacility.sol | 605 ++++++++++ contracts/ConcordInstructionSender.sol | 75 ++ contracts/ConcordTypes.sol | 82 ++ contracts/interfaces/IERC20Minimal.sol | 10 + .../interfaces/ITeeExtensionRegistry.sol | 26 + contracts/interfaces/ITeeMachineRegistry.sol | 10 + contracts/test/ConcordFlow.t.sol | 249 ++++ contracts/test/MockERC20.sol | 47 + docker-compose.coston2.yaml | 19 + docker-compose.yaml | 62 + docker/node-base.Dockerfile | 90 ++ docs/CONCORD_STATUS.md | 63 + docs/README.md | 25 + docs/architecture.md | 70 ++ docs/cloudflared.md | 130 ++ docs/getting-started.md | 75 ++ docs/lineage.md | 25 + docs/testing.md | 52 + foundry.toml | 8 + go/Dockerfile | 98 ++ go/Dockerfile.dockerignore | 37 + go/README.md | 45 + go/cmd/docker/main.go | 61 + go/cmd/main.go | 38 + go/cmd/start-tee/main.go | 151 +++ go/go.mod | 61 + go/go.sum | 263 +++++ go/internal/config/config.go | 41 + go/internal/extension/extension.go | 464 ++++++++ go/internal/extension/extension_test.go | 127 ++ go/internal/extension/utils.go | 5 + go/language.env | 27 + go/pkg/server/server.go | 16 + go/pkg/types/types.go | 64 + proxy/Dockerfile | 46 + script/DeployCoston2.s.sol | 65 + scripts/check-docs.sh | 67 ++ scripts/check-versions.sh | 101 ++ scripts/deploy-coston2.sh | 29 + scripts/extension-post-setup.sh | 60 + scripts/extension-setup.sh | 53 + scripts/generate-bindings.sh | 55 + scripts/lib/language.sh | 69 ++ scripts/lib/versions.sh | 77 ++ scripts/post-build.sh | 164 +++ scripts/pre-build.sh | 151 +++ scripts/resolve-coston2-assets.sh | 57 + scripts/start-services.sh | 446 +++++++ scripts/stop-services.sh | 117 ++ scripts/update-tee-url.sh | 59 + scripts/use-chain.sh | 92 ++ tools/cmd/allow-tee-version/main.go | 89 ++ tools/cmd/audit-deploy/main.go | 212 ++++ tools/cmd/deploy-contract/main.go | 68 ++ tools/cmd/query-tee/main.go | 100 ++ tools/cmd/register-extension/main.go | 55 + tools/cmd/register-tee/main.go | 73 ++ tools/cmd/run-test/main.go | 113 ++ tools/cmd/set-governance/main.go | 101 ++ tools/cmd/start-proxy/main.go | 90 ++ tools/cmd/verify-deploy/main.go | 105 ++ tools/go.mod | 86 ++ tools/go.sum | 324 +++++ tools/pkg/configs/configs.go | 34 + .../ConcordInstructionSender.abi | 141 +++ .../ConcordInstructionSender.bin | 1 + tools/pkg/contracts/concordsender/autogen.go | 452 +++++++ .../contracts/concordsender/concordsender.go | 6 + tools/pkg/fccutils/common.go | 53 + tools/pkg/fccutils/diag.go | 130 ++ tools/pkg/fccutils/ecies.go | 41 + tools/pkg/fccutils/encoding.go | 58 + tools/pkg/fccutils/extension.go | 221 ++++ tools/pkg/fccutils/governance.go | 67 ++ tools/pkg/fccutils/policy_consistency.go | 75 ++ tools/pkg/fccutils/policy_consistency_test.go | 47 + tools/pkg/fccutils/registration.go | 425 +++++++ tools/pkg/fccutils/registration_test.go | 214 ++++ tools/pkg/fccutils/revert.go | 123 ++ tools/pkg/fccutils/revert_test.go | 249 ++++ tools/pkg/fccutils/tee_calls.go | 140 +++ tools/pkg/support/support.go | 331 ++++++ tools/pkg/support/support_test.go | 160 +++ tools/pkg/utils/instructions.go | 121 ++ tools/pkg/validate/checks.go | 1051 +++++++++++++++++ tools/pkg/validate/checks_test.go | 488 ++++++++ tools/pkg/validate/report.go | 211 ++++ tools/pkg/validate/report_test.go | 237 ++++ tools/pkg/validate/validate.go | 93 ++ tools/pkg/validate/validate_test.go | 134 +++ 102 files changed, 12705 insertions(+), 150 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 config/.gitkeep create mode 100644 config/coston2/deployed-addresses.json create mode 100644 config/networks/coston2.json create mode 100644 config/proxy/extension_proxy.coston2.docker.toml.example create mode 100644 config/proxy/extension_proxy.coston2.toml.example create mode 100644 config/proxy/extension_proxy.docker.toml create mode 100644 config/proxy/extension_proxy.toml create mode 100644 contracts/AccordRegistry.sol create mode 100644 contracts/CapitalFacility.sol create mode 100644 contracts/ConcordInstructionSender.sol create mode 100644 contracts/ConcordTypes.sol create mode 100644 contracts/interfaces/IERC20Minimal.sol create mode 100644 contracts/interfaces/ITeeExtensionRegistry.sol create mode 100644 contracts/interfaces/ITeeMachineRegistry.sol create mode 100644 contracts/test/ConcordFlow.t.sol create mode 100644 contracts/test/MockERC20.sol create mode 100644 docker-compose.coston2.yaml create mode 100644 docker-compose.yaml create mode 100644 docker/node-base.Dockerfile create mode 100644 docs/CONCORD_STATUS.md create mode 100644 docs/README.md create mode 100644 docs/architecture.md create mode 100644 docs/cloudflared.md create mode 100644 docs/getting-started.md create mode 100644 docs/lineage.md create mode 100644 docs/testing.md create mode 100644 foundry.toml create mode 100644 go/Dockerfile create mode 100644 go/Dockerfile.dockerignore create mode 100644 go/README.md create mode 100644 go/cmd/docker/main.go create mode 100644 go/cmd/main.go create mode 100644 go/cmd/start-tee/main.go create mode 100644 go/go.mod create mode 100644 go/go.sum create mode 100644 go/internal/config/config.go create mode 100644 go/internal/extension/extension.go create mode 100644 go/internal/extension/extension_test.go create mode 100644 go/internal/extension/utils.go create mode 100644 go/language.env create mode 100644 go/pkg/server/server.go create mode 100644 go/pkg/types/types.go create mode 100644 proxy/Dockerfile create mode 100644 script/DeployCoston2.s.sol create mode 100755 scripts/check-docs.sh create mode 100755 scripts/check-versions.sh create mode 100755 scripts/deploy-coston2.sh create mode 100755 scripts/extension-post-setup.sh create mode 100755 scripts/extension-setup.sh create mode 100755 scripts/generate-bindings.sh create mode 100755 scripts/lib/language.sh create mode 100755 scripts/lib/versions.sh create mode 100755 scripts/post-build.sh create mode 100755 scripts/pre-build.sh create mode 100755 scripts/resolve-coston2-assets.sh create mode 100755 scripts/start-services.sh create mode 100755 scripts/stop-services.sh create mode 100755 scripts/update-tee-url.sh create mode 100755 scripts/use-chain.sh create mode 100644 tools/cmd/allow-tee-version/main.go create mode 100644 tools/cmd/audit-deploy/main.go create mode 100644 tools/cmd/deploy-contract/main.go create mode 100644 tools/cmd/query-tee/main.go create mode 100644 tools/cmd/register-extension/main.go create mode 100644 tools/cmd/register-tee/main.go create mode 100644 tools/cmd/run-test/main.go create mode 100644 tools/cmd/set-governance/main.go create mode 100644 tools/cmd/start-proxy/main.go create mode 100644 tools/cmd/verify-deploy/main.go create mode 100644 tools/go.mod create mode 100644 tools/go.sum create mode 100644 tools/pkg/configs/configs.go create mode 100644 tools/pkg/contracts/concordsender/ConcordInstructionSender.abi create mode 100644 tools/pkg/contracts/concordsender/ConcordInstructionSender.bin create mode 100644 tools/pkg/contracts/concordsender/autogen.go create mode 100644 tools/pkg/contracts/concordsender/concordsender.go create mode 100644 tools/pkg/fccutils/common.go create mode 100644 tools/pkg/fccutils/diag.go create mode 100644 tools/pkg/fccutils/ecies.go create mode 100644 tools/pkg/fccutils/encoding.go create mode 100644 tools/pkg/fccutils/extension.go create mode 100644 tools/pkg/fccutils/governance.go create mode 100644 tools/pkg/fccutils/policy_consistency.go create mode 100644 tools/pkg/fccutils/policy_consistency_test.go create mode 100644 tools/pkg/fccutils/registration.go create mode 100644 tools/pkg/fccutils/registration_test.go create mode 100644 tools/pkg/fccutils/revert.go create mode 100644 tools/pkg/fccutils/revert_test.go create mode 100644 tools/pkg/fccutils/tee_calls.go create mode 100644 tools/pkg/support/support.go create mode 100644 tools/pkg/support/support_test.go create mode 100644 tools/pkg/utils/instructions.go create mode 100644 tools/pkg/validate/checks.go create mode 100644 tools/pkg/validate/checks_test.go create mode 100644 tools/pkg/validate/report.go create mode 100644 tools/pkg/validate/report_test.go create mode 100644 tools/pkg/validate/validate.go create mode 100644 tools/pkg/validate/validate_test.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a322541 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +.git +.gitignore +.env +.env.* +*.md +docs/ +testing/ +.superpowers/ +.claude/ +cache/ +out/ +config/ +contracts/ +scripts/ +proxy/ +foundry.toml +docker-compose*.yaml +Dockerfile* +.dockerignore diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..95ef8a8 --- /dev/null +++ b/.env.example @@ -0,0 +1,73 @@ +# Extension implementation language. +# +# Resolved by directory convention: LANGUAGE= is valid iff /language.env +# exists. The scaffold ships go, python and typescript; adding your own means +# creating a directory with a language.env manifest and a Dockerfile that +# satisfies docs/extension-contract.md — no script changes required. +# +# Reproducibility differs by language: the Go image is bit-for-bit reproducible +# across machines; python/typescript target same-machine determinism only. +# See REPRODUCIBILITY.md. +LANGUAGE=go + +# Private key for proxy signing. Set this only in a local, ignored .env file. +PROXY_PRIVATE_KEY= + +# # Devnet +# INITIAL_OWNER="0x..." # Initial owner address (derived from DEPLOYMENT_PRIVATE_KEY) +# DEPLOYMENT_PRIVATE_KEY="" # Private key for deployments and contract calls + +# Coston +INITIAL_OWNER="" +DEPLOYMENT_PRIVATE_KEY="" + +# TEE governance — the signer set + threshold that governs this extension's TEE +# machines. post-build registers it on-chain (set-governance) AND the TEE node +# signs with it; both read these same vars, so they always agree. If unset, +# defaults to GOVERNANCE_SIGNERS= (the deployer) and +# GOVERNANCE_THRESHOLD=1. Set your own to use a different governance set: +# GOVERNANCE_SIGNERS="0xAbc...,0xDef..." # comma-separated 0x addresses +# GOVERNANCE_THRESHOLD=2 + +# Docker image registry (defaults to "local" for locally-built images) +# Uncomment to use remote registry: +# REGISTRY=registry.gitlab.com/flarenetwork/tee/e2e + +# Chain RPC URL +CHAIN_URL=https://coston2-api.flare.network/ext/C/rpc + +# Path to Coston2 deployed contract addresses +ADDRESSES_FILE=./config/coston2/deployed-addresses.json + +# Must be false on live networks +LOCAL_MODE=false + +# TEE attestation mode: true = use test code hash/platform (no real TEE hardware) +# Set to false when running on actual GCP Confidential VM with real attestation +SIMULATED_TEE=true + +# Normal/FTDC proxy URL on Coston2 +NORMAL_PROXY_URL=https://tee-proxy-coston2-1.flare.rocks + +# Extension proxy URL (local) +# Overwritten automatically when start-services.sh syncs the cloudflared tunnel +# (--tunnel, or any already-running tunnel). Set it by hand for ngrok. +# EXT_PROXY_URL=http://localhost:6674 +EXT_PROXY_URL=https://.ngrok-free.dev + +# Cloudflare tunnel (docs/cloudflared.md) — quick tunnel by default, no account. +# TUNNEL_ARGS=run --token eyJ... # named tunnel = stable hostname +# TUNNEL_TARGET=http://host.docker.internal:6664 # only if the proxy is not on 6674 + +# --- Docker Compose overrides (all have defaults, uncomment to customize) --- +# SOURCE_DATE_EPOCH= # Unix timestamp for reproducible builds (auto-derived from git by start-services.sh) +# CHAIN_ID=31337 # Chain id the node binds signatures to (local=31337, coston=16, coston2=114) +# LOG_LEVEL=INFO # TEE node log level +# MODE=1 # TEE node mode +# REDIS_BIND=127.0.0.1:6382 # Host port binding for the extension Redis +# EXT_PROXY_INTERNAL_BIND=0.0.0.0:6673 # Host port binding for proxy internal API +# EXT_PROXY_EXTERNAL_BIND=0.0.0.0:6674 # Host port binding for proxy external API +# COMPOSE_NETWORK=docker_default # Docker network name (base compose joins this external network) +# USE_LOCAL_SIBLINGS=0 # Set to 1 to build node/proxy from on-disk sibling checkouts +# WAIT_TIMEOUT= # Timeout for post-build registration waits +# TEE_VERSION= # TEE version label for allow-tee-version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4a689b --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +.env* +*.env +!*.example + +# Language manifests must be committed — they are what makes a directory a +# discoverable extension implementation (scripts glob */language.env). The +# broad *.env rule above would otherwise swallow them and a fresh clone would +# find no languages at all. +!language.env +!*/language.env + +# Coston/Coston2 proxy configs (contain DB credentials) +config/proxy/extension_proxy.coston.toml +config/proxy/extension_proxy.coston.docker.toml +config/proxy/extension_proxy.coston2.toml +config/proxy/extension_proxy.coston2.docker.toml +config/extension.env +out/ +cache/ +.DS_Store + +# Uploaded artifacts and local evidence must never enter the source tree. +upload/ +*.log + +# go.work is generated on demand by start-services.sh for USE_LOCAL_SIBLINGS=1 +# (points host builds at sibling tee-node / tee-proxy). Never committed. +go.work +go.work.sum + +# Compiled Go binaries +tools/start-* +tools/allow-* +tools/deploy-* +tools/register-* +tools/run-* +tools/verify-* +tools/query-* +tools/set-* +tools/audit-* +tools/test-* + +# Generated contract bindings (produced by scripts/generate-bindings.sh) +tools/pkg/contracts/*/autogen.go +tools/pkg/contracts/*/*.abi +tools/pkg/contracts/*/*.bin + +# Language implementation build artifacts. +# package-lock.json is deliberately NOT ignored — it is the reproducibility +# anchor for `npm ci` in typescript/Dockerfile. +typescript/node_modules/ +typescript/dist/ +python/__pycache__/ +python/**/__pycache__/ +python/.venv/ +python/.pytest_cache/ +**/*.pyc + +docs/superpowers + +notes/ + +# Private deployment guide (SSH, redeploy instructions) +DEPLOYMENT.md + +config/**/*.log +config/register-tee.state diff --git a/README.md b/README.md index 20feb0e..bee4552 100644 --- a/README.md +++ b/README.md @@ -1,174 +1,102 @@ -# Hello World Extension +# Concord -A working Hello World example for building Flare Confidential Compute (FCC) extensions. This repository demonstrates a complete, runnable extension with on-chain contracts, deployment tooling, and registration scripts — everything you need to understand how extensions work on the Flare TEE infrastructure. +Concord is a confidential programmable relationship system for coordinating +capital between independent parties on Flare. -**The same extension is implemented in Go, Python and TypeScript.** Pick one with `LANGUAGE` in `.env`; everything else — contracts, deployment, registration, tests — is identical regardless of choice. +The MVP is an FXRP-backed syndicated capital facility: -## Choosing a Language - -```bash -LANGUAGE=go # default. Smallest image (~22MB distroless), bit-for-bit reproducible -LANGUAGE=python # ~268MB. Same-machine reproducible -LANGUAGE=typescript # ~472MB. Same-machine reproducible +```text +Root Accord + └─ Makkari session + └─ CoFill allocation + └─ Child Accords + └─ funded commitments + └─ multi-child draw and DrawLegs + └─ repayment and restored capacity ``` -All three implement identical behaviour and are verified against the same golden wire fixtures by `./scripts/test-conformance.sh`. The Go path is the most thoroughly reproducible because it produces a static binary on a distroless base; Python wheels and `node_modules` trees embed build-host variance (see [REPRODUCIBILITY.md](REPRODUCIBILITY.md)). - -Language selection is **convention-based**: `LANGUAGE=` is valid iff `/language.env` exists, so adding a fourth language requires no changes to any script, tool or compose file — you create one directory. - -> **→ [Working in Multiple Languages](docs/languages.md)** covers choosing between them, the same handler written three ways, and a step-by-step for adding your own. The normative spec an implementation must satisfy is [docs/extension-contract.md](docs/extension-contract.md). +The persistent economic relationship is the canonical object. Transactions, +FCC sessions, allocations, child relationships, draws, legs, settlements, and +repayments are explicit nodes in its lineage. -## Repository Structure - -The repo splits into a **language-neutral spine** (contracts, deployment tooling, scripts) and **pluggable language implementations**. You customize one language directory plus the Solidity contract. - -``` -├── go/ # ── Go implementation -│ ├── cmd/main.go # ★ Extension server entry point (standalone, for dev) -│ ├── cmd/docker/main.go # Combined TEE node + extension (single-process image) -│ ├── cmd/start-tee/main.go # Host-process runner for --local mode -│ ├── internal/config/config.go # ★ OPType constants, version, port defaults -│ ├── internal/extension/extension.go # ★ MAIN CUSTOMIZATION POINT: processAction routing -│ ├── internal/extension/utils.go # Boilerplate: actionHandler, buildResult -│ ├── pkg/types/types.go # ★ Request/response types -│ ├── Dockerfile # Single-process image (distroless) -│ └── language.env # Language manifest (marks this dir as an implementation) -├── python/ # ── Python implementation -│ ├── base/ # Framework: server, wire types, encoding, node client -│ ├── app/config.py # ★ OPType constants and version -│ ├── app/handlers.py # ★ MAIN CUSTOMIZATION POINT: your handlers -│ ├── app/abi.py # ★ ABI decoding for non-JSON payloads -│ ├── tests/ # pytest suite -│ ├── Dockerfile # Two-process image (tee-node binary + python) -│ └── language.env -├── typescript/ # ── TypeScript implementation -│ ├── src/base/ # Framework: server, wire types, encoding, node client -│ ├── src/app/config.ts # ★ OPType constants and version -│ ├── src/app/handlers.ts # ★ MAIN CUSTOMIZATION POINT: your handlers -│ ├── src/app/abi.ts # ★ ABI decoding for non-JSON payloads -│ ├── src/__tests__/ # vitest suite -│ ├── Dockerfile # Two-process image (tee-node binary + node) -│ └── language.env -│ -├── contracts/InstructionSender.sol # ★ Your extension's on-chain entry point (shared) -├── docker/node-base.Dockerfile # Shared tee-node builder for non-Go images -├── testdata/conformance/ # Golden wire fixtures, asserted against every language -├── config/ -│ ├── extension.env # Generated by pre-build (gitignored) -│ └── proxy/extension_proxy.toml # Proxy config (Redis, DB, ports, addresses) -├── scripts/ # ── Language-neutral -│ ├── full-setup.sh # Chains all phases: pre-build → compose → post-build → test -│ ├── pre-build.sh # Compile + deploy + register → writes config -│ ├── post-build.sh # Allow TEE version + register TEE on-chain -│ ├── test.sh # On-chain end-to-end test (identical for all languages) -│ ├── test-unit.sh # Unit tests, dispatched via language.env -│ ├── test-conformance.sh # Wire-contract conformance, no chain required -│ ├── check-versions.sh # Fails when dependency pins drift apart -│ ├── build-node-base.sh # Builds the shared tee-node base image -│ ├── generate-bindings.sh # Compile contract → generate Go bindings -│ └── lib/{language,versions}.sh # Language resolution + version derivation -├── tools/ # ── Language-neutral deployment tooling (Go) -│ ├── cmd/deploy-contract/ # Deploys InstructionSender to chain -│ ├── cmd/register-extension/ # Registers extension on TeeExtensionRegistry -│ ├── cmd/allow-tee-version/ # Registers TEE code hash as allowed version -│ ├── cmd/register-tee/ # Registers extension TEE machine on-chain -│ ├── cmd/run-test/ # Sends instructions and verifies results -│ └── pkg/utils/instructions.go # Deploy, SetExtensionId, SendSayHello helpers -├── docker-compose.yaml # Redis + proxy + extension-tee -├── foundry.toml # Foundry config for compiling contracts -└── .env.example # Sample env vars, including LANGUAGE - -★ = Files developers MUST modify for their extension -``` +## Current implementation -`tools/` is deliberately independent of every language implementation, which is what lets one deployment and test path serve all of them. +- Solidity `AccordRegistry` stores relationship identity and parent-child + lineage. +- `CapitalFacility` enforces FXRP collateral, USDT0 funding, selected versus + funded capacity, deterministic multi-child draws, exposure accounting, + repayment, expiry, and close rules. +- `ConcordInstructionSender` submits encrypted `SUBMIT_QUOTE` and + `FINALIZE_ROUND` instructions through the official Flare FCC scaffold. +- The Go extension validates signed provider quotes and runs deterministic + lowest-fee-first CoFill with a partial final allocation. +- The FCC extension requires ECIES-encrypted payloads through the TEE decrypt + endpoint. Local unit tests inject a decrypt function; they do not turn the + production handler into a plaintext path. -## Creating Your Extension +## Truth boundary -The scaffold ships with a working Hello World. To build your extension you modify four things: the operation constants, the handlers, the Solidity contract, and the test assertions. +This repository does not claim private FXRP or USDT0 transfers, private EVM +execution, or production hardware-backed confidentiality. Coston2 FCC +development may use the documented simulated TEE path. Public settlement, +root/child exposure, accepted executable capacity, and lineage remain onchain. -| # | File | What you do | -|---|------|-------------| -| 1 | `` config — `go/internal/config/config.go`, `python/app/config.py`, or `typescript/src/app/config.ts` | Define your OPType and OPCommand constants | -| 2 | `` handlers — `go/internal/extension/extension.go`, `python/app/handlers.py`, or `typescript/src/app/handlers.ts` | Implement your action handlers and state | -| 3 | `contracts/InstructionSender.sol` | Add matching `bytes32` constants and send functions | -| 4 | `tools/cmd/run-test/main.go` | Write test payloads and response assertions | +The allocation verifier is an explicit boundary: an offchain verifier must +validate the FCC result binding, active machine/signature evidence, operation, +round, root, and digest before calling `markAllocationVerified`. The first +checkpoint does not pretend that an EOA is a native FCC proof verifier. -Go additionally has `go/pkg/types/types.go` for request/response structs; Python and TypeScript declare those shapes inline in the handlers. +## Build and test -The key link between your Solidity contract and your handlers is the **OPType** and **OPCommand** pair, which must match exactly across every layer: +The canonical extension runtime is the current Flare FCE scaffold, with Go as +the Concord implementation language. Install Foundry and Go 1.25.1+, then: -``` -Solidity: bytes32 constant OP_TYPE_GREETING = bytes32("GREETING"); - bytes32 constant OP_COMMAND_SAY_HELLO = bytes32("SAY_HELLO"); - -Go: OPTypeGreeting = "GREETING" // internal/config/config.go - OPCommandSayHello = "SAY_HELLO" - -Python: OP_TYPE_GREETING = "GREETING" # app/config.py - OP_COMMAND_SAY_HELLO = "SAY_HELLO" +```bash +PATH=/workspace/tooling/foundry:/workspace/tooling/go/bin:$PATH \ +FOUNDRY_SOLC=/workspace/tooling/solc-0.8.27 \ +forge test -TypeScript: OP_TYPE_GREETING = "GREETING" // src/app/config.ts - OP_COMMAND_SAY_HELLO = "SAY_HELLO" +(cd go && GOTOOLCHAIN=local go test ./...) +(cd tools && GOTOOLCHAIN=local go test ./...) ``` -A mismatch means the action falls through to "unsupported op type" (HTTP 501). - -Every handler follows the same 4-step pattern in all three languages: decode the request, validate it, execute your logic, return a result. - -> ### **→ [Read the Extension Development Guide](docs/extension-guide.md)** for a detailed walkthrough, and **[docs/extension-contract.md](docs/extension-contract.md)** for the normative wire and container contract. - -## Making It Your Own - -This repository works out of the box as a Hello World extension. When you're ready to build your own extension, you'll rename the HelloWorld placeholders to your own names and replace the SAY_HELLO logic with your own operations. - -> ### **→ [Follow the Making It Your Own guide](docs/manual-setup.md)** for step-by-step renaming instructions. -> -> Using [Claude Code](https://claude.ai/code)? Run `/rename-scaffold` to do it automatically. - - -## Run It - -With local infrastructure up (`docker compose up` from `e2e/`): +Generate deployment bindings after changing the instruction sender: ```bash -cp .env.example .env # set DEPLOYMENT_PRIVATE_KEY and CHAIN_ID -LANGUAGE=go ./scripts/full-setup.sh --test # or python, typescript +./scripts/generate-bindings.sh ``` -Prerequisites, configuration, ports and the failure table are in -[docs/getting-started.md](docs/getting-started.md). Coston2 deployment and the platform -traps that cost redeploys are in [docs/deployment-steps.md](docs/deployment-steps.md). - -## Testing - -Three layers, cheapest first — unit (`test-unit.sh`), wire conformance against golden -fixtures with no chain required (`test-conformance.sh`), and on-chain end-to-end -(`test.sh`). Conformance is what guarantees the three implementations stay -byte-identical on the wire, and it is the acceptance test for any new language. See -[docs/testing.md](docs/testing.md). - -## Further Reading +The current Coston2 FCC sequence remains the official scaffold sequence: -**Building your extension** - -- [Extension Development Guide](docs/extension-guide.md) — how the code works and how to add your own logic -- [Working in Multiple Languages](docs/languages.md) — choosing a language, working in each, and **adding your own** -- [Making It Your Own](docs/manual-setup.md) — renaming from HelloWorld to your own extension -- [InstructionSender Contract](docs/instruction-sender.md) — how the on-chain contract works and how to customize it - -**Reference** - -- [Extension Container Contract](docs/extension-contract.md) — the normative wire format and container spec every implementation must satisfy -- [Testing Guide](docs/testing.md) — the test layers, conformance fixtures, and what to run when -- [Reproducibility](REPRODUCIBILITY.md) — what each language's build actually guarantees - -**Per-language** - -- [go/README.md](go/README.md) · [python/README.md](python/README.md) · [typescript/README.md](typescript/README.md) +```text +pre-build → start-services --chain coston2 → post-build → run-test +``` ---- +Concord's facility deployment and full Coston2 evidence are not claimed until +the necessary funded account, public proxy, provider offers, and transaction +receipts are available. See [docs/CONCORD_STATUS.md](docs/CONCORD_STATUS.md). + +## Repository map + +```text +contracts/ + AccordRegistry.sol + CapitalFacility.sol + ConcordInstructionSender.sol + ConcordTypes.sol + interfaces/ + test/ +go/ FCC extension and CoFill +tools/ deployment, FCC verification, and test commands +config/ network and proxy configuration templates +docs/ product, architecture, truth, and runbooks +scripts/ scaffold lifecycle and binding commands +``` -## Built On +## Official Flare sources -Flare Confidential Compute — see the [FCC overview](https://dev.flare.network/fcc/overview) for the underlying primitives (extensions, signing policies, data providers, attestation, Protocol Managed Wallets). +- [Build Your First FCC Extension](https://dev.flare.network/fcc/guides/getting-started) +- [Coston2 network configuration](https://dev.flare.network/network/overview) +- [FXRP address guidance](https://dev.flare.network/fxrp/token-interactions/fxrp-address) +- [Official FCE scaffold](https://github.com/flare-foundation/fce-extension-scaffold) +- [Official Foundry periphery package](https://github.com/flare-foundation/flare-foundry-periphery-package) diff --git a/config/.gitkeep b/config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/config/coston2/deployed-addresses.json b/config/coston2/deployed-addresses.json new file mode 100644 index 0000000..4f53220 --- /dev/null +++ b/config/coston2/deployed-addresses.json @@ -0,0 +1,517 @@ +[ + { + "name": "Submission", + "contractName": "Submission.sol", + "address": "0x2cA6571Daa15ce734Bbd0Bf27D5C9D16787fc33f" + }, + { + "name": "RNat", + "contractName": "RNat.sol", + "address": "0x221D27529e7788B929E13533edc3b00ec1ac5e8A" + }, + { + "name": "RNatAccount", + "contractName": "RNatAccount.sol", + "address": "0x14174a6A12172ad47FFF001C8BEe7aea2Bbf1d9f" + }, + { + "name": "EntityManager", + "contractName": "EntityManager.sol", + "address": "0xE62c5557210a5D095BfC2fDc8B2b5D64609cfDf1" + }, + { + "name": "NodePossessionVerifier", + "contractName": "NodePossessionVerifier.sol", + "address": "0x8DE9e19E8DB504572cF8e5B789CCD68Afe543bB0" + }, + { + "name": "VoterRegistry", + "contractName": "VoterRegistry.sol", + "address": "0x6a0AF07b7972177B176d3D422555cbc98DfDe914" + }, + { + "name": "FlareSystemsCalculator", + "contractName": "FlareSystemsCalculator.sol", + "address": "0x7faEB56E8e98B8507db618631Be65994Ae2065Df" + }, + { + "name": "FlareSystemsManager", + "contractName": "FlareSystemsManager.sol", + "address": "0xA90Db6D10F856799b10ef2A77EBCbF460aC71e52" + }, + { + "name": "RewardManager", + "contractName": "RewardManager.sol", + "address": "0xB4f43E342c5c77e6fe060c0481Fe313Ff2503454" + }, + { + "name": "FtsoRewardManager", + "contractName": "FtsoRewardManagerProxy.sol", + "address": "0x7A0bFB85387314d7F8C0FcCD9D9B74A76115c322" + }, + { + "name": "Relay", + "contractName": "Relay.sol", + "address": "0xa10B672D1c62e5457b17af63d4302add6A99d7dE" + }, + { + "name": "WNatDelegationFee", + "contractName": "WNatDelegationFee.sol", + "address": "0xe97611e7678CFd534532eBC4f6aD7F142C012501" + }, + { + "name": "FtsoInflationConfigurations", + "contractName": "FtsoInflationConfigurations.sol", + "address": "0x14Cf71e1a73e22B1ef9b47d85eC492259D8C3C81" + }, + { + "name": "FtsoRewardOffersManager", + "contractName": "FtsoRewardOffersManager.sol", + "address": "0x1b7ffab226b66b5eCBdC79a42287BC09C05bCb33" + }, + { + "name": "FtsoFeedDecimals", + "contractName": "FtsoFeedDecimals.sol", + "address": "0xdC338222e3Bf815A87F561C6BD42C094a5d171d4" + }, + { + "name": "FtsoFeedPublisher", + "contractName": "FtsoFeedPublisher.sol", + "address": "0x4fEcE851632C2A88519bE4C92eA238bbE71D882D" + }, + { + "name": "FtsoFeedIdConverter", + "contractName": "FtsoFeedIdConverter.sol", + "address": "0x1224AE03e0564c4C2dD3B49B1834F2ac765a529E" + }, + { + "name": "PollingFoundation", + "contractName": "PollingFoundation.sol", + "address": "0x6D7ca85Cb3451b772B87EBB32A9E5cFc500BfA94" + }, + { + "name": "ValidatorRewardOffersManager", + "contractName": "ValidatorRewardOffersManager.sol", + "address": "0xb0B93eb5ef1a15A12F68f824Ab7d21fe5d876983" + }, + { + "name": "FastUpdateIncentiveManager", + "contractName": "FastUpdateIncentiveManager.sol", + "address": "0xC71C1C6E6FB31eF6D948B2C074fA0d38a07D4f68" + }, + { + "name": "FastUpdatesConfiguration", + "contractName": "FastUpdatesConfiguration.sol", + "address": "0x222768c5AbCe56Af9Fd75c5f59614a8B7F5dBe80" + }, + { + "name": "FastUpdater", + "contractName": "FastUpdater.sol", + "address": "0x0B162CA3acf3482d3357972e12d794434085D839" + }, + { + "name": "FeeCalculator", + "contractName": "FeeCalculator.sol", + "address": "0x88A9315f96c9b5518BBeC58dC6a914e13fAb13e2" + }, + { + "name": "FtsoManager", + "contractName": "FtsoManagerProxy.sol", + "address": "0x2675cB89AE83f19cEF8600Eb0E9F2071EDBDf11A" + }, + { + "name": "FtsoC2Flr", + "contractName": "FtsoProxy.sol", + "address": "0xd7351C8bbFD6F508d674C87c75Bc39F2D83e22CB" + }, + { + "name": "FtsoTestXrp", + "contractName": "FtsoProxy.sol", + "address": "0x22d10E7305Fd39833B4d14d113Bca3602bA1F701" + }, + { + "name": "FtsoTestLtc", + "contractName": "FtsoProxy.sol", + "address": "0xE31D989C09ae2a944451b71519B65eaf207EDC4B" + }, + { + "name": "FtsoTestXlm", + "contractName": "FtsoProxy.sol", + "address": "0x6B1244b81b3b6ad289Db61fefFe67DEeF9671750" + }, + { + "name": "FtsoTestDoge", + "contractName": "FtsoProxy.sol", + "address": "0xb7Af7fB7693A922Ba4fa7b04C5bd7d441758b98D" + }, + { + "name": "FtsoTestAda", + "contractName": "FtsoProxy.sol", + "address": "0x9414b2aE2E252D4f90d6e141196Eb9505f134Af1" + }, + { + "name": "FtsoTestAlgo", + "contractName": "FtsoProxy.sol", + "address": "0xc49B4F03cbf206F473Fe130Ff1cAb23Cdccd9771" + }, + { + "name": "FtsoTestBtc", + "contractName": "FtsoProxy.sol", + "address": "0xc6757dF6e0f4Cd997Be8B41aF0E58d5d45e36111" + }, + { + "name": "FtsoTestEth", + "contractName": "FtsoProxy.sol", + "address": "0xA08C5aa9E2e1F4F55f68f89DC9130b533AD2a673" + }, + { + "name": "FtsoTestFil", + "contractName": "FtsoProxy.sol", + "address": "0xACAa2031cb540d0a4aFfba94DBd773a6cA3A0440" + }, + { + "name": "FtsoTestArb", + "contractName": "FtsoProxy.sol", + "address": "0x75EfFD69D9e2f79183c9389598E98F3d2885a535" + }, + { + "name": "FtsoTestAvax", + "contractName": "FtsoProxy.sol", + "address": "0x8E38dB4C429a67a5f6e05D480da83f2Ba31365Fb" + }, + { + "name": "FtsoTestBnb", + "contractName": "FtsoProxy.sol", + "address": "0xa3e66Cf49bF299984dD57290819445C33eB40A5a" + }, + { + "name": "FtsoTestMatic", + "contractName": "FtsoProxy.sol", + "address": "0x964cf992F8d563154EdAb2A4Db1530279DFDA9aF" + }, + { + "name": "FtsoTestPol", + "contractName": "FtsoProxy.sol", + "address": "0x8a37960CFEec34EB436bf15Ec5DE2F24b412ef4E" + }, + { + "name": "FtsoTestSol", + "contractName": "FtsoProxy.sol", + "address": "0xBC8d04BF260306cAc7c952081ce39a2a3166a030" + }, + { + "name": "FtsoTestUsdc", + "contractName": "FtsoProxy.sol", + "address": "0x250d28B44287151D2aDa269e715E2b8B639448A3" + }, + { + "name": "FtsoTestUsdt", + "contractName": "FtsoProxy.sol", + "address": "0xf32bE3d2bF466916e9a8d9EAA7Ad0179da18268c" + }, + { + "name": "FtsoTestXdc", + "contractName": "FtsoProxy.sol", + "address": "0x481931Feb34Ef3CE97BB4Fef9b74855b4c8Fd7aA" + }, + { + "name": "FtsoV2", + "contractName": "FtsoV2Proxy.sol", + "address": "0xC4e9c78EA53db782E28f28Fdf80BaF59336B304d" + }, + { + "name": "PriceSubmitter", + "contractName": "PriceSubmitterProxy.sol", + "address": "0x9179aAB4C8b6Ac1402617eFa0fD130AbFa42c6e4" + }, + { + "name": "VoterWhitelister", + "contractName": "VoterWhitelisterProxy.sol", + "address": "0xbF4FaE78d3e5aF75A091f01a5405a1B82d740270" + }, + { + "name": "FdcHub", + "contractName": "FdcHub.sol", + "address": "0x48aC463d7975828989331F4De43341627b9c5f1D" + }, + { + "name": "FdcInflationConfigurations", + "contractName": "FdcInflationConfigurations.sol", + "address": "0x5C670a6950111D6f38B0D7cAdEB58D534fd9D209" + }, + { + "name": "FdcRequestFeeConfigurations", + "contractName": "FdcRequestFeeConfigurations.sol", + "address": "0x191a1282Ac700edE65c5B0AaF313BAcC3eA7fC7e" + }, + { + "name": "FdcVerification", + "contractName": "FdcVerificationProxy.sol", + "address": "0x906507E0B64bcD494Db73bd0459d1C667e14B933" + }, + { + "name": "VoterPreRegistry", + "contractName": "VoterPreRegistry.sol", + "address": "0xee963D1867d8B5f3D3E304802B40BC55B3eDeCB8" + }, + { + "name": "PollingManagementGroup", + "contractName": "PollingManagementGroup.sol", + "address": "0x056A8AcdCd2B5D3bF7a4F1d218B8A1660BB4D912" + }, + { + "name": "FtsoV2Implementation", + "contractName": "FtsoV2.sol", + "address": "0xA9E744A9dfA9177196816C9B92791065655c74aB" + }, + { + "name": "USDTSwapper", + "contractName": "USDTSwapper.sol", + "address": "0x0B70c0C7A242d4f81D85e9C8384C8AB1593851Cb" + }, + { + "name": "FdcVerificationImplementation", + "contractName": "FdcVerification.sol", + "address": "0x6E33205293aE1C6dcC91249951A5A67C863918A7" + }, + { + "name": "ChainlinkAdapterImplementation", + "contractName": "ChainlinkAdapter.sol", + "address": "0xAB8A8ABE1B73dE9a8c67D4591961E0a3be5274c7" + }, + { + "name": "ChainlinkAdapterFlr", + "contractName": "ChainlinkAdapterProxy.sol", + "address": "0x230052d081dFdF5C72BdAE1cB262B6F79563DDC6" + }, + { + "name": "ChainlinkAdapterXrp", + "contractName": "ChainlinkAdapterProxy.sol", + "address": "0x9b4f882C5a43bd98F7f6FAC7F24E5fBb697dB0fB" + }, + { + "name": "ChainlinkAdapterUsdt", + "contractName": "ChainlinkAdapterProxy.sol", + "address": "0xdf020706adB01E393A9026150FBFB810cDda789A" + }, + { + "name": "ChainlinkAdapterUsdc", + "contractName": "ChainlinkAdapterProxy.sol", + "address": "0x2fa3D57fa4ED7cD44443166F240D8223D42cB38B" + }, + { + "name": "DiamondGovernanceFacet", + "contractName": "DiamondGovernanceFacet.sol", + "address": "0x58e8C512678482b7eF8D97DBcF90238FC7D16645" + }, + { + "name": "DiamondLoupeFacet", + "contractName": "DiamondLoupeFacet.sol", + "address": "0x1aFDba98059527E1af069490fa009A390eE6303e" + }, + { + "name": "ExtensionManagerFacet", + "contractName": "ExtensionManagerFacet.sol", + "address": "0x13ebf34c3Fd436A657cb0f819c59790dF55CE14B" + }, + { + "name": "InstructionsFacet", + "contractName": "InstructionsFacet.sol", + "address": "0xe0958De99d4C9Fcb960AEd936Ba5964506AA62Ff" + }, + { + "name": "MachineManagerFacet", + "contractName": "MachineManagerFacet.sol", + "address": "0xF40B9a2e70EE96042217F10D94A4B1eDf13096a8" + }, + { + "name": "VerificationFacet", + "contractName": "VerificationFacet.sol", + "address": "0x78203332236cF39A0079746385F33060aCC95778" + }, + { + "name": "OperationFeesFacet", + "contractName": "OperationFeesFacet.sol", + "address": "0x7747a3012aFCA610Ec519eB8DCFD51dFC154a3dF" + }, + { + "name": "OwnerAllowlistFacet", + "contractName": "OwnerAllowlistFacet.sol", + "address": "0xDD692398C91c077798aEE6aC5c0e404FbD15c8C9" + }, + { + "name": "WalletManagerFacet", + "contractName": "WalletManagerFacet.sol", + "address": "0xcbf21163bC2A47E8a0FF69cC006C94684bC8Dc9b" + }, + { + "name": "WalletKeyManagerFacet", + "contractName": "WalletKeyManagerFacet.sol", + "address": "0x9Aeb4C3959Ba15464241F7b8daf38Ac2Fa1Cca13" + }, + { + "name": "WalletProjectManagerFacet", + "contractName": "WalletProjectManagerFacet.sol", + "address": "0x9B2767BEaFf0d48147390fA15f001162FdcB33e1" + }, + { + "name": "WalletBackupManagerFacet", + "contractName": "WalletBackupManagerFacet.sol", + "address": "0x9391Caba91a99590C87F421A0C3Fb5E92d36DCb4" + }, + { + "name": "VrfFacet", + "contractName": "VrfFacet.sol", + "address": "0x90c2210E899db9f27Eb713aF5acF97226DD7cEa1" + }, + { + "name": "ExternalAddressesFacet", + "contractName": "ExternalAddressesFacet.sol", + "address": "0xF515D79df67c549C379E64Ec4Ff8f151906ADB0A" + }, + { + "name": "ExtensionGovernanceFacet", + "contractName": "ExtensionGovernanceFacet.sol", + "address": "0x8756eF3d5e491c101DC703bf1AeaB1123988dD1B" + }, + { + "name": "MachinePathManagerFacet", + "contractName": "MachinePathManagerFacet.sol", + "address": "0x2Fc4E122BC234f56a0D05aa24d8CE7c28cD25141" + }, + { + "name": "WalletProjectPauseFacet", + "contractName": "WalletProjectPauseFacet.sol", + "address": "0xc2333fa57d96BeFceB1A45e53C52C533d28eFc74" + }, + { + "name": "MachineEmergencyPauseFacet", + "contractName": "MachineEmergencyPauseFacet.sol", + "address": "0x8F6FBdcC73819CD0a2E71A3E1D40bE036D335E3d" + }, + { + "name": "FlareTeeManagerInit", + "contractName": "FlareTeeManagerInit.sol", + "address": "0x8025F92764a9e5936A02570c0534F7B3dbFBDf20" + }, + { + "name": "FlareTeeManager", + "contractName": "FlareTeeManager.sol", + "address": "0x1a9C4A0f9D76c0b1D91d22E24E573a9b377618aE" + }, + { + "name": "Fdc2HubImplementation", + "contractName": "Fdc2Hub.sol", + "address": "0xF0338A1b9f8902000c562F34f43B4400FCc77A8f" + }, + { + "name": "Fdc2Hub", + "contractName": "Fdc2HubProxy.sol", + "address": "0x04dd3Ba33aC798d400bEc42A26F82f9812A421dc" + }, + { + "name": "Fdc2RequestFeeConfigurationsImplementation", + "contractName": "Fdc2RequestFeeConfigurations.sol", + "address": "0x85884d027d270Ed66801bDe56FBC3F2195A147ca" + }, + { + "name": "Fdc2RequestFeeConfigurations", + "contractName": "Fdc2RequestFeeConfigurationsProxy.sol", + "address": "0xbC135843998E0669a603e89599E9D184a713cac7" + }, + { + "name": "Fdc2VerificationImplementation", + "contractName": "Fdc2Verification.sol", + "address": "0x49484b974b3bBbF4e22cb5f5f3891EfacF5b1B2F" + }, + { + "name": "Fdc2Verification", + "contractName": "Fdc2VerificationProxy.sol", + "address": "0xA34Ff9be42b2C7782786270a51d33b1baC0462Cd" + }, + { + "name": "TeePaymentsRegistryImplementation", + "contractName": "TeePaymentsRegistry.sol", + "address": "0xD205AcEe3a785A4C59f090f71D909DB1F6CAeFf9" + }, + { + "name": "TeePaymentsRegistry", + "contractName": "TeePaymentsRegistryProxy.sol", + "address": "0x1A78D02DD19400138f4F3A817E95fC715B6c34Fe" + }, + { + "name": "TeePaymentsConfigVerifierImplementation", + "contractName": "TeePaymentsConfigVerifier.sol", + "address": "0x4ea574f16c87a9474A8Ae8ac65516b5f167B071a" + }, + { + "name": "TeePaymentsConfigVerifier", + "contractName": "TeePaymentsConfigVerifierProxy.sol", + "address": "0xC2973CEAD7c193e3e915Eeb6b4DA672660d84F7D" + }, + { + "name": "AddressValidatorImplementation", + "contractName": "AddressValidator.sol", + "address": "0x4860B2bf6237bF9a1779C60E9Bfb263330930661" + }, + { + "name": "AddressValidator", + "contractName": "AddressValidatorProxy.sol", + "address": "0x2dB76e4E0162400747561bD72dE52442897f6dA2" + }, + { + "name": "TeePaymentsImplementation", + "contractName": "TeePayments.sol", + "address": "0xA8b8E5174C2a389ba115cE8C4Ac508374aEBC957" + }, + { + "name": "TeePayments", + "contractName": "TeePaymentsProxy.sol", + "address": "0x86CB3e2dA1a4Ec30e2Ff2E5bA1A2D5ff9C9D44f4" + }, + { + "name": "TeePaymentsFeeScheduleManagerImplementation", + "contractName": "TeePaymentsFeeScheduleManager.sol", + "address": "0x4df266f37FBdcB6a1FCEd842e284f73B527D4cf2" + }, + { + "name": "TeePaymentsFeeScheduleManager", + "contractName": "TeePaymentsFeeScheduleManagerProxy.sol", + "address": "0x8640E30B0B165FA3F91497fE4AA01e003905681A" + }, + { + "name": "TeeRewardOffersManagerImplementation", + "contractName": "TeeRewardOffersManager.sol", + "address": "0x2ae91Ff299DF011e1056F2B1BF56C8C53e2D723F" + }, + { + "name": "TeeRewardOffersManager", + "contractName": "TeeRewardOffersManagerProxy.sol", + "address": "0x512f156Ff1a3F1E0D8fa8BA112Ff5a35637dbC9A" + }, + { + "name": "Fdc2InflationConfigurationsImplementation", + "contractName": "Fdc2InflationConfigurations.sol", + "address": "0x5bCC1274C3EE4Feae6359f59c9C3965D2c701032" + }, + { + "name": "Fdc2InflationConfigurations", + "contractName": "Fdc2InflationConfigurationsProxy.sol", + "address": "0x54f53294E21C729b98aa095E01BDb2770DB7d96d" + }, + { + "name": "Fdc2RewardOffersManagerImplementation", + "contractName": "Fdc2RewardOffersManager.sol", + "address": "0x86470A859EC5F6d91c426488Fc693Ce2C5f09CB1" + }, + { + "name": "Fdc2RewardOffersManager", + "contractName": "Fdc2RewardOffersManagerProxy.sol", + "address": "0xfa5DE5C1547297F79A974646DA80a625D6FaaD48" + }, + { + "name": "VrfVerifier", + "contractName": "VrfVerifier.sol", + "address": "0x7214D4c13aaFE0e26F5EC5Fc625a6b60AfF99533" + } +] \ No newline at end of file diff --git a/config/networks/coston2.json b/config/networks/coston2.json new file mode 100644 index 0000000..86d0ae4 --- /dev/null +++ b/config/networks/coston2.json @@ -0,0 +1,20 @@ +{ + "network": "coston2", + "chainId": 114, + "rpcUrl": "https://coston2-api.flare.network/ext/C/rpc", + "explorerUrl": "https://coston2-explorer.flare.network", + "contractRegistry": "0xaD67FE66660Fb8dFE9d6b1b4240d8650e30F6019", + "fxrp": { + "assetManagerRegistryName": "AssetManagerFXRP", + "assetManager": "0xc1Ca88b937d0b528842F95d5731ffB586f4fbDFA", + "token": "0x0b6A3645c240605887a5532109323A3E12273dc7", + "decimals": 6, + "resolution": "Resolve AssetManagerFXRP from ContractRegistry, then call fAsset()." + }, + "usdt0": { + "symbol": "USDT0", + "token": "0x479854495cefBc8D12B971A3Ec4d18E6dbcE81a3", + "decimals": 18, + "resolution": "Current Coston2 token snapshot; validate symbol, decimals, and bytecode with the resolver." + } +} diff --git a/config/proxy/extension_proxy.coston2.docker.toml.example b/config/proxy/extension_proxy.coston2.docker.toml.example new file mode 100644 index 0000000..817b81a --- /dev/null +++ b/config/proxy/extension_proxy.coston2.docker.toml.example @@ -0,0 +1,33 @@ +# Docker-specific Coston2 proxy config. Service names instead of localhost. +redis_port = "redis:6379" +private_key_variable = "PROXY_PRIVATE_KEY" +initial_signing_policy_offset = 2 +signing_policy_fetch_interval = "20s" + +chain_id = 114 + +[db] +host = "" +port = 3306 +database = "" +username = "" +password = "" +log_queries = false + +[addresses] +flare_systems_manager = "0xA90Db6D10F856799b10ef2A77EBCbF460aC71e52" +relay = "0xa10B672D1c62e5457b17af63d4302add6A99d7dE" +voter_registry = "0x6a0AF07b7972177B176d3D422555cbc98DfDe914" + + +[ports] +internal = "6663" +external = "6664" + +[info_timing] +cycle_internal = "10s" +cycle_queue_response_wait = "2s" + +[voting] +proposal_expiration = "12s" +max_pending_request = 10000 diff --git a/config/proxy/extension_proxy.coston2.toml.example b/config/proxy/extension_proxy.coston2.toml.example new file mode 100644 index 0000000..c68f126 --- /dev/null +++ b/config/proxy/extension_proxy.coston2.toml.example @@ -0,0 +1,36 @@ +redis_port = ":6380" + +private_key_variable = "" +initial_signing_policy_offset = 2 +signing_policy_fetch_interval = "20s" + +chain_id = 114 + +[db] +host = "" +port = 3306 +database = "" +username = "" +password = "" +log_queries = false + +[addresses] +flare_systems_manager = "0xA90Db6D10F856799b10ef2A77EBCbF460aC71e52" +relay = "0xa10B672D1c62e5457b17af63d4302add6A99d7dE" +voter_registry = "0x6a0AF07b7972177B176d3D422555cbc98DfDe914" + + + +[ports] +internal = "6663" +external = "6664" + + +[info_timing] +cycle_internal = "10s" +cycle_queue_response_wait ="2s" + + +[voting] +proposal_expiration = "12s" +max_pending_request = 10000 diff --git a/config/proxy/extension_proxy.docker.toml b/config/proxy/extension_proxy.docker.toml new file mode 100644 index 0000000..c990c0b --- /dev/null +++ b/config/proxy/extension_proxy.docker.toml @@ -0,0 +1,34 @@ +# Docker-specific proxy config. +# ext-proxy runs in a separate compose from the infrastructure. It reaches +# infrastructure services (indexer-db, blockchain node) via host.docker.internal. +redis_port = "redis:6379" +private_key_variable = "PROXY_PRIVATE_KEY" +initial_signing_policy_offset = 0 +signing_policy_fetch_interval = "20s" + +chain_id = 31337 + +[db] +host = "host.docker.internal" +port = 3306 +database = "db" +username = "root" +password = "root" +log_queries = false + +[addresses] +flare_systems_manager = "0xa4bcDF64Cdd5451b6ac3743B414124A6299B65FF" +relay = "0x5A0773Ff307Bf7C71a832dBB5312237fD3437f9F" +voter_registry = "0xB00cC45B4a7d3e1FEE684cFc4417998A1c183e6d" + +[ports] +internal = "6663" +external = "6664" + +[info_timing] +cycle_internal = "10s" +cycle_queue_response_wait = "2s" + +[voting] +proposal_expiration = "12s" +max_pending_request = 10000 diff --git a/config/proxy/extension_proxy.toml b/config/proxy/extension_proxy.toml new file mode 100644 index 0000000..cdc22bd --- /dev/null +++ b/config/proxy/extension_proxy.toml @@ -0,0 +1,37 @@ +redis_port = ":6382" + +private_key_variable = "PROXY_PRIVATE_KEY" +initial_signing_policy_offset = 0 +signing_policy_fetch_interval = "20s" + +chain_id = 114 + + +[db] +host = "localhost" +port = 3306 +database = "db" +username = "root" +password = "root" +log_queries = false + + +[addresses] +flare_systems_manager = "0xa4bcDF64Cdd5451b6ac3743B414124A6299B65FF" +relay = "0x5A0773Ff307Bf7C71a832dBB5312237fD3437f9F" +voter_registry = "0xB00cC45B4a7d3e1FEE684cFc4417998A1c183e6d" + + +[ports] +internal = "6663" +external = "6664" + + +[info_timing] +cycle_internal = "10s" +cycle_queue_response_wait ="2s" + + +[voting] +proposal_expiration = "12s" +max_pending_request = 10000 diff --git a/contracts/AccordRegistry.sol b/contracts/AccordRegistry.sol new file mode 100644 index 0000000..021b2f4 --- /dev/null +++ b/contracts/AccordRegistry.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +/// @title AccordRegistry +/// @notice Stores Concord relationship identity and explicit parent-child +/// lineage. Economic state remains in CapitalFacility; this registry answers +/// which relationship authorized each derived action. +contract AccordRegistry { + enum NodeKind { + ROOT_ACCORD, + MAKKARI_SESSION, + COFILL_ALLOCATION, + CHILD_ACCORD, + DRAW, + DRAW_LEG, + SETTLEMENT, + REPAYMENT + } + + struct LineageNode { + bytes32 id; + bytes32 parentId; + NodeKind kind; + uint64 createdAt; + bool exists; + } + + struct SettlementRecord { + bytes32 id; + bytes32 parentId; + address asset; + address from; + address to; + uint256 principal; + uint64 createdAt; + } + + struct RepaymentRecord { + bytes32 id; + bytes32 drawId; + uint256 principal; + uint64 createdAt; + } + + address public immutable admin; + address public facility; + + mapping(bytes32 => LineageNode) public nodes; + mapping(bytes32 => bytes32[]) private childrenByParent; + mapping(bytes32 => SettlementRecord) public settlements; + mapping(bytes32 => RepaymentRecord) public repayments; + + event FacilitySet(address indexed facility); + event LineageRegistered(bytes32 indexed id, bytes32 indexed parentId, NodeKind kind); + event SettlementRegistered( + bytes32 indexed id, + bytes32 indexed parentId, + address indexed asset, + address from, + address to, + uint256 principal + ); + event RepaymentRegistered(bytes32 indexed id, bytes32 indexed drawId, uint256 principal); + + error Unauthorized(); + error AlreadyInitialized(); + error InvalidLineage(); + error DuplicateNode(); + + constructor(address admin_) { + require(admin_ != address(0), "admin is zero"); + admin = admin_; + } + + modifier onlyFacility() { + if (msg.sender != facility) revert Unauthorized(); + _; + } + + function setFacility(address facility_) external { + if (msg.sender != admin) revert Unauthorized(); + if (facility != address(0)) revert AlreadyInitialized(); + require(facility_ != address(0), "facility is zero"); + facility = facility_; + emit FacilitySet(facility_); + } + + function registerNode(bytes32 id, bytes32 parentId, NodeKind kind) public onlyFacility { + if (id == bytes32(0)) revert InvalidLineage(); + if (nodes[id].exists) revert DuplicateNode(); + if (kind != NodeKind.ROOT_ACCORD && !nodes[parentId].exists) revert InvalidLineage(); + + nodes[id] = LineageNode({ + id: id, + parentId: parentId, + kind: kind, + createdAt: uint64(block.timestamp), + exists: true + }); + childrenByParent[parentId].push(id); + emit LineageRegistered(id, parentId, kind); + } + + function registerSettlement( + bytes32 id, + bytes32 parentId, + address asset, + address from, + address to, + uint256 principal + ) external onlyFacility { + registerNode(id, parentId, NodeKind.SETTLEMENT); + settlements[id] = SettlementRecord({ + id: id, + parentId: parentId, + asset: asset, + from: from, + to: to, + principal: principal, + createdAt: uint64(block.timestamp) + }); + emit SettlementRegistered(id, parentId, asset, from, to, principal); + } + + function registerRepayment(bytes32 id, bytes32 drawId, uint256 principal) external onlyFacility { + registerNode(id, drawId, NodeKind.REPAYMENT); + repayments[id] = RepaymentRecord({ + id: id, + drawId: drawId, + principal: principal, + createdAt: uint64(block.timestamp) + }); + emit RepaymentRegistered(id, drawId, principal); + } + + function getChildren(bytes32 parentId) external view returns (bytes32[] memory) { + return childrenByParent[parentId]; + } + + function hasNode(bytes32 id) external view returns (bool) { + return nodes[id].exists; + } +} diff --git a/contracts/CapitalFacility.sol b/contracts/CapitalFacility.sol new file mode 100644 index 0000000..decdb4b --- /dev/null +++ b/contracts/CapitalFacility.sol @@ -0,0 +1,605 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {AccordRegistry} from "./AccordRegistry.sol"; +import {ConcordTypes} from "./ConcordTypes.sol"; +import {IERC20Minimal} from "./interfaces/IERC20Minimal.sol"; + +/// @title CapitalFacility +/// @notice The economic lifecycle for one FXRP-backed, USDT0-liquidity +/// syndicated facility. Makkari/CoFill decide an allocation; this contract +/// owns funding, draws, exposure, settlement, and repayment. +contract CapitalFacility { + uint256 private constant BPS = 10_000; + + address public immutable owner; + address public immutable allocationVerifier; + bytes32 public immutable extensionId; + AccordRegistry public immutable accordRegistry; + address public immutable collateralAsset; + address public immutable liquidityAsset; + + struct MakkariRound { + bytes32 id; + bytes32 rootAccordId; + uint256 targetCapacity; + uint32 maxFeeBps; + uint64 roundExpiry; + ConcordTypes.RoundStatus status; + } + + struct Draw { + bytes32 id; + bytes32 rootAccordId; + uint256 principal; + uint256 repaidPrincipal; + uint64 createdAt; + bool exists; + } + + struct DrawLeg { + bytes32 id; + bytes32 drawId; + bytes32 childAccordId; + uint256 principal; + uint256 repaidPrincipal; + } + + mapping(bytes32 => ConcordTypes.RootAccord) public rootAccords; + mapping(bytes32 => ConcordTypes.ChildAccord) public childAccords; + mapping(bytes32 => MakkariRound) public rounds; + mapping(bytes32 => Draw) public draws; + mapping(bytes32 => DrawLeg) public drawLegs; + mapping(bytes32 => bytes32[]) private childrenByRoot; + mapping(bytes32 => bytes32[]) private legsByDraw; + mapping(bytes32 => address[]) private eligibleProvidersByRound; + mapping(bytes32 => mapping(address => bool)) public eligibleProvider; + mapping(bytes32 => bool) public verifiedAllocationDigest; + mapping(bytes32 => bool) public consumedAllocationDigest; + + uint256 private repaymentNonce; + + uint256 private _entered; + + event RootAccordCreated( + bytes32 indexed rootId, + address indexed borrower, + uint256 targetCapacity, + uint64 validUntil, + bytes32 policyHash + ); + event CollateralLocked(bytes32 indexed rootId, address indexed borrower, uint256 amount); + event SyndicationOpened( + bytes32 indexed rootId, + bytes32 indexed roundId, + uint256 targetCapacity, + uint32 maxFeeBps, + uint64 roundExpiry + ); + event AllocationVerified(bytes32 indexed resultDigest); + event ChildAccordSelected( + bytes32 indexed childId, + bytes32 indexed rootId, + address indexed provider, + uint256 selectedCapacity, + uint32 feeBps + ); + event ChildAccordFunded(bytes32 indexed childId, address indexed provider, uint256 amount); + event RootActivated(bytes32 indexed rootId, uint256 committedCapacity); + event DrawCreated(bytes32 indexed drawId, bytes32 indexed rootId, uint256 principal); + event DrawLegCreated( + bytes32 indexed legId, + bytes32 indexed drawId, + bytes32 indexed childId, + uint256 principal + ); + event Settlement( + bytes32 indexed settlementId, + bytes32 indexed parentId, + address indexed asset, + address from, + address to, + uint256 principal + ); + event Repayment(bytes32 indexed repaymentId, bytes32 indexed drawId, uint256 principal); + event ChildClosed(bytes32 indexed childId, address indexed provider, uint256 returnedAmount); + event RootClosed(bytes32 indexed rootId, uint256 collateralReturned); + event RootExpired(bytes32 indexed rootId); + event ChildExpired(bytes32 indexed childId, uint256 returnedAmount); + + error Unauthorized(); + error InvalidState(); + error InvalidAmount(); + error InvalidExpiry(); + error InvalidAllocation(); + error AllocationNotVerified(); + error AllocationAlreadyConsumed(); + error InsufficientCapacity(); + error UnknownAccord(); + error TransferFailed(); + error Reentrancy(); + + modifier onlyOwner() { + if (msg.sender != owner) revert Unauthorized(); + _; + } + + modifier nonReentrant() { + if (_entered == 1) revert Reentrancy(); + _entered = 1; + _; + _entered = 0; + } + + constructor( + AccordRegistry registry_, + address collateralAsset_, + address liquidityAsset_, + address allocationVerifier_, + bytes32 extensionId_ + ) { + require(address(registry_) != address(0), "registry is zero"); + require(collateralAsset_ != address(0), "collateral asset is zero"); + require(liquidityAsset_ != address(0), "liquidity asset is zero"); + require(allocationVerifier_ != address(0), "verifier is zero"); + require(extensionId_ != bytes32(0), "extension id is zero"); + owner = msg.sender; + accordRegistry = registry_; + collateralAsset = collateralAsset_; + liquidityAsset = liquidityAsset_; + allocationVerifier = allocationVerifier_; + extensionId = extensionId_; + } + + function createRootAccord( + bytes32 rootId, + uint256 targetCapacity, + uint64 validUntil, + bytes32 policyHash + ) external { + if (rootId == bytes32(0) || targetCapacity == 0) revert InvalidAmount(); + if (validUntil <= block.timestamp) revert InvalidExpiry(); + if (rootAccords[rootId].state != ConcordTypes.RootState.NONE) revert InvalidState(); + + rootAccords[rootId] = ConcordTypes.RootAccord({ + id: rootId, + borrower: msg.sender, + collateralAsset: collateralAsset, + liquidityAsset: liquidityAsset, + targetCapacity: targetCapacity, + committedCapacity: 0, + drawnPrincipal: 0, + collateralLocked: 0, + validUntil: validUntil, + policyHash: policyHash, + syndicationRoundId: bytes32(0), + state: ConcordTypes.RootState.PROPOSED + }); + accordRegistry.registerNode(rootId, bytes32(0), AccordRegistry.NodeKind.ROOT_ACCORD); + emit RootAccordCreated(rootId, msg.sender, targetCapacity, validUntil, policyHash); + } + + function lockCollateral(bytes32 rootId, uint256 amount) external nonReentrant { + ConcordTypes.RootAccord storage root = _root(rootId); + if (msg.sender != root.borrower) revert Unauthorized(); + if (root.state != ConcordTypes.RootState.PROPOSED || amount == 0) revert InvalidState(); + _safeTransferFrom(collateralAsset, msg.sender, address(this), amount); + root.collateralLocked += amount; + emit CollateralLocked(rootId, msg.sender, amount); + } + + function openSyndication( + bytes32 rootId, + bytes32 roundId, + uint32 maxFeeBps, + uint64 roundExpiry, + address[] calldata eligibleProviders + ) external { + ConcordTypes.RootAccord storage root = _root(rootId); + if (msg.sender != root.borrower) revert Unauthorized(); + if (root.state != ConcordTypes.RootState.PROPOSED || root.collateralLocked == 0) { + revert InvalidState(); + } + if (roundId == bytes32(0) || rounds[roundId].status != ConcordTypes.RoundStatus.NONE) { + revert InvalidAllocation(); + } + if (maxFeeBps > BPS || roundExpiry <= block.timestamp || roundExpiry > root.validUntil) { + revert InvalidExpiry(); + } + if (eligibleProviders.length == 0) revert InvalidAllocation(); + + rounds[roundId] = MakkariRound({ + id: roundId, + rootAccordId: rootId, + targetCapacity: root.targetCapacity, + maxFeeBps: maxFeeBps, + roundExpiry: roundExpiry, + status: ConcordTypes.RoundStatus.OPEN + }); + root.syndicationRoundId = roundId; + root.state = ConcordTypes.RootState.SYNDICATING; + + for (uint256 i; i < eligibleProviders.length; ++i) { + address provider = eligibleProviders[i]; + if (provider == address(0) || eligibleProvider[roundId][provider]) revert InvalidAllocation(); + eligibleProvider[roundId][provider] = true; + eligibleProvidersByRound[roundId].push(provider); + } + + accordRegistry.registerNode(roundId, rootId, AccordRegistry.NodeKind.MAKKARI_SESSION); + emit SyndicationOpened(rootId, roundId, root.targetCapacity, maxFeeBps, roundExpiry); + } + + /// @notice Called only by the configured result verifier after it has + /// checked the FCC response and its active machine/signature binding. + function markAllocationVerified(bytes32 resultDigest) external { + // The explicit verifier boundary is kept as a separate immutable + // address so an owner key cannot silently impersonate the FCC path. + if (msg.sender != allocationVerifier) revert Unauthorized(); + if (resultDigest == bytes32(0)) revert InvalidAllocation(); + verifiedAllocationDigest[resultDigest] = true; + emit AllocationVerified(resultDigest); + } + + function materializeAllocation(ConcordTypes.AllocationResult calldata result) external { + ConcordTypes.RootAccord storage root = _root(result.rootAccordId); + MakkariRound storage round = rounds[result.roundId]; + + if (result.extensionId != extensionId || result.rootAccordId != round.rootAccordId) { + revert InvalidAllocation(); + } + if (root.state != ConcordTypes.RootState.SYNDICATING || round.status != ConcordTypes.RoundStatus.OPEN) { + revert InvalidState(); + } + if (block.timestamp > round.roundExpiry || result.roundExpiry != round.roundExpiry) revert InvalidExpiry(); + if (!result.success) revert InvalidAllocation(); + if (!verifiedAllocationDigest[result.resultDigest]) revert AllocationNotVerified(); + if (consumedAllocationDigest[result.resultDigest]) revert AllocationAlreadyConsumed(); + if (_allocationDigest(result) != result.resultDigest) revert InvalidAllocation(); + + uint256 length = result.selectedProviders.length; + if (length == 0 || result.allocatedCapacity.length != length || result.acceptedFeeBps.length != length) { + revert InvalidAllocation(); + } + if (result.termsCommitments.length != length) revert InvalidAllocation(); + + uint256 total; + for (uint256 i; i < length; ++i) { + address provider = result.selectedProviders[i]; + if (!eligibleProvider[result.roundId][provider] || result.allocatedCapacity[i] == 0) { + revert InvalidAllocation(); + } + if (result.acceptedFeeBps[i] > round.maxFeeBps) revert InvalidAllocation(); + for (uint256 j; j < i; ++j) { + if (result.selectedProviders[j] == provider) revert InvalidAllocation(); + } + total += result.allocatedCapacity[i]; + } + if (total != root.targetCapacity) revert InsufficientCapacity(); + + consumedAllocationDigest[result.resultDigest] = true; + round.status = ConcordTypes.RoundStatus.FINALIZED; + root.state = ConcordTypes.RootState.FUNDING; + accordRegistry.registerNode( + result.resultDigest, + result.roundId, + AccordRegistry.NodeKind.COFILL_ALLOCATION + ); + + for (uint256 i; i < length; ++i) { + bytes32 childId = keccak256(abi.encode(result.rootAccordId, result.resultDigest, result.selectedProviders[i])); + childAccords[childId] = ConcordTypes.ChildAccord({ + id: childId, + rootId: result.rootAccordId, + allocationId: result.resultDigest, + provider: result.selectedProviders[i], + selectedCapacity: result.allocatedCapacity[i], + committedCapacity: 0, + drawnPrincipal: 0, + feeBps: result.acceptedFeeBps[i], + validUntil: result.roundExpiry, + termsCommitment: result.termsCommitments[i], + state: ConcordTypes.ChildState.SELECTED + }); + childrenByRoot[result.rootAccordId].push(childId); + accordRegistry.registerNode(childId, result.resultDigest, AccordRegistry.NodeKind.CHILD_ACCORD); + emit ChildAccordSelected( + childId, + result.rootAccordId, + result.selectedProviders[i], + result.allocatedCapacity[i], + result.acceptedFeeBps[i] + ); + } + } + + function fundChild(bytes32 childId, uint256 amount) external nonReentrant { + ConcordTypes.ChildAccord storage child = _child(childId); + ConcordTypes.RootAccord storage root = _root(child.rootId); + if (msg.sender != child.provider) revert Unauthorized(); + if ( + (child.state != ConcordTypes.ChildState.SELECTED && child.state != ConcordTypes.ChildState.FUNDED) || + amount == 0 + ) revert InvalidState(); + if (root.state != ConcordTypes.RootState.FUNDING) revert InvalidState(); + if (child.committedCapacity + amount > child.selectedCapacity) revert InsufficientCapacity(); + + _safeTransferFrom(liquidityAsset, msg.sender, address(this), amount); + child.committedCapacity += amount; + root.committedCapacity += amount; + // Any successful USDT0 transfer makes the relationship funded. The + // provider may finish a selected allocation in more than one transfer, + // but selected capacity is never counted as committed before transfer. + child.state = ConcordTypes.ChildState.FUNDED; + emit ChildAccordFunded(childId, msg.sender, amount); + + if (root.committedCapacity == root.targetCapacity) { + root.state = ConcordTypes.RootState.ACTIVE; + bytes32[] storage childIds = childrenByRoot[root.id]; + for (uint256 i; i < childIds.length; ++i) { + if (childAccords[childIds[i]].state == ConcordTypes.ChildState.FUNDED) { + childAccords[childIds[i]].state = ConcordTypes.ChildState.ACTIVE; + } + } + emit RootActivated(root.id, root.committedCapacity); + } + } + + function draw(bytes32 drawId, bytes32 rootId, uint256 amount) external nonReentrant { + ConcordTypes.RootAccord storage root = _root(rootId); + if (msg.sender != root.borrower) revert Unauthorized(); + if (root.state != ConcordTypes.RootState.ACTIVE || amount == 0) revert InvalidState(); + if (drawId == bytes32(0) || draws[drawId].exists) revert InvalidAllocation(); + if (amount > availableCapacity(rootId)) revert InsufficientCapacity(); + + bytes32[] storage childIds = childrenByRoot[rootId]; + bytes32[] memory selectedChildren = new bytes32[](childIds.length); + uint256[] memory selectedAmounts = new uint256[](childIds.length); + uint256 selectedCount; + uint256 remaining = amount; + for (uint256 i; i < childIds.length && remaining > 0; ++i) { + ConcordTypes.ChildAccord storage child = childAccords[childIds[i]]; + if (child.state != ConcordTypes.ChildState.ACTIVE) continue; + uint256 available = child.committedCapacity - child.drawnPrincipal; + if (available == 0) continue; + uint256 legAmount = available < remaining ? available : remaining; + selectedChildren[selectedCount] = childIds[i]; + selectedAmounts[selectedCount] = legAmount; + ++selectedCount; + remaining -= legAmount; + } + if (remaining != 0) revert InsufficientCapacity(); + + _safeTransfer(liquidityAsset, root.borrower, amount); + draws[drawId] = Draw({ + id: drawId, + rootAccordId: rootId, + principal: amount, + repaidPrincipal: 0, + createdAt: uint64(block.timestamp), + exists: true + }); + root.drawnPrincipal += amount; + accordRegistry.registerNode(drawId, rootId, AccordRegistry.NodeKind.DRAW); + emit DrawCreated(drawId, rootId, amount); + + for (uint256 i; i < selectedCount; ++i) { + bytes32 childId = selectedChildren[i]; + bytes32 legId = keccak256(abi.encode(drawId, childId)); + DrawLeg memory leg = DrawLeg({ + id: legId, + drawId: drawId, + childAccordId: childId, + principal: selectedAmounts[i], + repaidPrincipal: 0 + }); + drawLegs[legId] = leg; + legsByDraw[drawId].push(legId); + childAccords[childId].drawnPrincipal += selectedAmounts[i]; + accordRegistry.registerNode(legId, drawId, AccordRegistry.NodeKind.DRAW_LEG); + emit DrawLegCreated(legId, drawId, childId, selectedAmounts[i]); + } + + bytes32 settlementId = keccak256(abi.encode("DRAW_SETTLEMENT", drawId)); + accordRegistry.registerSettlement( + settlementId, + drawId, + liquidityAsset, + address(this), + root.borrower, + amount + ); + emit Settlement(settlementId, drawId, liquidityAsset, address(this), root.borrower, amount); + } + + function repay(bytes32 drawId, uint256 amount) external nonReentrant { + Draw storage drawState = draws[drawId]; + if (!drawState.exists || amount == 0) revert InvalidAmount(); + ConcordTypes.RootAccord storage root = _root(drawState.rootAccordId); + if (msg.sender != root.borrower) revert Unauthorized(); + uint256 outstanding = drawState.principal - drawState.repaidPrincipal; + if (amount > outstanding) revert InvalidAmount(); + + _safeTransferFrom(liquidityAsset, msg.sender, address(this), amount); + drawState.repaidPrincipal += amount; + root.drawnPrincipal -= amount; + + uint256 remaining = amount; + bytes32[] storage legIds = legsByDraw[drawId]; + for (uint256 i; i < legIds.length && remaining > 0; ++i) { + DrawLeg storage leg = drawLegs[legIds[i]]; + uint256 legOutstanding = leg.principal - leg.repaidPrincipal; + if (legOutstanding == 0) continue; + uint256 legRepayment = legOutstanding < remaining ? legOutstanding : remaining; + leg.repaidPrincipal += legRepayment; + childAccords[leg.childAccordId].drawnPrincipal -= legRepayment; + remaining -= legRepayment; + } + if (remaining != 0) revert InvalidAmount(); + + bytes32 repaymentId = keccak256(abi.encode(drawId, amount, repaymentNonce++)); + accordRegistry.registerRepayment(repaymentId, drawId, amount); + emit Repayment(repaymentId, drawId, amount); + + bytes32 settlementId = keccak256(abi.encode("REPAYMENT_SETTLEMENT", repaymentId)); + accordRegistry.registerSettlement( + settlementId, + repaymentId, + liquidityAsset, + root.borrower, + address(this), + amount + ); + emit Settlement(settlementId, repaymentId, liquidityAsset, root.borrower, address(this), amount); + } + + function closeChild(bytes32 childId) external nonReentrant { + ConcordTypes.ChildAccord storage child = _child(childId); + ConcordTypes.RootAccord storage root = _root(child.rootId); + if (msg.sender != child.provider) revert Unauthorized(); + if ( + child.state != ConcordTypes.ChildState.ACTIVE && + child.state != ConcordTypes.ChildState.FUNDED && + child.state != ConcordTypes.ChildState.SELECTED + ) revert InvalidState(); + if (child.drawnPrincipal != 0) revert InvalidState(); + + uint256 returned = child.committedCapacity; + if (returned != 0) { + root.committedCapacity -= returned; + child.committedCapacity = 0; + _safeTransfer(liquidityAsset, child.provider, returned); + } + child.state = ConcordTypes.ChildState.CLOSED; + if (root.state == ConcordTypes.RootState.ACTIVE && root.committedCapacity < root.targetCapacity) { + root.state = ConcordTypes.RootState.FUNDING; + } + emit ChildClosed(childId, child.provider, returned); + } + + function expireChild(bytes32 childId) external nonReentrant { + ConcordTypes.ChildAccord storage child = _child(childId); + ConcordTypes.RootAccord storage root = _root(child.rootId); + if (block.timestamp <= child.validUntil || child.drawnPrincipal != 0) revert InvalidState(); + if ( + child.state != ConcordTypes.ChildState.SELECTED && + child.state != ConcordTypes.ChildState.FUNDED && + child.state != ConcordTypes.ChildState.ACTIVE + ) revert InvalidState(); + + uint256 returned = child.committedCapacity; + if (returned != 0) { + root.committedCapacity -= returned; + child.committedCapacity = 0; + _safeTransfer(liquidityAsset, child.provider, returned); + } + child.state = ConcordTypes.ChildState.EXPIRED; + emit ChildExpired(childId, returned); + } + + function closeRoot(bytes32 rootId) external nonReentrant { + ConcordTypes.RootAccord storage root = _root(rootId); + if (msg.sender != root.borrower) revert Unauthorized(); + if (root.drawnPrincipal != 0 || root.committedCapacity != 0) revert InvalidState(); + bytes32[] storage childIds = childrenByRoot[rootId]; + for (uint256 i; i < childIds.length; ++i) { + ConcordTypes.ChildState state = childAccords[childIds[i]].state; + if (state != ConcordTypes.ChildState.CLOSED && state != ConcordTypes.ChildState.EXPIRED) { + revert InvalidState(); + } + } + if (root.collateralLocked == 0) revert InvalidState(); + uint256 returnedCollateral = root.collateralLocked; + root.collateralLocked = 0; + root.state = ConcordTypes.RootState.CLOSED; + _safeTransfer(collateralAsset, root.borrower, returnedCollateral); + emit RootClosed(rootId, returnedCollateral); + } + + function expireRoot(bytes32 rootId) external nonReentrant { + ConcordTypes.RootAccord storage root = _root(rootId); + if (block.timestamp <= root.validUntil || root.drawnPrincipal != 0 || root.committedCapacity != 0) { + revert InvalidState(); + } + if (root.collateralLocked == 0) revert InvalidState(); + uint256 returnedCollateral = root.collateralLocked; + root.collateralLocked = 0; + root.state = ConcordTypes.RootState.EXPIRED; + _safeTransfer(collateralAsset, root.borrower, returnedCollateral); + emit RootExpired(rootId); + } + + function availableCapacity(bytes32 rootId) public view returns (uint256) { + ConcordTypes.RootAccord storage root = _root(rootId); + return root.committedCapacity - root.drawnPrincipal; + } + + function childAvailableCapacity(bytes32 childId) public view returns (uint256) { + ConcordTypes.ChildAccord storage child = _child(childId); + return child.committedCapacity - child.drawnPrincipal; + } + + function getRoot(bytes32 rootId) external view returns (ConcordTypes.RootAccord memory) { + return _root(rootId); + } + + function getChild(bytes32 childId) external view returns (ConcordTypes.ChildAccord memory) { + return _child(childId); + } + + function getChildIds(bytes32 rootId) external view returns (bytes32[] memory) { + return childrenByRoot[rootId]; + } + + function getEligibleProviders(bytes32 roundId) external view returns (address[] memory) { + return eligibleProvidersByRound[roundId]; + } + + function getDrawLegIds(bytes32 drawId) external view returns (bytes32[] memory) { + return legsByDraw[drawId]; + } + + function _root(bytes32 rootId) internal view returns (ConcordTypes.RootAccord storage root) { + root = rootAccords[rootId]; + if (root.state == ConcordTypes.RootState.NONE) revert UnknownAccord(); + } + + function _child(bytes32 childId) internal view returns (ConcordTypes.ChildAccord storage child) { + child = childAccords[childId]; + if (child.state == ConcordTypes.ChildState.NONE) revert UnknownAccord(); + } + + function _allocationDigest(ConcordTypes.AllocationResult calldata result) internal pure returns (bytes32) { + bytes memory encoded = abi.encodePacked( + result.extensionId, + result.roundId, + result.rootAccordId, + result.success, + result.roundExpiry + ); + for (uint256 i; i < result.selectedProviders.length; ++i) { + encoded = bytes.concat( + encoded, + abi.encodePacked( + result.selectedProviders[i], + result.allocatedCapacity[i], + result.acceptedFeeBps[i], + result.termsCommitments[i] + ) + ); + } + return keccak256(encoded); + } + + function _safeTransferFrom(address token, address from, address to, uint256 amount) internal { + (bool ok, bytes memory data) = token.call( + abi.encodeWithSelector(IERC20Minimal.transferFrom.selector, from, to, amount) + ); + if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert TransferFailed(); + } + + function _safeTransfer(address token, address to, uint256 amount) internal { + (bool ok, bytes memory data) = token.call(abi.encodeWithSelector(IERC20Minimal.transfer.selector, to, amount)); + if (!ok || (data.length != 0 && !abi.decode(data, (bool)))) revert TransferFailed(); + } +} diff --git a/contracts/ConcordInstructionSender.sol b/contracts/ConcordInstructionSender.sol new file mode 100644 index 0000000..ba93525 --- /dev/null +++ b/contracts/ConcordInstructionSender.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {ITeeExtensionRegistry} from "./interfaces/ITeeExtensionRegistry.sol"; +import {ITeeMachineRegistry} from "./interfaces/ITeeMachineRegistry.sol"; + +/// @title ConcordInstructionSender +/// @notice Concord's Flare Confidential Compute instruction entry point. +/// The message is expected to be encrypted to the active FCC machine public +/// key before it is submitted. Settlement and relationship state remain public +/// onchain; quote terms and losing offers remain inside the FCC boundary. +contract ConcordInstructionSender { + bytes32 public constant OP_TYPE_CONCORD = bytes32("CONCORD"); + bytes32 public constant OP_COMMAND_SUBMIT_QUOTE = bytes32("SUBMIT_QUOTE"); + bytes32 public constant OP_COMMAND_FINALIZE_ROUND = bytes32("FINALIZE_ROUND"); + + ITeeExtensionRegistry public immutable TEE_EXTENSION_REGISTRY; + ITeeMachineRegistry public immutable TEE_MACHINE_REGISTRY; + + uint256 private constant FIRST_PUBLIC_EXTENSION_ID = 0x10000; + uint256 private _extensionId; + + constructor(ITeeExtensionRegistry extensionRegistry_, ITeeMachineRegistry machineRegistry_) { + require(address(extensionRegistry_) != address(0), "extension registry is zero"); + require(address(machineRegistry_) != address(0), "machine registry is zero"); + require(address(extensionRegistry_).code.length > 0, "extension registry has no code"); + require(address(machineRegistry_).code.length > 0, "machine registry has no code"); + TEE_EXTENSION_REGISTRY = extensionRegistry_; + TEE_MACHINE_REGISTRY = machineRegistry_; + } + + function setExtensionId() external { + require(_extensionId == 0, "Extension ID already set."); + uint256 nextId = TEE_EXTENSION_REGISTRY.nextPublicExtensionId(); + for (uint256 i = FIRST_PUBLIC_EXTENSION_ID; i < nextId; ++i) { + if (TEE_EXTENSION_REGISTRY.getTeeExtensionInstructionsSender(i) == address(this)) { + _extensionId = i; + return; + } + } + revert("Extension ID not found."); + } + + function extensionId() external view returns (uint256) { + return _extensionId; + } + + function sendSubmitQuote(bytes calldata encryptedMessage) external payable returns (bytes32 instructionId) { + instructionId = _send(OP_COMMAND_SUBMIT_QUOTE, encryptedMessage); + } + + function sendFinalizeRound(bytes calldata encryptedMessage) external payable returns (bytes32 instructionId) { + instructionId = _send(OP_COMMAND_FINALIZE_ROUND, encryptedMessage); + } + + function _send(bytes32 command, bytes calldata encryptedMessage) internal returns (bytes32 instructionId) { + require(encryptedMessage.length != 0, "encrypted message is empty"); + address[] memory teeIds = TEE_MACHINE_REGISTRY.getRandomTeeIds(_getExtensionId(), 1); + address[] memory cosigners = new address[](0); + ITeeExtensionRegistry.TeeInstructionParams memory params = ITeeExtensionRegistry.TeeInstructionParams({ + opType: OP_TYPE_CONCORD, + opCommand: command, + message: encryptedMessage, + cosigners: cosigners, + cosignersThreshold: 0, + claimBackAddress: msg.sender + }); + instructionId = TEE_EXTENSION_REGISTRY.sendInstructions{value: msg.value}(teeIds, params); + } + + function _getExtensionId() internal view returns (uint256) { + require(_extensionId != 0, "Extension ID is not set."); + return _extensionId; + } +} diff --git a/contracts/ConcordTypes.sol b/contracts/ConcordTypes.sol new file mode 100644 index 0000000..e16526f --- /dev/null +++ b/contracts/ConcordTypes.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +/// @title ConcordTypes +/// @notice Shared value types for Concord's relationship and facility layers. +/// +/// The transaction is not Concord's canonical object. A RootAccord is. Child +/// Accords, rounds, draws, legs, and settlements are bounded by that root. +library ConcordTypes { + enum RootState { + NONE, + PROPOSED, + SYNDICATING, + FUNDING, + ACTIVE, + CLOSED, + FROZEN, + EXPIRED + } + + enum ChildState { + NONE, + SELECTED, + FUNDED, + ACTIVE, + CLOSED, + EXPIRED, + DEFAULTED + } + + enum RoundStatus { + NONE, + OPEN, + FINALIZED, + EXPIRED + } + + struct RootAccord { + bytes32 id; + address borrower; + address collateralAsset; + address liquidityAsset; + uint256 targetCapacity; + uint256 committedCapacity; + uint256 drawnPrincipal; + uint256 collateralLocked; + uint64 validUntil; + bytes32 policyHash; + bytes32 syndicationRoundId; + RootState state; + } + + struct ChildAccord { + bytes32 id; + bytes32 rootId; + bytes32 allocationId; + address provider; + uint256 selectedCapacity; + uint256 committedCapacity; + uint256 drawnPrincipal; + uint32 feeBps; + uint64 validUntil; + bytes32 termsCommitment; + ChildState state; + } + + /// @dev This is the verified boundary between FCC and materialization. + /// `resultDigest` commits to every field in this struct using the canonical + /// packed encoding implemented by CapitalFacility._allocationDigest(). + struct AllocationResult { + bytes32 extensionId; + bytes32 roundId; + bytes32 rootAccordId; + bool success; + address[] selectedProviders; + uint256[] allocatedCapacity; + uint32[] acceptedFeeBps; + bytes32[] termsCommitments; + uint64 roundExpiry; + bytes32 resultDigest; + } +} diff --git a/contracts/interfaces/IERC20Minimal.sol b/contracts/interfaces/IERC20Minimal.sol new file mode 100644 index 0000000..e164fe7 --- /dev/null +++ b/contracts/interfaces/IERC20Minimal.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +interface IERC20Minimal { + function transfer(address to, uint256 amount) external returns (bool); + + function transferFrom(address from, address to, uint256 amount) external returns (bool); + + function balanceOf(address account) external view returns (uint256); +} diff --git a/contracts/interfaces/ITeeExtensionRegistry.sol b/contracts/interfaces/ITeeExtensionRegistry.sol new file mode 100644 index 0000000..918ed72 --- /dev/null +++ b/contracts/interfaces/ITeeExtensionRegistry.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.7.6 <0.9; + +// TODO: Replace this minimal interface with the full import once flare-smart-contracts-v2 +// is published as a package: +// import { ITeeExtensionRegistry } from "flare-smart-contracts-v2/contracts/userInterfaces/tee/ITeeExtensionRegistry.sol"; +interface ITeeExtensionRegistry { + struct TeeInstructionParams { + bytes32 opType; + bytes32 opCommand; + bytes message; + address[] cosigners; + uint64 cosignersThreshold; + address claimBackAddress; + } + + function sendInstructions( + address[] calldata _teeIds, + TeeInstructionParams calldata _instructionParams + ) external payable returns (bytes32 _instructionId); + + function nextPublicExtensionId() external view returns (uint256); + + function getTeeExtensionInstructionsSender(uint256 _extensionId) + external view returns (address); +} diff --git a/contracts/interfaces/ITeeMachineRegistry.sol b/contracts/interfaces/ITeeMachineRegistry.sol new file mode 100644 index 0000000..8333d79 --- /dev/null +++ b/contracts/interfaces/ITeeMachineRegistry.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.7.6 <0.9; + +// TODO: Replace this minimal interface with the full import once flare-smart-contracts-v2 +// is published as a package: +// import { ITeeMachineRegistry } from "flare-smart-contracts-v2/contracts/userInterfaces/tee/ITeeMachineRegistry.sol"; +interface ITeeMachineRegistry { + function getRandomTeeIds(uint256 _extensionId, uint256 _count) + external view returns (address[] memory); +} diff --git a/contracts/test/ConcordFlow.t.sol b/contracts/test/ConcordFlow.t.sol new file mode 100644 index 0000000..978016e --- /dev/null +++ b/contracts/test/ConcordFlow.t.sol @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {AccordRegistry} from "../AccordRegistry.sol"; +import {CapitalFacility} from "../CapitalFacility.sol"; +import {ConcordTypes} from "../ConcordTypes.sol"; +import {MockERC20} from "./MockERC20.sol"; + +contract ConcordActor { + function approve(MockERC20 token, address spender, uint256 amount) external { + require(token.approve(spender, amount), "approve failed"); + } + + function createRoot( + CapitalFacility facility, + bytes32 rootId, + uint256 target, + uint64 validUntil, + bytes32 policyHash + ) external { + facility.createRootAccord(rootId, target, validUntil, policyHash); + } + + function lock(CapitalFacility facility, bytes32 rootId, uint256 amount) external { + facility.lockCollateral(rootId, amount); + } + + function open( + CapitalFacility facility, + bytes32 rootId, + bytes32 roundId, + uint32 maxFeeBps, + uint64 roundExpiry, + address[] calldata providers + ) external { + facility.openSyndication(rootId, roundId, maxFeeBps, roundExpiry, providers); + } + + function fund(CapitalFacility facility, bytes32 childId, uint256 amount) external { + facility.fundChild(childId, amount); + } + + function draw(CapitalFacility facility, bytes32 drawId, bytes32 rootId, uint256 amount) external { + facility.draw(drawId, rootId, amount); + } + + function repay(CapitalFacility facility, bytes32 drawId, uint256 amount) external { + facility.repay(drawId, amount); + } + + function closeChild(CapitalFacility facility, bytes32 childId) external { + facility.closeChild(childId); + } + + function closeRoot(CapitalFacility facility, bytes32 rootId) external { + facility.closeRoot(rootId); + } + + function mark(CapitalFacility facility, bytes32 digest) external { + facility.markAllocationVerified(digest); + } + + function materialize(CapitalFacility facility, ConcordTypes.AllocationResult calldata result) external { + facility.materializeAllocation(result); + } +} + +contract ConcordFlowTest { + bytes32 private constant EXTENSION_ID = keccak256("CONCORD_FCC_EXTENSION"); + bytes32 private constant ROOT_ID = keccak256("ROOT-01"); + bytes32 private constant ROUND_ID = keccak256("ROUND-01"); + bytes32 private constant ALLOCATION_ID = keccak256("ALLOCATION-01"); + bytes32 private constant DRAW_ID = keccak256("DRAW-01"); + + MockERC20 private fxrp; + MockERC20 private usdt0; + AccordRegistry private registry; + CapitalFacility private facility; + ConcordActor private treasury; + ConcordActor private providerA; + ConcordActor private providerB; + ConcordActor private providerC; + + function setUp() public { + fxrp = new MockERC20("Flare XRP", "FXRP", 6); + usdt0 = new MockERC20("Tether USD0", "USDT0", 18); + registry = new AccordRegistry(address(this)); + treasury = new ConcordActor(); + providerA = new ConcordActor(); + providerB = new ConcordActor(); + providerC = new ConcordActor(); + facility = new CapitalFacility( + registry, + address(fxrp), + address(usdt0), + address(providerC), + EXTENSION_ID + ); + registry.setFacility(address(facility)); + + fxrp.mint(address(treasury), 100); + usdt0.mint(address(providerA), 600); + usdt0.mint(address(providerB), 400); + treasury.approve(fxrp, address(facility), type(uint256).max); + providerA.approve(usdt0, address(facility), type(uint256).max); + providerB.approve(usdt0, address(facility), type(uint256).max); + } + + function testFullRelationshipLifecycle() public { + uint64 expiry = uint64(block.timestamp + 30 days); + treasury.createRoot(facility, ROOT_ID, 1_000, expiry, keccak256("POLICY")); + treasury.lock(facility, ROOT_ID, 100); + + address[] memory providers = new address[](3); + providers[0] = address(providerA); + providers[1] = address(providerB); + providers[2] = address(providerC); + treasury.open(facility, ROOT_ID, ROUND_ID, 700, expiry - 1 days, providers); + + address[] memory selected = new address[](2); + selected[0] = address(providerA); + selected[1] = address(providerB); + uint256[] memory allocations = new uint256[](2); + allocations[0] = 600; + allocations[1] = 400; + uint32[] memory fees = new uint32[](2); + fees[0] = 610; + fees[1] = 640; + bytes32[] memory terms = new bytes32[](2); + terms[0] = keccak256("A-TERMS"); + terms[1] = keccak256("B-TERMS"); + + ConcordTypes.AllocationResult memory result = ConcordTypes.AllocationResult({ + extensionId: EXTENSION_ID, + roundId: ROUND_ID, + rootAccordId: ROOT_ID, + success: true, + selectedProviders: selected, + allocatedCapacity: allocations, + acceptedFeeBps: fees, + termsCommitments: terms, + roundExpiry: expiry - 1 days, + resultDigest: bytes32(0) + }); + result.resultDigest = allocationDigest(result); + + // Only the configured verifier may authorize materialization. + providerC.mark(facility, result.resultDigest); + treasury.materialize(facility, result); + + bytes32 childA = keccak256(abi.encode(ROOT_ID, result.resultDigest, address(providerA))); + bytes32 childB = keccak256(abi.encode(ROOT_ID, result.resultDigest, address(providerB))); + require(facility.getChild(childA).state == ConcordTypes.ChildState.SELECTED, "A not selected"); + require(facility.getChild(childB).state == ConcordTypes.ChildState.SELECTED, "B not selected"); + + providerA.fund(facility, childA, 600); + providerB.fund(facility, childB, 400); + require(facility.getRoot(ROOT_ID).state == ConcordTypes.RootState.ACTIVE, "root not active"); + require(facility.availableCapacity(ROOT_ID) == 1_000, "available mismatch after funding"); + + treasury.draw(facility, DRAW_ID, ROOT_ID, 750); + require(facility.getRoot(ROOT_ID).drawnPrincipal == 750, "root exposure mismatch"); + require(facility.getChild(childA).drawnPrincipal == 600, "A exposure mismatch"); + require(facility.getChild(childB).drawnPrincipal == 150, "B exposure mismatch"); + require(facility.getDrawLegIds(DRAW_ID).length == 2, "draw was not split"); + + usdt0.mint(address(treasury), 750); + treasury.approve(usdt0, address(facility), type(uint256).max); + treasury.repay(facility, DRAW_ID, 750); + require(facility.getRoot(ROOT_ID).drawnPrincipal == 0, "root exposure not repaid"); + require(facility.availableCapacity(ROOT_ID) == 1_000, "capacity not restored"); + + providerA.closeChild(facility, childA); + providerB.closeChild(facility, childB); + treasury.closeRoot(facility, ROOT_ID); + require(facility.getRoot(ROOT_ID).state == ConcordTypes.RootState.CLOSED, "root not closed"); + require(fxrp.balanceOf(address(treasury)) == 100, "collateral not returned"); + } + + function testUnauthorizedResultAndOverfundingFail() public { + uint64 expiry = uint64(block.timestamp + 30 days); + treasury.createRoot(facility, ROOT_ID, 1_000, expiry, keccak256("POLICY")); + treasury.lock(facility, ROOT_ID, 100); + address[] memory providers = new address[](1); + providers[0] = address(providerA); + treasury.open(facility, ROOT_ID, ROUND_ID, 700, expiry - 1 days, providers); + + address[] memory selected = new address[](1); + selected[0] = address(providerA); + uint256[] memory allocations = new uint256[](1); + allocations[0] = 1_000; + uint32[] memory fees = new uint32[](1); + fees[0] = 700; + bytes32[] memory terms = new bytes32[](1); + terms[0] = keccak256("A-TERMS"); + ConcordTypes.AllocationResult memory result = ConcordTypes.AllocationResult({ + extensionId: EXTENSION_ID, + roundId: ROUND_ID, + rootAccordId: ROOT_ID, + success: true, + selectedProviders: selected, + allocatedCapacity: allocations, + acceptedFeeBps: fees, + termsCommitments: terms, + roundExpiry: expiry - 1 days, + resultDigest: bytes32(0) + }); + result.resultDigest = allocationDigest(result); + + (bool unauthorizedOk,) = address(treasury).call( + abi.encodeWithSelector(ConcordActor.materialize.selector, facility, result) + ); + require(!unauthorizedOk, "unverified materialization succeeded"); + + providerC.mark(facility, result.resultDigest); + // The provider list is eligible but the allocation is insufficiently + // funded for the requested target; it remains a selected child. + treasury.materialize(facility, result); + bytes32 childA = keccak256(abi.encode(ROOT_ID, result.resultDigest, address(providerA))); + usdt0.mint(address(providerA), 400); + providerA.fund(facility, childA, 1_000); + (bool overfundOk,) = address(providerA).call( + abi.encodeWithSelector(ConcordActor.fund.selector, facility, childA, 1) + ); + require(!overfundOk, "overfunding succeeded"); + } + + function allocationDigest(ConcordTypes.AllocationResult memory result) internal pure returns (bytes32) { + bytes memory encoded = abi.encodePacked( + result.extensionId, + result.roundId, + result.rootAccordId, + result.success, + result.roundExpiry + ); + for (uint256 i; i < result.selectedProviders.length; ++i) { + encoded = bytes.concat( + encoded, + abi.encodePacked( + result.selectedProviders[i], + result.allocatedCapacity[i], + result.acceptedFeeBps[i], + result.termsCommitments[i] + ) + ); + } + return keccak256(encoded); + } +} diff --git a/contracts/test/MockERC20.sol b/contracts/test/MockERC20.sol new file mode 100644 index 0000000..b74aa8b --- /dev/null +++ b/contracts/test/MockERC20.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +contract MockERC20 { + string public name; + string public symbol; + uint8 public immutable decimals; + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + constructor(string memory name_, string memory symbol_, uint8 decimals_) { + name = name_; + symbol = symbol_; + decimals = decimals_; + } + + function mint(address to, uint256 amount) external { + balanceOf[to] += amount; + totalSupply += amount; + } + + function approve(address spender, uint256 amount) external returns (bool) { + allowance[msg.sender][spender] = amount; + return true; + } + + function transfer(address to, uint256 amount) external returns (bool) { + _move(msg.sender, to, amount); + return true; + } + + function transferFrom(address from, address to, uint256 amount) external returns (bool) { + uint256 allowed = allowance[from][msg.sender]; + require(allowed >= amount, "allowance"); + allowance[from][msg.sender] = allowed - amount; + _move(from, to, amount); + return true; + } + + function _move(address from, address to, uint256 amount) internal { + require(balanceOf[from] >= amount, "balance"); + balanceOf[from] -= amount; + balanceOf[to] += amount; + } +} diff --git a/docker-compose.coston2.yaml b/docker-compose.coston2.yaml new file mode 100644 index 0000000..5d6fecc --- /dev/null +++ b/docker-compose.coston2.yaml @@ -0,0 +1,19 @@ +# Docker Compose override for Coston2 deployment. +# Usage: docker compose -f docker-compose.yaml -f docker-compose.coston2.yaml up +services: + ext-proxy: + volumes: + - ./config/proxy/extension_proxy.coston2.docker.toml:/app/config/config.toml:ro + + extension-tee: + environment: + - CHAIN_URL=${CHAIN_URL:-https://coston2-api.flare.network/ext/C/rpc} + # Coston2 chain id. The node binds every signature to this value; it must + # match the proxy's chain_id (114) and the on-chain registry, or signature + # verification fails (InvalidTeePublicKeyOrSignature / proxy panic). + - CHAIN_ID=${CHAIN_ID:-114} + +networks: + default: + name: ${COMPOSE_NETWORK:-extension-scaffold-coston2} + external: false diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..3d4ab34 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,62 @@ +services: + redis: + image: redis:7-alpine + command: redis-server --save "" --appendonly no + ports: + - "${REDIS_BIND:-127.0.0.1:6382}:6379" + tmpfs: + - /data + + ext-proxy: + image: ${REGISTRY:-local}/tee-proxy + volumes: + - ./config/proxy/extension_proxy.docker.toml:/app/config/config.toml:ro + environment: + - PROXY_PRIVATE_KEY=${PROXY_PRIVATE_KEY:?PROXY_PRIVATE_KEY must be set in .env} + extra_hosts: + - "host.docker.internal:host-gateway" + ports: + - "${EXT_PROXY_INTERNAL_BIND:-0.0.0.0:6673}:6663" + - "${EXT_PROXY_EXTERNAL_BIND:-0.0.0.0:6674}:6664" + depends_on: + - redis + + extension-tee: + build: + context: . + # Selected by LANGUAGE in .env; start-services.sh resolves /Dockerfile + # by directory convention. Adding a language needs no change here. + dockerfile: ${EXTENSION_DOCKERFILE:-go/Dockerfile} + args: + SOURCE_DATE_EPOCH: ${SOURCE_DATE_EPOCH:-0} + # Consumed by language images that build tee-node from source. A Go + # pseudo-version pin is a commit SHA, not a tag — see scripts/lib/versions.sh. + TEE_NODE_REF: ${TEE_NODE_REF:-} + env_file: + - path: ./config/extension.env + required: false + environment: + - MODE=${MODE:-1} + # Chain id the node binds signatures to — must match the proxy's chain_id + # and the chain itself. Local e2e Hardhat = 31337; the coston/coston2 + # overrides set 16/114 for those networks. + - CHAIN_ID=${CHAIN_ID:-31337} + - INITIAL_OWNER=${INITIAL_OWNER:?INITIAL_OWNER must be set in .env} + # TEE governance the node signs with. Must match what set-governance + # registers on-chain (post-build reads the same vars). Defaults to the + # deployer (INITIAL_OWNER) as sole signer, threshold 1; override in .env. + - GOVERNANCE_SIGNERS=${GOVERNANCE_SIGNERS:?GOVERNANCE_SIGNERS must be set in .env} + - GOVERNANCE_THRESHOLD=${GOVERNANCE_THRESHOLD:-1} + - PROXY_URL=http://ext-proxy:6663 + - CONFIG_PORT=5501 + - SIGN_PORT=7701 + - EXTENSION_PORT=7702 + - LOG_LEVEL=${LOG_LEVEL:-INFO} + - SIMULATED_TEE=${SIMULATED_TEE:-true} + depends_on: + - ext-proxy + +networks: + default: + name: ${COMPOSE_NETWORK:-docker_default} + external: true diff --git a/docker/node-base.Dockerfile b/docker/node-base.Dockerfile new file mode 100644 index 0000000..272a10d --- /dev/null +++ b/docker/node-base.Dockerfile @@ -0,0 +1,90 @@ +# Shared tee-node builder — produces the `server` binary that non-Go language +# images run alongside their extension process. +# +# Built once as local/tee-node-base:${TEE_NODE_REF} and consumed by every +# two-process language image, so a new language's Dockerfile is ~30 lines rather +# than ~110. The Go image does not use this: it links tee-node as a library and +# ships a single binary (see go/Dockerfile and docs/extension-contract.md §1). +# +# Build directly: +# docker build -f docker/node-base.Dockerfile --build-arg TEE_NODE_REF= -t local/tee-node-base: docker/ +# +# scripts/start-services.sh builds this automatically when the selected language +# needs it, tagging by TEE_NODE_REF so a pin bump invalidates the cache naturally. + +# Same digest-pinned golang image as go/Dockerfile, so the server binary bytes +# are identical no matter which language image embeds it. +FROM golang:1.25.1-trixie@sha256:ff83f3762390c2cccb53618ccc18af23e556aff9b1db4428637e9f63287c8171 AS builder + +ARG SOURCE_DATE_EPOCH +ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH + +# Git-resolvable tee-node ref, derived from the Go module pin by +# scripts/lib/versions.sh. Required — there is no default, because a stale +# default is exactly the drift this image exists to prevent. +ARG TEE_NODE_REF + +WORKDIR /build + +# apt normally resolves to whatever package versions the mirror serves at build time. +# Redirect apt at snapshot.debian.org keyed on SOURCE_DATE_EPOCH so every build +# installs the exact package set that existed at that instant. +# NOTE: adapted from https://github.com/reproducible-containers/repro-sources-list.sh/blob/master/alternative/Dockerfile.debian-13 +RUN \ + --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + if [ "${SOURCE_DATE_EPOCH:-0}" -gt 0 ] 2>/dev/null; then \ + snapshot="$(/bin/bash -euc "printf \"%(%Y%m%dT%H%M%SZ)T\n\" \"${SOURCE_DATE_EPOCH}\"")" && \ + sed -i -e '/Types: deb/ a\Snapshot: true' /etc/apt/sources.list.d/debian.sources && \ + rm -f /etc/apt/apt.conf.d/docker-clean && \ + echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache && \ + apt-get install --update --snapshot "${snapshot}" -o Acquire::Check-Valid-Until=false -o Acquire::https::Verify-Peer=false -y ca-certificates && \ + rm -rf /var/log/* /var/cache/ldconfig/aux-cache; \ + else \ + apt-get update && apt-get install -y ca-certificates && \ + rm -rf /var/log/* /var/cache/ldconfig/aux-cache; \ + fi + +# Fetch tee-node at exactly the pinned ref. +# +# Two approaches are deliberately NOT used here: +# +# git clone --branch ${TEE_NODE_REF} +# The Go module pin is frequently a pseudo-version +# (v0.0.21-0.20260619120252-31fc839ae6d2) whose last segment is a commit +# SHA. --branch resolves tags and branches only, so it fails on a SHA. +# +# git fetch --depth 1 origin ${TEE_NODE_REF} +# Fetching a raw commit requires the server to allow it +# (uploadpack.allowAnySHA1InWant) AND a full 40-char SHA. The pin gives an +# abbreviated 12-char SHA, and GitHub rejects it: "couldn't find remote ref". +# +# A blobless partial clone fetches all refs cheaply (deferring file contents), +# after which an abbreviated SHA resolves locally. Works for tags and SHAs alike. +RUN test -n "${TEE_NODE_REF}" || (echo "ERROR: TEE_NODE_REF build arg is required" >&2 && exit 1) +RUN git clone --filter=blob:none https://github.com/flare-foundation/tee-node.git tee-node && \ + cd tee-node && \ + git checkout "${TEE_NODE_REF}" && \ + echo "tee-node at $(git rev-parse HEAD)" + +WORKDIR /build/tee-node + +RUN go mod download +RUN go mod verify + +# -trimpath strips build-host paths; -buildid= clears go's non-deterministic build id; +# -s -w drop symbol and dwarf tables; -buildvcs=false omits embedded vcs metadata; +# CGO_ENABLED=0 produces a static binary, avoiding link-time libc variance. +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOFLAGS="-buildvcs=false" \ + go build -trimpath -ldflags="-buildid= -s -w" -o /app/server ./cmd/extension + +# Carry the Confidential Space root cert alongside the binary — language images +# copy both from this stage. +RUN cp /build/tee-node/assets/google_confidential_space_root.crt /app/google_confidential_space_root.crt + +RUN if [ "${SOURCE_DATE_EPOCH:-0}" -gt 0 ] 2>/dev/null; then find /app -exec touch -h -d @${SOURCE_DATE_EPOCH} {} +; fi + +# Minimal final stage: language images only ever COPY --from this image. +FROM scratch +COPY --from=builder /app/server /app/server +COPY --from=builder /app/google_confidential_space_root.crt /app/assets/google_confidential_space_root.crt diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md new file mode 100644 index 0000000..fa9736d --- /dev/null +++ b/docs/CONCORD_STATUS.md @@ -0,0 +1,63 @@ +# Concord status + +Updated 2026-08-12. + +## Works in the durable checkpoint + +- AccordRegistry, CapitalFacility, ConcordInstructionSender and shared Concord + types compile with Solidity 0.8.27. +- Foundry flow tests pass locally, including the two-child draw and repayment + lifecycle. +- The Go Concord extension compiles and tests signed quotes and deterministic + CoFill inside the official FCE scaffold structure. +- Deployment tooling generates a binding for ConcordInstructionSender and has + no embedded private-key fallback. +- Coston2 asset resolution is centralized in config/networks/coston2.json and + validated by scripts/resolve-coston2-assets.sh. + +## Not yet evidenced + +- No Concord contracts are claimed as deployed from this checkpoint. +- No funded Coston2 treasury/provider accounts, FCC registration, instruction + IDs, settlement receipts or end-to-end explorer evidence are recorded yet. +- The configured allocationVerifier is an explicit offchain integration + boundary. A local test actor is not a live FCC proof. + +## Privacy truth + +The implementation does not claim private FXRP transfers, private USDT0 +transfers, private EVM execution, or production hardware-backed TEE security. +The FCC development path may use the documented simulated TEE. Provider terms, +losing quotes and allocation computation are intended for the confidential +boundary; accepted capacity, exposure, transfers and lineage are public where +the facility requires them. + +## Current Flare inputs + +- Coston2 chain ID: 114. +- FXRP manager is resolved from ContractRegistry under AssetManagerFXRP. +- FXRP token is resolved from that manager's fAsset(). +- USDT0 is the current validated Coston2 liquidity-token snapshot in the + network config; the resolver checks symbol USDT0, decimals 18, and code. + +The authoritative implementation/docs links are kept in the root README. If +the official Flare registry, scaffold or FCC behaviour changes, update the +affected configuration and record the contradiction before continuing. + +## Deployment evidence template + +When the live slice is run, append: + +~~~text +Network: +AccordRegistry: +CapitalFacility: +ConcordInstructionSender: +FCC extension id: +Makkari round: +SUBMIT_QUOTE instruction ids: +FINALIZE_ROUND instruction id: +allocation result digest: +root / child / draw / repayment transaction hashes: +Explorer links: +~~~ diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..5581847 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,25 @@ +# Concord documentation + +Start with [architecture.md](architecture.md) for the relationship model, +[getting-started.md](getting-started.md) for the durable build and Coston2 +inputs, and [CONCORD_STATUS.md](CONCORD_STATUS.md) for the evidence boundary. + +## Product-specific + +- [architecture.md](architecture.md) — Accord, Makkari, CoFill and lifecycle. +- [lineage.md](lineage.md) — queryable parent-child causal graph. +- [getting-started.md](getting-started.md) — local checks, asset resolution and deployment inputs. +- [testing.md](testing.md) — contract, extension and tooling tests. +- [CONCORD_STATUS.md](CONCORD_STATUS.md) — what is and is not evidenced. + +## Official scaffold reference + +The remaining scaffold documents describe the official FCC runtime and remain +reference material for operating Concord: + +[deployment-steps.md](deployment-steps.md) · +[testing-against-coston2.md](testing-against-coston2.md) · +[cloudflared.md](cloudflared.md) · +[extension-guide.md](extension-guide.md) · +[instruction-sender.md](instruction-sender.md) · +[manual-setup.md](manual-setup.md) diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..ab77da0 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,70 @@ +# Concord architecture + +Concord coordinates one persistent economic relationship: an FXRP-backed +syndicated capital facility. The canonical object is the relationship, not an +individual transaction. + +~~~text +Root Accord + ↓ +Makkari session (confidential FCC boundary) + ↓ +CoFill allocation + ↓ +Child Accords + ↓ +funded commitments → Draw → DrawLegs → Settlement → Repayment +~~~ + +## Onchain responsibilities + +| Component | Responsibility | +|---|---| +| AccordRegistry | Relationship identity, parent-child links, lineage nodes, settlement and repayment references. | +| CapitalFacility | FXRP collateral, USDT0 commitments, child funding, exposure, deterministic multi-child draws, repayment, expiry and close rules. | +| ConcordInstructionSender | Encrypted SUBMIT_QUOTE and FINALIZE_ROUND instruction submission through the official FCC registry path. | +| ConcordTypes | Root Accord, Child Accord, round and allocation data shapes. | + +The facility does not run CoFill. It accepts a verified, bound allocation +result and materializes each selected provider as a child relationship. + +## Makkari and CoFill + +The Go extension is built inside the official Flare FCE scaffold. It requires +the instruction payload to be encrypted to the active TEE machine and calls +the scaffold's decrypt endpoint. It validates provider signatures, quote +expiry, fee policy, provider eligibility and round/root binding. + +CoFill sorts by lowest fee, then provider address, then quote nonce. It permits +a partial final allocation and fails explicitly when eligible capacity is +insufficient. The returned digest binds the extension, round, root, expiry, +providers, amounts, fees and terms commitments. + +## Economic state + +Provider capacity is intentionally distinct from provider quotes: + +~~~text +quoted → selected → funded → active → closed +~~~ + +selectedCapacity is the FCC result. committedCapacity increases only after the +provider's USDT0 transferFrom succeeds. Root committed capacity is the sum of +actual child commitments, not the sum of selected quotes. + +Draws are not flattened. A root draw creates one Draw node and one DrawLeg per +child relationship consumed. Repayment reduces the draw leg, child and root +exposures only after the ERC-20 transfer succeeds. + +## Trust and privacy boundary + +The current checkpoint does not claim private FXRP or USDT0 transfers, private +EVM execution, or production hardware-backed TEE security. FCC development may +use the documented simulated TEE path. Settlement, accepted executable +capacity, exposure, and lineage are public where the facility requires them. + +allocationVerifier is an explicit integration boundary. It must verify the FCC +result's operation, active machine/signature evidence, extension, round, root +and digest before calling markAllocationVerified. The Solidity tests use a +verifier actor; that is not proof that the live Coston2 verifier has been +deployed. diff --git a/docs/cloudflared.md b/docs/cloudflared.md new file mode 100644 index 0000000..db459f8 --- /dev/null +++ b/docs/cloudflared.md @@ -0,0 +1,130 @@ +# Cloudflare Tunnel (ngrok replacement) + +Exposes the extension proxy's external port (host `6674`) over a public HTTPS URL, so +Flare's TEE infrastructure can reach it. Use instead of ngrok. Compose file: +`docker-compose.cloudflared.yaml`. + +Only testnets need this. On `--chain local` nothing is started — a local devnet reaches +the proxy on localhost. + +## The scripts drive it + +`start-services.sh` syncs the tunnel *before* every other container, then writes the URL +into `.env` as `EXT_PROXY_URL`. `full-setup.sh` calls it in Phase 2, and `post-build.sh` / +`test.sh` re-read `.env`, so a whole run picks the URL up by itself: + +```bash +./scripts/full-setup.sh --chain coston2 --tunnel --test +``` + +| Situation | What `start-services.sh` does | +|---|---| +| Tunnel already running | Reuses it and adopts its URL — no flag needed | +| No tunnel, `--tunnel` passed | Starts it, waits for the URL, writes `EXT_PROXY_URL` | +| No tunnel, no `--tunnel` | Warns; `EXT_PROXY_URL` must already be valid in `.env` | +| `TUNNEL_ARGS` set (named tunnel) | Starts it and leaves your `EXT_PROXY_URL` alone | + +`stop-services.sh` leaves the tunnel up unless you pass `--tunnel` — see below. + +## One tunnel, shared by every extension + +The tunnel does **not** belong to this extension. It runs in a compose project called +`tunnel`, set by the `name:` line at the top of `docker-compose.cloudflared.yaml`. + +Compose identifies a container by **project name + service name** and nothing else — not +the file's path, not the folder it sits in. So every extension's copy of this file +addresses the *same* container: the first `up -d` creates it, every later one finds it +running and inherits its URL. That works because every extension here publishes ext-proxy +on host `6674`, so only one stack is ever behind the tunnel; it is handed from extension to +extension, keeping one URL across the switch. + +That is also why `stop-services.sh` keeps it alive by default: tearing it down rotates the +URL for every other extension and strands their `EXT_PROXY_URL`. + +**Copy `docker-compose.cloudflared.yaml` verbatim.** Compose compares the *resolved service +config* to decide reuse-vs-recreate. Comments and file paths are not part of that +comparison, but `command:` is — so a differing `TUNNEL_TARGET` recreates the container and +mints a new URL for everyone. Setting `TUNNEL_TARGET` by hand warns you for this reason. + +Two cases that legitimately need their own tunnel, and get their own project: + +| Case | Project | Why | +|---|---|---| +| `--local` (host Go proxy) | `tunnel-local` | Proxy listens on 6664, a different origin — the script handles this | +| `prediction_market` | give it its own | Publishes on 6676, cannot share | + +## Manual + +From the repo root, in Git Bash: + +```bash +# 1. Start it (pulls cloudflare/cloudflared on first run) +docker compose -f docker-compose.cloudflared.yaml up -d + +# 2. Read the URL cloudflared printed at startup +docker compose -f docker-compose.cloudflared.yaml logs cloudflared \ + | grep -o 'https://[a-z0-9-]*\.trycloudflare\.com' | tail -1 +``` + +Put that URL in `.env` as `EXT_PROXY_URL=`, **then** start the containers. +`start-services.sh` blocks on `$EXT_PROXY_URL/info`, so a stale value there is what makes +it fail. Stop with `docker compose -f docker-compose.cloudflared.yaml down`. + +Skipping the copy-paste — does step 2 and writes the result straight into `.env`: + +```bash +URL=$(docker compose -f docker-compose.cloudflared.yaml logs cloudflared \ + | grep -o 'https://[a-z0-9-]*\.trycloudflare\.com' | tail -1) \ + && sed -i "s|^EXT_PROXY_URL=.*|EXT_PROXY_URL=$URL|" .env \ + && grep '^EXT_PROXY_URL=' .env +``` + +Rewrites an existing `EXT_PROXY_URL=` line only — if the echo comes back empty, add the +line once by hand and this works from then on. + +## Already registered on-chain? Repoint the machine record too + +`EXT_PROXY_URL` in `.env` only feeds the local scripts. Flare's TEE infra delivers +instructions to the URL stored in the `MachineManager`, written once at registration — +`post-build.sh` will **not** rewrite it for an already-registered machine. A rotated URL +therefore looks like every on-chain op timing out at `pollAction` even though the chain leg +succeeded. + +```bash +./scripts/update-tee-url.sh # defaults to EXT_PROXY_URL from .env +``` + +Calls `MachineManager.updateTeeMachineSettings` directly — no attestation, one transaction, +ledger untouched. A full re-registration would trip `ChallengeExpired` on a long-running +TEE. This is the strongest reason to want a stable hostname. + +## Making the URL stable + +Quick tunnels always rotate and no flag pins them. Cheapest fix first: + +**Don't restart the tunnel.** It has no dependency on the main stack, so `down && up` on the +app containers does not touch it. Only restarting `cloudflared` itself rotates the URL. + +**Named tunnel — permanently fixed hostname.** Needs a free Cloudflare account with a domain +on it: + +1. Zero Trust → Networks → Tunnels → *Create a tunnel* → Cloudflared. Copy the `eyJ...` token. +2. Add a public hostname pointing at `http://host.docker.internal:6674`. +3. In `.env`: + ``` + TUNNEL_ARGS=run --token eyJhIjoi... + EXT_PROXY_URL=https://tee.yourdomain.com + ``` +4. `docker compose -f docker-compose.cloudflared.yaml up -d` — same command, named mode now. + +`EXT_PROXY_URL` and the on-chain machine record then survive every restart, and +`restart: unless-stopped` becomes safe to add to the service. + +## Know this + +| | | +|---|---| +| A quick tunnel's URL changes on every start | There is deliberately no `restart:` policy — a silent restart would mint a new URL and strand `EXT_PROXY_URL` | +| The tunnel starts fine with nothing behind it | It just 502s until the containers are up | +| No network dependency on the main stack | Start order does not matter | +| Proxy running as a local Go binary on 6664? | `TUNNEL_TARGET=http://host.docker.internal:6664` — use a separate `-p` project so the shared tunnel is untouched | diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..54a222a --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,75 @@ +# Getting started + +The durable source checkout is /workspace/Concord in this build environment. +The runtime foundation is the official Flare FCE scaffold; the Concord +implementation is the Go extension under go/. + +## Local verification + +Use Foundry and Go from the installed toolchain or your own equivalent: + +~~~bash +export PATH=/workspace/tooling/foundry:/workspace/tooling/go/bin:$PATH +export FOUNDRY_SOLC=/workspace/tooling/solc-0.8.27 + +forge test +(cd go && GOTOOLCHAIN=local go test ./...) +(cd tools && GOTOOLCHAIN=local go test ./...) +~~~ + +The Solidity flow test covers root creation, FXRP collateral, a verified +allocation, two funded child Accords, a multi-child draw, repayment, restored +capacity, child closure and root closure. The Go tests cover encrypted-action +dispatch, signed quote validation, deterministic ordering, partial final +allocation, invalid signatures and insufficient capacity. + +Generate the instruction-sender binding after changing its ABI: + +~~~bash +./scripts/generate-bindings.sh +~~~ + +## Coston2 asset resolution + +The current target is Coston2, chain ID 114. Resolve and validate live asset +metadata before deployment: + +~~~bash +./scripts/resolve-coston2-assets.sh +~~~ + +The resolver obtains AssetManagerFXRP from Flare's ContractRegistry and then +calls fAsset(). USDT0 is kept in the centralized Coston2 network config and +validated by live symbol, decimals and bytecode because it is not currently a +named entry in the checked-in Flare ContractRegistry package. + +## Deployment inputs + +The deployment script requires a funded Coston2 broadcaster and an explicit +allocation verifier: + +~~~bash +export DEPLOYMENT_OWNER=0x... +export ALLOCATION_VERIFIER=0x... +export CONCORD_EXTENSION_ID=0x... +export TEE_EXTENSION_REGISTRY=0x... +export TEE_MACHINE_REGISTRY=0x... +./scripts/deploy-coston2.sh +~~~ + +The TEE registry values are normally the Coston2 FlareTeeManager diamond +address used by the official scaffold tooling. The script does not guess +them. Keep private keys in an ignored .env, never in source or compose +defaults. + +## FCC sequence + +Registration and proxy setup follow the current Flare FCE scaffold sequence: + +~~~text +register extension → start official proxy/TEE scaffold → submit encrypted quotes +→ finalize encrypted round → verify result offchain → mark digest → materialize children +~~~ + +No live FCC or facility deployment is claimed by local tests alone. See +CONCORD_STATUS.md for the current evidence boundary. diff --git a/docs/lineage.md b/docs/lineage.md new file mode 100644 index 0000000..91ff12c --- /dev/null +++ b/docs/lineage.md @@ -0,0 +1,25 @@ +# Lineage + +The relationship graph is queryable through AccordRegistry. + +~~~text +ROOT_ACCORD + └─ MAKKARI_SESSION + └─ COFILL_ALLOCATION + └─ CHILD_ACCORD + └─ DRAW + └─ DRAW_LEG + └─ SETTLEMENT + └─ REPAYMENT + └─ SETTLEMENT +~~~ + +The facility registers nodes only after the corresponding economic boundary is +valid. A draw is registered only after its USDT0 transfer succeeds; every leg +points back to the child relationship that supplied that portion. A repayment +is registered only after the borrower's transfer succeeds and carries the draw +as its parent. + +The getChildren(parentId) query is the minimal graph traversal primitive. +Relationship state remains in CapitalFacility, while AccordRegistry answers +which relationship authorized a derived action. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..acd1a49 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,52 @@ +# Testing + +Concord's tests are organized around the causal chain, including failure +behaviour. + +## Solidity + +~~~bash +PATH=/workspace/tooling/foundry:/workspace/tooling/go/bin:$PATH \ +FOUNDRY_SOLC=/workspace/tooling/solc-0.8.27 \ +forge test -vv +~~~ + +contracts/test/ConcordFlow.t.sol covers: + +- root Accord creation and FXRP collateral locking; +- verifier-gated, root/round/extension-bound allocation materialization; +- duplicate-provider and overfunding constraints; +- selected versus funded child capacity; +- root activation only after actual USDT0 transfers; +- deterministic draw allocation across at least two children; +- explicit DrawLeg and settlement lineage; +- exposure reduction after repayment and restored availability; +- provider capital return and collateral return after exposure is clear. + +## FCC extension + +~~~bash +(cd go && GOTOOLCHAIN=local go test ./...) +~~~ + +The Go tests cover strict payload decoding, the encrypted-action boundary, +EIP-191 provider signature recovery, expired and invalid quotes, deterministic +fee/address/nonce ordering, partial final allocation, and explicit insufficient +capacity failure. + +## Deployment tooling + +~~~bash +(cd tools && GOTOOLCHAIN=local go test ./...) +~~~ + +This validates deployment support, result parsing, diagnostics and the +generated ConcordInstructionSender binding. A live Coston2 run additionally +requires a funded account, registered extension, reachable official FCC +scaffold, provider accounts, and an independently verified allocation result. + +## Evidence rule + +Passing local tests do not equal a Coston2 deployment. Record contract +addresses, instruction IDs, transaction hashes, result digests and explorer +links in the status record before making a live-flow claim. diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..e7aef5f --- /dev/null +++ b/foundry.toml @@ -0,0 +1,8 @@ +[profile.default] +src = "contracts" +out = "out" +libs = ["lib"] +# TODO: Once flare-smart-contracts-v2 is published as a package, add it as a dependency +# and restore the remapping: +# remappings = ["flare-smart-contracts-v2=lib/flare-smart-contracts-v2"] +via-ir = true diff --git a/go/Dockerfile b/go/Dockerfile new file mode 100644 index 0000000..d145281 --- /dev/null +++ b/go/Dockerfile @@ -0,0 +1,98 @@ +# Default node build: self-contained and reproducible. Build context is this extension dir. +# The extension module pins github.com/flare-foundation/tee-node to a released version in go.mod +# and fetches it from the network (verified against go.sum) — no on-disk sibling repo or replace +# directive is needed, so only the module's own sources are copied into the build. +# +# To build against an on-disk sibling tee-node instead (while developing it), use +# `USE_LOCAL_SIBLINGS=1 ./scripts/start-services.sh`, which selects ./Dockerfile.siblings. + +# pin base image by digest so every build starts from the same bytes +FROM golang:1.25.1-trixie@sha256:ff83f3762390c2cccb53618ccc18af23e556aff9b1db4428637e9f63287c8171 AS builder + +# commit timestamp, propagated through the build to clamp file mtimes and normalize embedded dates +ARG SOURCE_DATE_EPOCH +ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH + +WORKDIR /build + +# apt normally resolves to whatever package versions the mirror serves at build time, so two builds days apart install different bytes +# redirect apt at snapshot.debian.org keyed on SOURCE_DATE_EPOCH so every build installs the exact package set that existed at that instant +# NOTE: taken verbatim from https://github.com/reproducible-containers/repro-sources-list.sh/blob/master/alternative/Dockerfile.debian-13 +RUN \ + --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + if [ "${SOURCE_DATE_EPOCH:-0}" -gt 0 ] 2>/dev/null; then \ + : "${SOURCE_DATE_EPOCH:=$(stat --format=%Y /etc/apt/sources.list.d/debian.sources)}" && \ + snapshot="$(/bin/bash -euc "printf \"%(%Y%m%dT%H%M%SZ)T\n\" \"${SOURCE_DATE_EPOCH}\"")" && \ + : "Enabling snapshot" && \ + sed -i -e '/Types: deb/ a\Snapshot: true' /etc/apt/sources.list.d/debian.sources && \ + : "Enabling cache" && \ + rm -f /etc/apt/apt.conf.d/docker-clean && \ + echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache && \ + : "Fetching the snapshot and installing ca-certificates in one command" && \ + apt-get install --update --snapshot "${snapshot}" -o Acquire::Check-Valid-Until=false -o Acquire::https::Verify-Peer=false -y ca-certificates && \ + : "Installing ca-certificates" && \ + apt-get install --snapshot "${snapshot}" -y ca-certificates && \ + : "Clean up for improving reproducibility (optional)" && \ + rm -rf /var/log/* /var/cache/ldconfig/aux-cache; \ + else \ + apt-get update && apt-get install -y ca-certificates && \ + rm -rf /var/log/* /var/cache/ldconfig/aux-cache; \ + fi + +# Build context is the repo root (compose builds from `.`), so module paths are +# prefixed with go/ — the language directories are siblings under that root. +# +# Copy only the module manifest first so `go mod download` is cached independently of source edits. +# tee-node and every other dependency are fetched from the network during download. +COPY --chmod=644 --chown=0:0 go/go.mod go/go.sum ./ +RUN go mod download +RUN go mod verify + +# Copy the module's own source packages (cmd/docker is the node entrypoint). +COPY --chmod=644 --chown=0:0 go/cmd/ ./cmd/ +COPY --chmod=644 --chown=0:0 go/internal/ ./internal/ +COPY --chmod=644 --chown=0:0 go/pkg/ ./pkg/ + +# -trimpath strips build-host paths from the binary +# -buildid= clears go's non-deterministic build id +# -s -w drop symbol and dwarf tables, which contain build-time data +# -buildvcs=false omits embedded vcs metadata +# CGO_ENABLED=0 produces a static binary, avoiding link-time libc variance +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOFLAGS="-buildvcs=false" \ + go build -trimpath -ldflags="-buildid= -s -w" -o /app/extension-tee ./cmd/docker + +# NOTE: buildkit's rewrite-timestamp only clamps mtimes down to SOURCE_DATE_EPOCH (moby/buildkit#3180) +# files older than SOURCE_DATE_EPOCH are left at their original non-deterministic mtime +# touch every path to SOURCE_DATE_EPOCH explicitly so timestamps are normalized in both directions +RUN if [ "${SOURCE_DATE_EPOCH:-0}" -gt 0 ] 2>/dev/null; then find /app -exec touch -h -d @${SOURCE_DATE_EPOCH} {} +; fi + +# empty base image so nothing outside these explicit copies ends up in the final layers +FROM gcr.io/distroless/static + +WORKDIR /app + +# ca-certificates.crt is the tls trust store for outbound https (e.g. flare rpc, oauth) +# re-apply chmod/chown on each COPY so metadata is pinned here and does not depend on whatever the builder stage left behind +COPY --chmod=644 --chown=65532:65532 --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +COPY --chmod=755 --chown=65532:65532 --from=builder /app/extension-tee /app/extension-tee + +# MODE=1 is SIMULATED attestation (local dev). MODE=0 is production attestation. +# Set to 1 here so a bare `docker run` works against the local devnet; compose +# overrides it via MODE=${MODE:-1}. +# +# For a real Confidential Space deploy this MUST be 0 — FTDC rejects simulated +# attestation. See docs/deployment-steps.md §5. Keep this value identical across +# go/, python/ and typescript/ Dockerfiles. +ENV MODE=1 CONFIG_PORT=5501 SIGN_PORT=7701 EXTENSION_PORT=7702 + +# match tee-node: run as root (USER 0:0) — the TEE workload itself is the isolation boundary +USER 0:0 + +# confidential space launch policy label: allow the operator to override these env vars at workload launch +# without this, the confidential space VM rejects overrides at attestation time and the values baked here are final +LABEL "tee.launch_policy.allow_env_override"="LOG_LEVEL,PROXY_URL,INITIAL_OWNER,EXTENSION_ID,CHAIN_URL,MODE,CONFIG_PORT,SIGN_PORT,EXTENSION_PORT" + +EXPOSE 5501 7701 7702 + +CMD ["/app/extension-tee"] diff --git a/go/Dockerfile.dockerignore b/go/Dockerfile.dockerignore new file mode 100644 index 0000000..9528d09 --- /dev/null +++ b/go/Dockerfile.dockerignore @@ -0,0 +1,37 @@ +# Build context for go/Dockerfile is the repo root. BuildKit prefers this file +# over the root .dockerignore because it is named .dockerignore. +# +# Excluding the other language directories matters for more than build speed: +# node_modules and __pycache__ are large, host-dependent trees, and anything in +# the context can perturb layer hashes and undermine reproducibility. +.git +.gitignore +.env +.env.* +*.md +docs/ +testing/ +.superpowers/ +.claude/ +cache/ +out/ +results/ +config/ +contracts/ +scripts/ +proxy/ +tools/ +testdata/ +foundry.toml +docker-compose*.yaml +Dockerfile* +.dockerignore + +# Other language implementations — not part of the Go image. +python/ +typescript/ +**/node_modules/ +**/__pycache__/ +**/*.pyc +**/.venv/ +**/dist/ diff --git a/go/README.md b/go/README.md new file mode 100644 index 0000000..0dc7b54 --- /dev/null +++ b/go/README.md @@ -0,0 +1,45 @@ +# Concord Go FCC implementation + +This is Concord's selected FCE implementation. It embeds tee-node as a +library, so the image runs as a single static binary on a distroless base. The +runtime remains the official Flare FCE scaffold; Concord-specific quote +handling and CoFill live in internal/extension. + +## Layout + +~~~text +cmd/ +├── main.go Standalone extension server for local development +├── docker/main.go Combined tee-node plus extension image entry point +└── start-tee/main.go Host-process runner for local services +internal/ +├── config/config.go Concord operation constants and version +└── extension/ + ├── extension.go Decryption boundary, routing, quote and CoFill handlers + └── utils.go Shared extension helpers +pkg/ +├── server/server.go Extension server lifecycle +└── types/types.go Quote, allocation and state types +~~~ + +## Develop + +~~~bash +cd go +GOTOOLCHAIN=local go build ./... +GOTOOLCHAIN=local go test ./... +~~~ + +## Concord operations + +SUBMIT_QUOTE accepts one signed provider offer after the payload has been +decrypted inside the TEE. FINALIZE_ROUND validates the eligible quote set and +returns the deterministic CoFill allocation. The result digest is later +checked by the explicit onchain allocation-verifier boundary before child +Accords are materialized. + +The handler never treats the encrypted instruction message as plaintext. Unit +tests inject a decrypt function only to exercise the handler without starting +the TEE. + +For the complete contract and tooling checks, see ../docs/testing.md. diff --git a/go/cmd/docker/main.go b/go/cmd/docker/main.go new file mode 100644 index 0000000..8c63a05 --- /dev/null +++ b/go/cmd/docker/main.go @@ -0,0 +1,61 @@ +// Package main provides a combined entry point for Docker that starts both the +// tee-node server and the extension in a single process. Unlike tools/cmd/start-tee, +// this avoids importing extension-e2e — Docker sets PROXY_URL as an env var which +// tee-node reads directly via settings.init(). +package main + +import ( + "os" + "os/signal" + "strconv" + "syscall" + "time" + + "github.com/flare-foundation/go-flare-common/pkg/logger" + teeServer "github.com/flare-foundation/tee-node/pkg/server" + + "concord/internal/config" + extserver "concord/pkg/server" +) + +func main() { + configPort := intEnv("CONFIG_PORT", 5501) + + // config.SignPort and config.ExtensionPort are set from SIGN_PORT and + // EXTENSION_PORT env vars via config.init(). + signPort := config.SignPort + extensionPort := config.ExtensionPort + + // Start tee-node in extension mode. + go teeServer.StartServerExtension(configPort, signPort, extensionPort) + + // Start extension server — fail fast if port binding fails. + extErrCh := extserver.StartExtension(extensionPort, signPort) + + // Give server a moment to bind, then check for early failures. + time.Sleep(100 * time.Millisecond) + select { + case err := <-extErrCh: + logger.Fatalf("extension server failed to start: %v", err) + default: + } + + logger.Infof("extension TEE running (config=%d, sign=%d, ext=%d)", configPort, signPort, extensionPort) + + // Wait for signal or server error. + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + select { + case <-sigChan: + logger.Info("shutting down") + case err := <-extErrCh: + logger.Fatalf("extension server error: %v", err) + } +} + +func intEnv(key string, fallback int) int { + if v, err := strconv.Atoi(os.Getenv(key)); err == nil { + return v + } + return fallback +} diff --git a/go/cmd/main.go b/go/cmd/main.go new file mode 100644 index 0000000..81a390f --- /dev/null +++ b/go/cmd/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "context" + + "net/http" + "os" + "os/signal" + "syscall" + + "github.com/flare-foundation/go-flare-common/pkg/logger" + + "concord/internal/config" + extension "concord/internal/extension" +) + +func main() { + e := extension.New(config.ExtensionPort, config.SignPort) + + // Graceful shutdown. + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + go func() { + <-sigChan + ctx, cancel := context.WithTimeout(context.Background(), config.TimeoutShutdown) + defer cancel() + if err := e.Server.Shutdown(ctx); err != nil { + logger.Errorf("graceful shutdown: %v", err) + } + os.Exit(0) + }() + + logger.Infof("starting extension server on :%d", config.ExtensionPort) + err := e.Server.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + logger.Fatalf("server: %v", err) + } +} diff --git a/go/cmd/start-tee/main.go b/go/cmd/start-tee/main.go new file mode 100644 index 0000000..2668d55 --- /dev/null +++ b/go/cmd/start-tee/main.go @@ -0,0 +1,151 @@ +// Command start-tee runs the tee-node and the Go extension as a single host +// process, without Docker. It backs `./scripts/start-services.sh --local`. +// +// This lives in the extension module (not tools/) because it links the Go +// extension in-process — it is a Go-path development convenience, not +// deployment tooling. tools/ must stay independent of any one language +// implementation so that it can deploy and test all of them; see +// docs/extension-contract.md. +// +// Consequently `--local` mode is Go-only. start-services.sh rejects it for +// other languages and points at Docker Compose instead. +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "net/http" + "os" + "os/signal" + "path/filepath" + "runtime" + "strings" + "syscall" + "time" + + extserver "concord/pkg/server" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/flare-foundation/go-flare-common/pkg/logger" + teeServer "github.com/flare-foundation/tee-node/pkg/server" + teetypes "github.com/flare-foundation/tee-node/pkg/types" + "github.com/joho/godotenv" +) + +// Port constants matching the extension-e2e configs. +const ( + ExtConfigurationPort = 5501 // TEE configuration port (proxyURL, initialOwner, extensionID) + ExtProxyInternalPort = 6663 // Internal port: TEE polls actions from proxy queue + ExtensionServerPort = 7701 // TEE signing port: extension calls TEE for signing/encrypting + ExtensionPort = 7702 // Extension server port: TEE forwards POST /action here +) + +func main() { + extensionID := flag.String("extensionID", "", "extension ID (bytes32 hex)") + flag.Parse() + + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) + + loadEnv() + + _ = setOwnerAddress() + + if *extensionID != "" { + os.Setenv("EXTENSION_ID", *extensionID) + } + + runExtension() + + sig := <-signalChan + logger.Infof("Received %v signal, shutting down", sig) +} + +func loadEnv() { + // Try project-root .env first (works even when CWD is elsewhere). + _, thisFile, _, _ := runtime.Caller(0) + rootEnv := filepath.Join(filepath.Dir(thisFile), "..", "..", ".env") + if err := godotenv.Load(rootEnv); err != nil { + // Fallback to CWD .env. + if err := godotenv.Load(); err != nil { + fmt.Printf("Warning: Error loading .env file: %v\n", err) + } + } +} + +func runExtension() { + // Start tee-node in extension mode. + go teeServer.StartServerExtension(ExtConfigurationPort, ExtensionServerPort, ExtensionPort) + + // Start extension server — fail fast if port binding fails. + extErrCh := extserver.StartExtension(ExtensionPort, ExtensionServerPort) + + // Give server a moment to bind, then check for early failures. + time.Sleep(100 * time.Millisecond) + select { + case err := <-extErrCh: + logger.Fatalf("extension server failed to start: %v", err) + default: + } + + logger.Infof("Starting extension TEE on port %d", ExtConfigurationPort) + + time.Sleep(150 * time.Millisecond) + + err := setProxyURL(ExtConfigurationPort, ExtProxyInternalPort) + if err != nil { + logger.Fatalf("Error: %v", err) + } +} + +// setProxyURL points the freshly started node at the local extension proxy. +// Inlined from tools/pkg/fccutils so this command carries no dependency on the +// tools module. +func setProxyURL(configurationPort, proxyPort int) error { + url := fmt.Sprintf("http://localhost:%d", proxyPort) + request := teetypes.ConfigureProxyURLRequest{URL: &url} + + body, err := json.Marshal(request) + if err != nil { + return err + } + + url = fmt.Sprintf("http://localhost:%d/proxy", configurationPort) + logger.Infof("Setting proxy url on tee: %s", url) + resp, err := http.Post(url, "application/json", bytes.NewReader(body)) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + return nil +} + +func setOwnerAddress() common.Address { + owner := os.Getenv("INITIAL_OWNER") + if owner == "" { + privKeyString := os.Getenv("DEPLOYMENT_PRIVATE_KEY") + if privKeyString == "" { + logger.Fatalf("INITIAL_OWNER or DEPLOYMENT_PRIVATE_KEY must be set") + } + if strings.HasPrefix(privKeyString, "0x") || strings.HasPrefix(privKeyString, "0X") { + privKeyString = privKeyString[2:] + } + privKey, err := crypto.HexToECDSA(privKeyString) + if err != nil { + logger.Fatalf("Error: %v", err) + } + + ownerAddress := crypto.PubkeyToAddress(privKey.PublicKey) + os.Setenv("INITIAL_OWNER", ownerAddress.String()) + } + + return common.HexToAddress(owner) +} diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..99c0efb --- /dev/null +++ b/go/go.mod @@ -0,0 +1,61 @@ +module concord + +go 1.25.1 + +require ( + github.com/ethereum/go-ethereum v1.17.4 + github.com/flare-foundation/go-flare-common v1.2.2-0.20260727094511-09a10067e6a4 + github.com/flare-foundation/tee-node v0.0.24 + github.com/joho/godotenv v1.5.1 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251106012722-c7be33e82a11 // indirect + github.com/bits-and-blooms/bitset v1.24.3 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/consensys/gnark-crypto v0.19.2 // indirect + github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deckarep/golang-set/v2 v2.8.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect + github.com/fjl/jsonw v0.1.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/supranational/blst v0.3.16 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/text v0.36.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/mysql v1.6.0 // indirect + gorm.io/gorm v1.31.0 // indirect +) diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000..9f4cbb0 --- /dev/null +++ b/go/go.sum @@ -0,0 +1,263 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251106012722-c7be33e82a11 h1:cP8UbFCldZ6uVbZnI3/EI4FSdO9NaYnx4hY+tyW6FbU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251106012722-c7be33e82a11/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.24.3 h1:Bte86SlO3lwPQqww+7BE9ZuUCKIjfqnG5jtEyqA9y9Y= +github.com/bits-and-blooms/bitset v1.24.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/gnark-crypto v0.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80= +github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= +github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= +github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= +github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= +github.com/ethereum/go-ethereum v1.17.4 h1:uA4q+qiLp7QImBsjdRbINu8iX6OEVmj4DPc5/E5Fsxc= +github.com/ethereum/go-ethereum v1.17.4/go.mod h1:qMdgwqqRAen+aT8P7KKQKi0Qt6RzG4cfejVAbCpJgqA= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/fjl/jsonw v0.1.0 h1:V3MyR79fjLpn/+bMgvegdGUIhoJOzjmqWcKDgcOmY1I= +github.com/fjl/jsonw v0.1.0/go.mod h1:2KMLevM6FXEJnfhtk7naXu9vZdVfOma1GlnGdPRlumU= +github.com/flare-foundation/go-flare-common v1.2.2-0.20260727094511-09a10067e6a4 h1:B+WFq+c1k1Y1OcJh2jrojcvo4d9lslPNn1ze5IrebbM= +github.com/flare-foundation/go-flare-common v1.2.2-0.20260727094511-09a10067e6a4/go.mod h1:H9I3pwQAyiOK+jC7tFHYRZa5qxDeOldW+5VpRiUs6rA= +github.com/flare-foundation/tee-node v0.0.24 h1:NZxm3EFlSvzkvwKIB61B0P+lSTVkaDQ4lnYzDU07DFE= +github.com/flare-foundation/tee-node v0.0.24/go.mod h1:4BLiHby94bTeiARRXqXFtFPF850KPG0YpvOaOduv4rA= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac= +github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= +github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/stun/v3 v3.1.2 h1:86IhD8wFn6IDW4b1/0QzoQS+f5PeA8OHHRn8UZW5ErY= +github.com/pion/stun/v3 v3.1.2/go.mod h1:H7gDic7nNwlUL05pbs6T1dtaBehh/KjupxfWw3ZI7cA= +github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= +github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= +golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= +gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/go/internal/config/config.go b/go/internal/config/config.go new file mode 100644 index 0000000..c6301bd --- /dev/null +++ b/go/internal/config/config.go @@ -0,0 +1,41 @@ +// Package config contains configuration values and defaults used by the extension. +package config + +import ( + "os" + "strconv" + "time" +) + +const ( + Version = "0.1.0" + + OPTypeConcord = "CONCORD" + OPCommandSubmitQuote = "SUBMIT_QUOTE" + OPCommandFinalizeRound = "FINALIZE_ROUND" + + TimeoutShutdown = 5 * time.Second +) + +// Defaults. +var ( + ExtensionPort = 8080 + SignPort = 9090 +) + +// Environment variables override defaults. +func init() { + ep := os.Getenv("EXTENSION_PORT") + sp := os.Getenv("SIGN_PORT") + + if ep != "" { + if v, err := strconv.Atoi(ep); err == nil { + ExtensionPort = v + } + } + if sp != "" { + if v, err := strconv.Atoi(sp); err == nil { + SignPort = v + } + } +} diff --git a/go/internal/extension/extension.go b/go/internal/extension/extension.go new file mode 100644 index 0000000..c421a7a --- /dev/null +++ b/go/internal/extension/extension.go @@ -0,0 +1,464 @@ +package extension + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "math/big" + "net/http" + "sort" + "strings" + "sync" + "time" + + "concord/internal/config" + "concord/pkg/types" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/flare-foundation/go-flare-common/pkg/tee/instruction" + "github.com/flare-foundation/tee-node/pkg/processorutils" + teetypes "github.com/flare-foundation/tee-node/pkg/types" + teeutils "github.com/flare-foundation/tee-node/pkg/utils" +) + +type Extension struct { + mu sync.RWMutex + Server *http.Server + signPort int + quotes map[string]map[string]types.QuoteRequest + quoteCount int + finalizedRounds int + decrypt func([]byte) ([]byte, error) +} + +func New(extensionPort, signPort int) *Extension { + e := &Extension{ + signPort: signPort, + quotes: make(map[string]map[string]types.QuoteRequest), + } + e.decrypt = e.decryptPayload + + mux := http.NewServeMux() + mux.HandleFunc("GET /state", e.stateHandler) + mux.HandleFunc("POST /action", e.actionHandler) + e.Server = &http.Server{Addr: fmt.Sprintf(":%d", extensionPort), Handler: mux} + return e +} + +func (e *Extension) stateHandler(w http.ResponseWriter, _ *http.Request) { + e.mu.RLock() + state := types.StateResponse{ + StateVersion: teeutils.ToHash(config.Version), + State: types.State{ + QuoteCount: e.quoteCount, + FinalizedRounds: e.finalizedRounds, + }, + } + e.mu.RUnlock() + if err := json.NewEncoder(w).Encode(state); err != nil { + http.Error(w, fmt.Sprintf("sending response: %v", err), http.StatusInternalServerError) + } +} + +func (e *Extension) actionHandler(w http.ResponseWriter, r *http.Request) { + var action teetypes.Action + if err := json.NewDecoder(r.Body).Decode(&action); err != nil { + http.Error(w, fmt.Sprintf("decoding action: %v", err), http.StatusBadRequest) + return + } + status, body := e.processAction(action) + w.WriteHeader(status) + _, _ = w.Write(body) +} + +func (e *Extension) processAction(action teetypes.Action) (int, []byte) { + dataFixed, err := processorutils.Parse[instruction.DataFixed](action.Data.Message) + if err != nil { + return http.StatusBadRequest, []byte(fmt.Sprintf("decoding fixed data: %v", err)) + } + if dataFixed.OPType != teeutils.ToHash(config.OPTypeConcord) { + return http.StatusNotImplemented, []byte(fmt.Sprintf( + "unsupported op type: received %s, expected %s (%s)", + dataFixed.OPType.Hex(), teeutils.ToHash(config.OPTypeConcord).Hex(), config.OPTypeConcord, + )) + } + + decrypted, err := e.decrypt(dataFixed.OriginalMessage) + if err != nil { + return http.StatusOK, marshalActionResult(action, dataFixed, nil, 0, fmt.Errorf("decrypting Concord payload: %w", err)) + } + + switch { + case dataFixed.OPCommand == teeutils.ToHash(config.OPCommandSubmitQuote): + return http.StatusOK, e.processSubmitQuote(action, dataFixed, decrypted) + case dataFixed.OPCommand == teeutils.ToHash(config.OPCommandFinalizeRound): + return http.StatusOK, e.processFinalizeRound(action, dataFixed, decrypted) + default: + return http.StatusNotImplemented, []byte(fmt.Sprintf( + "unsupported op command: received %s, expected one of [%s (%s), %s (%s)]", + dataFixed.OPCommand.Hex(), + teeutils.ToHash(config.OPCommandSubmitQuote).Hex(), config.OPCommandSubmitQuote, + teeutils.ToHash(config.OPCommandFinalizeRound).Hex(), config.OPCommandFinalizeRound, + )) + } +} + +func (e *Extension) processSubmitQuote(action teetypes.Action, df *instruction.DataFixed, payload []byte) []byte { + var req types.QuoteRequest + if err := decodeStrict(payload, &req); err != nil { + return marshalActionResult(action, df, nil, 0, fmt.Errorf("decoding quote: %w", err)) + } + if err := validateQuote(req, uint64(time.Now().Unix()), 0, 0, nil); err != nil { + return marshalActionResult(action, df, nil, 0, err) + } + + digest, err := quoteDigest(req) + if err != nil { + return marshalActionResult(action, df, nil, 0, err) + } + provider := strings.ToLower(common.HexToAddress(req.Provider).Hex()) + e.mu.Lock() + if e.quotes[req.RoundID] == nil { + e.quotes[req.RoundID] = make(map[string]types.QuoteRequest) + } + e.quotes[req.RoundID][provider] = req + e.quoteCount++ + e.mu.Unlock() + + data, _ := json.Marshal(types.SubmitQuoteResponse{Accepted: true, QuoteDigest: digest.Hex()}) + return marshalActionResult(action, df, data, 1, nil) +} + +func (e *Extension) processFinalizeRound(action teetypes.Action, df *instruction.DataFixed, payload []byte) []byte { + var req types.FinalizeRoundRequest + if err := decodeStrict(payload, &req); err != nil { + return marshalActionResult(action, df, nil, 0, fmt.Errorf("decoding finalization: %w", err)) + } + if len(req.Quotes) == 0 { + e.mu.RLock() + for _, quote := range e.quotes[req.RoundID] { + req.Quotes = append(req.Quotes, quote) + } + e.mu.RUnlock() + } + if req.EvaluationTime == 0 { + req.EvaluationTime = uint64(time.Now().Unix()) + } + result, err := CoFill(req) + if err != nil { + return marshalActionResult(action, df, nil, 0, err) + } + data, _ := json.Marshal(result) + e.mu.Lock() + e.finalizedRounds++ + e.mu.Unlock() + return marshalActionResult(action, df, data, 1, nil) +} + +func (e *Extension) decryptPayload(encrypted []byte) ([]byte, error) { + if len(encrypted) == 0 { + return nil, fmt.Errorf("encrypted payload is empty") + } + reqBody, err := json.Marshal(struct { + EncryptedMessage []byte `json:"encryptedMessage"` + }{EncryptedMessage: encrypted}) + if err != nil { + return nil, err + } + client := &http.Client{Timeout: 15 * time.Second} + url := fmt.Sprintf("http://localhost:%d/decrypt", e.signPort) + resp, err := client.Post(url, "application/json", bytes.NewReader(reqBody)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("TEE decrypt endpoint returned HTTP %d", resp.StatusCode) + } + var decoded struct { + DecryptedMessage []byte `json:"decryptedMessage"` + } + if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil { + return nil, err + } + if len(decoded.DecryptedMessage) == 0 { + return nil, fmt.Errorf("TEE returned an empty decrypted payload") + } + return decoded.DecryptedMessage, nil +} + +func decodeStrict(data []byte, target any) error { + dec := json.NewDecoder(bytes.NewReader(data)) + dec.DisallowUnknownFields() + if err := dec.Decode(target); err != nil { + return err + } + var extra any + if err := dec.Decode(&extra); err != io.EOF { + if err == nil { + return fmt.Errorf("payload contains trailing JSON") + } + return fmt.Errorf("invalid trailing payload: %w", err) + } + return nil +} + +func marshalActionResult(action teetypes.Action, df *instruction.DataFixed, data []byte, status uint8, err error) []byte { + result := buildResult(action, df, data, status, err) + encoded, _ := json.Marshal(result) + return encoded +} + +func buildResult(a teetypes.Action, df *instruction.DataFixed, data []byte, status uint8, err error) teetypes.ActionResult { + result := teetypes.ActionResult{ + ID: a.Data.ID, + SubmissionTag: a.Data.SubmissionTag, + Version: config.Version, + OPType: df.OPType, + OPCommand: df.OPCommand, + Data: data, + Status: status, + } + if status == 0 { + result.Log = fmt.Sprintf("error: %v", err) + } else if status == 1 { + result.Log = "ok" + } else { + result.Log = "pending" + } + return result +} + +func validBytes32(value string) bool { + return strings.HasPrefix(value, "0x") && len(value) == 66 && common.HexToHash(value) != (common.Hash{}) +} + +func validateQuote(q types.QuoteRequest, now uint64, maxFee uint32, roundExpiry uint64, eligible map[string]bool) error { + if !validBytes32(q.RoundID) || !validBytes32(q.RootAccordID) { + return fmt.Errorf("roundId and rootAccordId must be non-zero bytes32 values") + } + if !common.IsHexAddress(q.Provider) || common.HexToAddress(q.Provider) == (common.Address{}) { + return fmt.Errorf("provider must be a non-zero EVM address") + } + capacity, err := parseUint256(q.Capacity) + if err != nil || capacity.Sign() <= 0 { + return fmt.Errorf("capacity must be a positive uint256") + } + nonce, err := parseUint256(q.Nonce) + if err != nil || nonce.Sign() < 0 { + return fmt.Errorf("nonce must be a uint256") + } + if q.ValidUntil <= now { + return fmt.Errorf("quote is expired") + } + if maxFee != 0 && q.FeeBps > maxFee { + return fmt.Errorf("quote fee exceeds maxFeeBps") + } + if roundExpiry != 0 && q.ValidUntil > roundExpiry { + return fmt.Errorf("quote validity exceeds round expiry") + } + if eligible != nil && !eligible[strings.ToLower(common.HexToAddress(q.Provider).Hex())] { + return fmt.Errorf("provider is not eligible for the round") + } + if _, err := verifyQuoteSignature(q); err != nil { + return err + } + return nil +} + +func parseUint256(value string) (*big.Int, error) { + if value == "" || strings.HasPrefix(value, "-") { + return nil, fmt.Errorf("empty or negative uint256") + } + n := new(big.Int) + if _, ok := n.SetString(value, 10); !ok || n.Sign() < 0 || n.BitLen() > 256 { + return nil, fmt.Errorf("invalid uint256") + } + return n, nil +} + +var quoteABI = func() abi.Arguments { + bytes32Type, _ := abi.NewType("bytes32", "", nil) + addressType, _ := abi.NewType("address", "", nil) + uint256Type, _ := abi.NewType("uint256", "", nil) + uint32Type, _ := abi.NewType("uint32", "", nil) + uint64Type, _ := abi.NewType("uint64", "", nil) + return abi.Arguments{ + {Type: bytes32Type}, {Type: bytes32Type}, {Type: addressType}, + {Type: uint256Type}, {Type: uint32Type}, {Type: uint64Type}, {Type: uint256Type}, + } +}() + +func quotePacked(q types.QuoteRequest) ([]byte, error) { + capacity, err := parseUint256(q.Capacity) + if err != nil { + return nil, err + } + nonce, err := parseUint256(q.Nonce) + if err != nil { + return nil, err + } + return quoteABI.Pack( + common.HexToHash(q.RoundID), common.HexToHash(q.RootAccordID), common.HexToAddress(q.Provider), + capacity, q.FeeBps, q.ValidUntil, nonce, + ) +} + +func quoteDigest(q types.QuoteRequest) (common.Hash, error) { + packed, err := quotePacked(q) + if err != nil { + return common.Hash{}, err + } + sig, err := hexutil.Decode(q.ProviderSignature) + if err != nil { + return common.Hash{}, fmt.Errorf("invalid provider signature: %w", err) + } + return crypto.Keccak256Hash(append(packed, sig...)), nil +} + +func verifyQuoteSignature(q types.QuoteRequest) (common.Address, error) { + packed, err := quotePacked(q) + if err != nil { + return common.Address{}, err + } + sig, err := hexutil.Decode(q.ProviderSignature) + if err != nil || len(sig) != 65 { + return common.Address{}, fmt.Errorf("providerSignature must be a 65-byte hex signature") + } + if sig[64] >= 27 { + sig[64] -= 27 + } + if sig[64] > 1 { + return common.Address{}, fmt.Errorf("providerSignature has invalid recovery id") + } + digest := accounts.TextHash(crypto.Keccak256(packed)) + pub, err := crypto.SigToPub(digest, sig) + if err != nil { + return common.Address{}, fmt.Errorf("providerSignature recovery failed: %w", err) + } + recovered := crypto.PubkeyToAddress(*pub) + if !strings.EqualFold(recovered.Hex(), common.HexToAddress(q.Provider).Hex()) { + return common.Address{}, fmt.Errorf("providerSignature does not recover provider") + } + return recovered, nil +} + +// CoFill validates quotes and deterministically fills the facility target. +// Ordering is lowest fee, then provider address, then nonce. The final quote +// may be partially allocated. Identical inputs produce identical output. +func CoFill(req types.FinalizeRoundRequest) (types.FinalizeRoundResponse, error) { + if !validBytes32(req.ExtensionID) { + return types.FinalizeRoundResponse{}, fmt.Errorf("extensionId must be a non-zero bytes32 hex value") + } + if !validBytes32(req.RoundID) || !validBytes32(req.RootAccordID) { + return types.FinalizeRoundResponse{}, fmt.Errorf("roundId and rootAccordId must be non-zero bytes32 values") + } + target, err := parseUint256(req.TargetCapacity) + if err != nil || target.Sign() <= 0 { + return types.FinalizeRoundResponse{}, fmt.Errorf("targetCapacity must be a positive uint256") + } + if req.MaxFeeBps > 10_000 || req.RoundExpiry <= req.EvaluationTime { + return types.FinalizeRoundResponse{}, fmt.Errorf("invalid round bounds") + } + eligible := make(map[string]bool, len(req.EligibleProviders)) + for _, provider := range req.EligibleProviders { + if !common.IsHexAddress(provider) || common.HexToAddress(provider) == (common.Address{}) { + return types.FinalizeRoundResponse{}, fmt.Errorf("eligible provider is invalid") + } + eligible[strings.ToLower(common.HexToAddress(provider).Hex())] = true + } + quotes := append([]types.QuoteRequest(nil), req.Quotes...) + for i := range quotes { + if err := validateQuote(quotes[i], req.EvaluationTime, req.MaxFeeBps, req.RoundExpiry, eligible); err != nil { + return types.FinalizeRoundResponse{}, fmt.Errorf("quote %d rejected: %w", i, err) + } + if !strings.EqualFold(quotes[i].RoundID, req.RoundID) || !strings.EqualFold(quotes[i].RootAccordID, req.RootAccordID) { + return types.FinalizeRoundResponse{}, fmt.Errorf("quote %d is bound to a different round or root", i) + } + } + sort.Slice(quotes, func(i, j int) bool { + if quotes[i].FeeBps != quotes[j].FeeBps { + return quotes[i].FeeBps < quotes[j].FeeBps + } + left := common.HexToAddress(quotes[i].Provider) + right := common.HexToAddress(quotes[j].Provider) + if bytes.Compare(left.Bytes(), right.Bytes()) != 0 { + return bytes.Compare(left.Bytes(), right.Bytes()) < 0 + } + ln, _ := parseUint256(quotes[i].Nonce) + rn, _ := parseUint256(quotes[j].Nonce) + return ln.Cmp(rn) < 0 + }) + + result := types.FinalizeRoundResponse{ + ExtensionID: req.ExtensionID, + RoundID: req.RoundID, + RootAccordID: req.RootAccordID, + RoundExpiry: req.RoundExpiry, + SelectedProviders: make([]string, 0, len(quotes)), + AllocatedCapacity: make([]string, 0, len(quotes)), + AcceptedFeeBps: make([]uint32, 0, len(quotes)), + TermsCommitments: make([]string, 0, len(quotes)), + } + remaining := new(big.Int).Set(target) + seen := make(map[string]bool) + for _, quote := range quotes { + provider := strings.ToLower(common.HexToAddress(quote.Provider).Hex()) + if seen[provider] { + return types.FinalizeRoundResponse{}, fmt.Errorf("duplicate provider quote") + } + seen[provider] = true + capacity, _ := parseUint256(quote.Capacity) + if capacity.Sign() == 0 || remaining.Sign() == 0 { + continue + } + allocated := new(big.Int).Set(capacity) + if allocated.Cmp(remaining) > 0 { + allocated.Set(remaining) + } + qdigest, err := quoteDigest(quote) + if err != nil { + return types.FinalizeRoundResponse{}, err + } + result.SelectedProviders = append(result.SelectedProviders, provider) + result.AllocatedCapacity = append(result.AllocatedCapacity, allocated.String()) + result.AcceptedFeeBps = append(result.AcceptedFeeBps, quote.FeeBps) + result.TermsCommitments = append(result.TermsCommitments, qdigest.Hex()) + remaining.Sub(remaining, allocated) + } + if remaining.Sign() != 0 { + return types.FinalizeRoundResponse{}, fmt.Errorf("insufficient eligible capacity: short %s", remaining.String()) + } + result.Success = true + result.ResultDigest = allocationResultDigest(result).Hex() + return result, nil +} + +func allocationResultDigest(result types.FinalizeRoundResponse) common.Hash { + var encoded bytes.Buffer + encoded.Write(common.HexToHash(result.ExtensionID).Bytes()) + encoded.Write(common.HexToHash(result.RoundID).Bytes()) + encoded.Write(common.HexToHash(result.RootAccordID).Bytes()) + encoded.WriteByte(1) + var expiry [8]byte + binary.BigEndian.PutUint64(expiry[:], result.RoundExpiry) + encoded.Write(expiry[:]) + for i, provider := range result.SelectedProviders { + encoded.Write(common.HexToAddress(provider).Bytes()) + amount, _ := parseUint256(result.AllocatedCapacity[i]) + amountBytes := make([]byte, 32) + amount.FillBytes(amountBytes) + encoded.Write(amountBytes) + var fee [4]byte + binary.BigEndian.PutUint32(fee[:], result.AcceptedFeeBps[i]) + encoded.Write(fee[:]) + encoded.Write(common.HexToHash(result.TermsCommitments[i]).Bytes()) + } + return crypto.Keccak256Hash(encoded.Bytes()) +} diff --git a/go/internal/extension/extension_test.go b/go/internal/extension/extension_test.go new file mode 100644 index 0000000..7a8b9e9 --- /dev/null +++ b/go/internal/extension/extension_test.go @@ -0,0 +1,127 @@ +package extension + +import ( + "crypto/ecdsa" + "encoding/hex" + "encoding/json" + "strings" + "testing" + "time" + + "concord/internal/config" + "concord/pkg/types" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/flare-foundation/go-flare-common/pkg/tee/instruction" + teetypes "github.com/flare-foundation/tee-node/pkg/types" + teeutils "github.com/flare-foundation/tee-node/pkg/utils" +) + +func signQuote(t *testing.T, key *ecdsa.PrivateKey, quote types.QuoteRequest) string { + t.Helper() + packed, err := quotePacked(quote) + if err != nil { + t.Fatal(err) + } + sig, err := crypto.Sign(accounts.TextHash(crypto.Keccak256(packed)), key) + if err != nil { + t.Fatal(err) + } + return "0x" + hex.EncodeToString(sig) +} + +func quoteFixture(t *testing.T, key *ecdsa.PrivateKey, provider string, fee uint32, capacity string) types.QuoteRequest { + t.Helper() + q := types.QuoteRequest{ + RoundID: "0x0100000000000000000000000000000000000000000000000000000000000000", + RootAccordID: "0x0200000000000000000000000000000000000000000000000000000000000000", + Provider: provider, + Capacity: capacity, + FeeBps: fee, + ValidUntil: uint64(time.Now().Unix()) + 3600, + Nonce: "1", + } + q.ProviderSignature = signQuote(t, key, q) + return q +} + +func buildAction(opCommand string, payload []byte) teetypes.Action { + data := instruction.DataFixed{ + OPType: teeutils.ToHash(config.OPTypeConcord), + OPCommand: teeutils.ToHash(opCommand), + OriginalMessage: payload, + } + b, _ := json.Marshal(data) + return teetypes.Action{Data: teetypes.ActionData{ID: common.HexToHash("0x1234"), Message: b}} +} + +func TestCoFillDeterministicPartialAllocation(t *testing.T) { + keys := make([]*ecdsa.PrivateKey, 3) + quotes := make([]types.QuoteRequest, 3) + providers := make([]string, 3) + for i := range keys { + keys[i], _ = crypto.GenerateKey() + providers[i] = crypto.PubkeyToAddress(keys[i].PublicKey).Hex() + } + quotes[0] = quoteFixture(t, keys[0], providers[0], 640, "450") + quotes[1] = quoteFixture(t, keys[1], providers[1], 610, "250") + quotes[2] = quoteFixture(t, keys[2], providers[2], 680, "600") + req := types.FinalizeRoundRequest{ + ExtensionID: "0x0300000000000000000000000000000000000000000000000000000000000000", + RoundID: quotes[0].RoundID, + RootAccordID: quotes[0].RootAccordID, + TargetCapacity: "1000", + MaxFeeBps: 700, + RoundExpiry: uint64(time.Now().Unix()) + 7200, + EvaluationTime: uint64(time.Now().Unix()), + EligibleProviders: providers, + Quotes: quotes, + } + first, err := CoFill(req) + if err != nil { + t.Fatal(err) + } + second, err := CoFill(req) + if err != nil { + t.Fatal(err) + } + if first.ResultDigest != second.ResultDigest || first.AllocatedCapacity[0] != "250" || first.AllocatedCapacity[1] != "450" || first.AllocatedCapacity[2] != "300" { + t.Fatalf("CoFill was not deterministic or partial allocation was wrong: %+v / %+v", first, second) + } +} + +func TestCoFillRejectsInvalidSignatureAndInsufficientCapacity(t *testing.T) { + key, _ := crypto.GenerateKey() + provider := crypto.PubkeyToAddress(key.PublicKey).Hex() + quote := quoteFixture(t, key, provider, 600, "100") + quote.ProviderSignature = "0x" + strings.Repeat("00", 65) + req := types.FinalizeRoundRequest{ + ExtensionID: "0x0300000000000000000000000000000000000000000000000000000000000000", + RoundID: quote.RoundID, + RootAccordID: quote.RootAccordID, + TargetCapacity: "1000", + MaxFeeBps: 700, + RoundExpiry: uint64(time.Now().Unix()) + 7200, + EvaluationTime: uint64(time.Now().Unix()), + EligibleProviders: []string{provider}, + Quotes: []types.QuoteRequest{quote}, + } + if _, err := CoFill(req); err == nil { + t.Fatal("invalid signature accepted") + } + quote.ProviderSignature = signQuote(t, key, quote) + req.Quotes[0] = quote + if _, err := CoFill(req); err == nil { + t.Fatal("insufficient capacity accepted") + } +} + +func TestActionRequiresTEEDecryption(t *testing.T) { + e := New(0, 1) + status, body := e.processAction(buildAction(config.OPCommandSubmitQuote, []byte("not-plaintext"))) + if status != 200 || !strings.Contains(string(body), "decrypting Concord payload") { + t.Fatalf("expected encrypted payload boundary, got status=%d body=%s", status, body) + } +} diff --git a/go/internal/extension/utils.go b/go/internal/extension/utils.go new file mode 100644 index 0000000..6d77d8d --- /dev/null +++ b/go/internal/extension/utils.go @@ -0,0 +1,5 @@ +package extension + +// Concord's action routing and result envelope live with the operation +// handler in extension.go. This file is retained as the scaffold extension +// utility boundary for future FCC-specific helpers. diff --git a/go/language.env b/go/language.env new file mode 100644 index 0000000..a55cad9 --- /dev/null +++ b/go/language.env @@ -0,0 +1,27 @@ +# Language manifest — presence of this file is what marks a directory as an +# extension implementation. Scripts glob */language.env to discover languages, +# so a directory that merely happens to contain a Dockerfile (like proxy/) is +# never mistaken for one. +# +# Sourced by scripts/lib/language.sh. Keep it to simple KEY=value assignments. +# All commands run with this directory as the working directory. + +# Human-readable name, used in log output. +LANGUAGE_NAME="Go" + +# Dockerfile path, relative to this directory. +LANGUAGE_DOCKERFILE="Dockerfile" + +# Install dependencies. Empty when the toolchain handles it implicitly. +LANGUAGE_SETUP_CMD="" + +# Compile. Empty when the run command compiles on the fly. +LANGUAGE_BUILD_CMD="" + +# Run the unit test suite. +LANGUAGE_TEST_CMD="go test ./..." + +# Start the extension HTTP server in the foreground, reading EXTENSION_PORT and +# SIGN_PORT from the environment. Used by scripts/test-conformance.sh, which +# exercises the extension alone — no tee-node, no proxy, no chain. +LANGUAGE_RUN_CMD="go run ./cmd" diff --git a/go/pkg/server/server.go b/go/pkg/server/server.go new file mode 100644 index 0000000..e14074f --- /dev/null +++ b/go/pkg/server/server.go @@ -0,0 +1,16 @@ +package server + +import extension "concord/internal/extension" + +// StartExtension creates and starts the template extension server in a goroutine. +// Returns an error channel that receives any ListenAndServe failure (e.g., port already in use). +func StartExtension(extensionPort, signPort int) <-chan error { + e := extension.New(extensionPort, signPort) + errCh := make(chan error, 1) + go func() { + if err := e.Server.ListenAndServe(); err != nil { + errCh <- err + } + }() + return errCh +} diff --git a/go/pkg/types/types.go b/go/pkg/types/types.go new file mode 100644 index 0000000..7d8de57 --- /dev/null +++ b/go/pkg/types/types.go @@ -0,0 +1,64 @@ +// Package types contains the wire types shared by Concord's FCC extension and +// external integration tools. Amounts are decimal strings so uint256 values +// are not truncated by JSON number implementations. +package types + +import "github.com/ethereum/go-ethereum/common" + +type QuoteRequest struct { + RoundID string `json:"roundId"` + RootAccordID string `json:"rootAccordId"` + Provider string `json:"provider"` + Capacity string `json:"capacity"` + FeeBps uint32 `json:"feeBps"` + ValidUntil uint64 `json:"validUntil"` + Nonce string `json:"nonce"` + ProviderSignature string `json:"providerSignature"` +} + +type SubmitQuoteResponse struct { + Accepted bool `json:"accepted"` + QuoteDigest string `json:"quoteDigest"` +} + +type FinalizeRoundRequest struct { + ExtensionID string `json:"extensionId"` + RoundID string `json:"roundId"` + RootAccordID string `json:"rootAccordId"` + TargetCapacity string `json:"targetCapacity"` + MaxFeeBps uint32 `json:"maxFeeBps"` + RoundExpiry uint64 `json:"roundExpiry"` + EvaluationTime uint64 `json:"evaluationTime"` + EligibleProviders []string `json:"eligibleProviders"` + Quotes []QuoteRequest `json:"quotes"` +} + +type Allocation struct { + Provider string `json:"provider"` + AllocatedCapacity string `json:"allocatedCapacity"` + AcceptedFeeBps uint32 `json:"acceptedFeeBps"` + TermsCommitment string `json:"termsCommitment"` +} + +type FinalizeRoundResponse struct { + Success bool `json:"success"` + ExtensionID string `json:"extensionId"` + RoundID string `json:"roundId"` + RootAccordID string `json:"rootAccordId"` + SelectedProviders []string `json:"selectedProviders"` + AllocatedCapacity []string `json:"allocatedCapacity"` + AcceptedFeeBps []uint32 `json:"acceptedFeeBps"` + TermsCommitments []string `json:"termsCommitments"` + RoundExpiry uint64 `json:"roundExpiry"` + ResultDigest string `json:"resultDigest"` +} + +type State struct { + QuoteCount int `json:"quoteCount"` + FinalizedRounds int `json:"finalizedRounds"` +} + +type StateResponse struct { + StateVersion common.Hash `json:"stateVersion"` + State State `json:"state"` +} diff --git a/proxy/Dockerfile b/proxy/Dockerfile new file mode 100644 index 0000000..bd92739 --- /dev/null +++ b/proxy/Dockerfile @@ -0,0 +1,46 @@ +# Self-contained build of tee-proxy. Clones tee-proxy from GitHub at build time +# so no local sibling repos are needed. +# +# TEE_PROXY_VERSION MUST stay aligned with the extension's Go-module pins +# (tools/go.mod): tee-proxy v0.0.18. tee-proxy's own go.mod transitively pins +# the matching tee-node and go-flare-common line, so building it with +# `go mod download` pulls the exact governance-aware node code the extension-tee +# image uses — no separate tee-node clone or replace directive is needed. +# Override via build arg when bumping both sides together. + +ARG TEE_PROXY_VERSION=v0.0.18 + +# NOTE: not pinned by digest (unlike the main Dockerfile) — this is a dev convenience build. +# For full reproducibility, pin by digest: golang:1.25.1-alpine@sha256:... +FROM golang:1.25.1-alpine AS builder + +RUN apk add --no-cache git + +ARG TEE_PROXY_VERSION + +WORKDIR /app +RUN git clone --depth 1 --branch ${TEE_PROXY_VERSION} https://github.com/flare-foundation/tee-proxy.git + +WORKDIR /app/tee-proxy +RUN go mod download +RUN CGO_ENABLED=0 GOOS=linux go build -a -o main ./cmd/proxy + +# Final stage +FROM alpine:3.21 + +WORKDIR /app + +COPY --from=builder /app/tee-proxy/main . +COPY --from=builder /app/tee-proxy/config.example.toml ./config/config.toml + +RUN addgroup -g 1001 -S appgroup && \ + adduser -u 1001 -S appuser -G appgroup + +RUN chown -R appuser:appgroup /app + +USER appuser + +EXPOSE 6661 +EXPOSE 6662 + +CMD ["./main"] diff --git a/script/DeployCoston2.s.sol b/script/DeployCoston2.s.sol new file mode 100644 index 0000000..2b3da68 --- /dev/null +++ b/script/DeployCoston2.s.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import {AccordRegistry} from "../contracts/AccordRegistry.sol"; +import {CapitalFacility} from "../contracts/CapitalFacility.sol"; +import {ConcordInstructionSender} from "../contracts/ConcordInstructionSender.sol"; +import {ITeeExtensionRegistry} from "../contracts/interfaces/ITeeExtensionRegistry.sol"; +import {ITeeMachineRegistry} from "../contracts/interfaces/ITeeMachineRegistry.sol"; + +interface Vm { + function envAddress(string calldata name) external returns (address); + function envBytes32(string calldata name) external returns (bytes32); + function startBroadcast(address broadcaster) external; + function stopBroadcast() external; +} + +/// @notice Minimal, dependency-free Foundry deployment entry point. +/// Mutable Flare deployment addresses are supplied by the environment. +contract DeployCoston2 { + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + event ConcordDeployed( + address indexed accordRegistry, + address indexed capitalFacility, + address instructionSender, + address collateralAsset, + address liquidityAsset, + bytes32 extensionId + ); + + function run() external { + address owner = vm.envAddress("DEPLOYMENT_OWNER"); + address fxrp = vm.envAddress("FXRP_TOKEN"); + address usdt0 = vm.envAddress("USDT0_TOKEN"); + address verifier = vm.envAddress("ALLOCATION_VERIFIER"); + address teeExtensionRegistry = vm.envAddress("TEE_EXTENSION_REGISTRY"); + address teeMachineRegistry = vm.envAddress("TEE_MACHINE_REGISTRY"); + bytes32 extensionId = vm.envBytes32("CONCORD_EXTENSION_ID"); + + vm.startBroadcast(owner); + AccordRegistry registry = new AccordRegistry(owner); + CapitalFacility facility = new CapitalFacility( + registry, + fxrp, + usdt0, + verifier, + extensionId + ); + registry.setFacility(address(facility)); + ConcordInstructionSender sender = new ConcordInstructionSender( + ITeeExtensionRegistry(teeExtensionRegistry), + ITeeMachineRegistry(teeMachineRegistry) + ); + vm.stopBroadcast(); + + emit ConcordDeployed( + address(registry), + address(facility), + address(sender), + fxrp, + usdt0, + extensionId + ); + } +} diff --git a/scripts/check-docs.sh b/scripts/check-docs.sh new file mode 100755 index 0000000..27b85e5 --- /dev/null +++ b/scripts/check-docs.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# check-docs.sh — validate the shared docs standard (see docs/README.md). +# +# Checks presence, that the platform-wide traps are actually covered, and that +# nothing has grown too long to be read by a tester. Content checks match on +# keywords rather than headings so rewording does not break them. +# +# Keep this file identical across extensions; the only per-repo difference is +# ONE_SHOT_SETTER below. +# +# Exit 1 on a missing/incomplete required doc; long docs only warn. +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +DOCS="$(cd "$SCRIPT_DIR/.." && pwd)/docs" + +RED='\033[0;31m'; YELLOW='\033[0;33m'; GREEN='\033[0;32m'; NC='\033[0m' +fail=0 +err() { echo -e "${RED}FAIL${NC} $*"; fail=1; } +warn() { echo -e "${YELLOW}WARN${NC} $*"; } +ok() { echo -e "${GREEN}ok${NC} $*"; } + +# The one-shot binding differs per extension: shielded-transfer binds the vault's +# teeAuthority, orderbook binds the InstructionSender's teeAddress. +ONE_SHOT_SETTER="setExtensionId" + +REQUIRED="getting-started.md deployment-steps.md testing.md testing-against-coston2.md architecture.md cloudflared.md" +SCAFFOLD="extension-guide.md instruction-sender.md manual-setup.md types-server.md" +MAX_LINES=400 # past this a tester stops reading; split or cut + +echo "docs: $DOCS" +echo + +for f in $REQUIRED; do + p="$DOCS/$f" + if [[ ! -f "$p" ]]; then err "$f missing (required)"; continue; fi + n=$(wc -l <"$p") + # 20 lines is about the floor for a real doc; below that it is a stub. + if (( n < 20 )); then err "$f is only $n lines — stub?"; continue; fi + (( n > MAX_LINES )) && warn "$f is $n lines (>$MAX_LINES) — trim or split" + ok "$f ($n lines)" +done + +echo +for f in $SCAFFOLD; do + [[ -f "$DOCS/$f" ]] && ok "$f (scaffold)" || warn "$f missing — scaffold doc, expected" +done + +# Platform traps every extension hits. Absent = the doc will not save anyone. +echo +D="$DOCS/deployment-steps.md" +if [[ -f "$D" ]]; then + check() { grep -qiF "$2" "$D" && ok "deployment-steps: $1" || err "deployment-steps: missing $1 (looked for '$2')"; } + check "stale-machine pausing" "pause(" + check "active-machine listing" "getActiveTeeMachines" + check "one-shot binding warning" "$ONE_SHOT_SETTER" + check "launch-policy env override" "allow_env_override" + check "digest-pinned deploy" "digest" + check "attestation mode" "SIMULATED_TEE" +fi + +echo +if [[ -f "$DOCS/README.md" ]]; then ok "docs/README.md index present"; else err "docs/README.md index missing"; fi + +echo +if (( fail )); then echo -e "${RED}docs standard: FAILED${NC}"; else echo -e "${GREEN}docs standard: OK${NC}"; fi +exit $fail diff --git a/scripts/check-versions.sh b/scripts/check-versions.sh new file mode 100755 index 0000000..ae8b28a --- /dev/null +++ b/scripts/check-versions.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# +# check-versions.sh — fail fast when dependency pins drift apart. +# +# The Go image pins tee-node via go.mod; non-Go images build tee-node from +# source at TEE_NODE_REF. If those diverge, the node and proxy disagree on +# signature formats and the symptom is an opaque "signature check fail" at +# runtime, long after the build. Same for tee-proxy between tools/go.mod and +# proxy/Dockerfile. +# +# Run standalone, or let pre-build.sh invoke it as a pre-flight. +# +# Usage: ./scripts/check-versions.sh +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; NC='\033[0m' +log() { echo -e "${GREEN}[check-versions]${NC} $*"; } +warn() { echo -e "${YELLOW}[check-versions] WARN:${NC} $*" >&2; } +die() { echo -e "${RED}[check-versions] ERROR:${NC} $*" >&2; exit 1; } + +# shellcheck source=lib/versions.sh +source "$SCRIPT_DIR/lib/versions.sh" +load_versions "$PROJECT_DIR" || die "could not load versions" + +# Locate the two go.mod files (extension module may be at ./ or ./go/). +if [[ -f "$PROJECT_DIR/go/go.mod" ]]; then + EXT_GOMOD="$PROJECT_DIR/go/go.mod" +else + EXT_GOMOD="$PROJECT_DIR/go.mod" +fi +TOOLS_GOMOD="$PROJECT_DIR/tools/go.mod" + +pin() { grep -E "^[[:space:]]*${2//\//\\/}[[:space:]]+v" "$1" 2>/dev/null | head -1 | awk '{print $2}'; } + +FAILED=0 + +# --- 1. tee-node must match between the extension module and tools --- +EXT_NODE="$(pin "$EXT_GOMOD" github.com/flare-foundation/tee-node)" +TOOLS_NODE="$(pin "$TOOLS_GOMOD" github.com/flare-foundation/tee-node)" +if [[ -z "$EXT_NODE" ]]; then + die "no tee-node pin in $EXT_GOMOD" +elif [[ -z "$TOOLS_NODE" ]]; then + warn "no tee-node pin in $TOOLS_GOMOD — skipping cross-check" +elif [[ "$EXT_NODE" != "$TOOLS_NODE" ]]; then + echo -e "${RED} tee-node mismatch:${NC}" >&2 + echo " $EXT_GOMOD: $EXT_NODE" >&2 + echo " $TOOLS_GOMOD: $TOOLS_NODE" >&2 + FAILED=1 +else + log "tee-node $EXT_NODE (extension == tools)" +fi + +# --- 1b. tee-node must not sit below the platform minimum --- +# sort -V puts the lower version first. ponytail: misreads a pseudo-version of +# the floor tag itself; not worth a real semver parser for one comparison. +TEE_NODE_MIN="v0.0.22" +if [[ -n "$EXT_NODE" && "$(printf '%s\n%s\n' "$TEE_NODE_MIN" "$EXT_NODE" | sort -V | head -1)" != "$TEE_NODE_MIN" ]]; then + echo -e "${RED} tee-node $EXT_NODE is below the $TEE_NODE_MIN minimum${NC}" >&2 + echo " bump the pin in $EXT_GOMOD and $TOOLS_GOMOD" >&2 + FAILED=1 +fi + +# --- 2. go-flare-common must match too (drift here breaks ABI encoding) --- +EXT_COMMON="$(pin "$EXT_GOMOD" github.com/flare-foundation/go-flare-common)" +TOOLS_COMMON="$(pin "$TOOLS_GOMOD" github.com/flare-foundation/go-flare-common)" +if [[ -n "$EXT_COMMON" && -n "$TOOLS_COMMON" && "$EXT_COMMON" != "$TOOLS_COMMON" ]]; then + echo -e "${RED} go-flare-common mismatch:${NC}" >&2 + echo " $EXT_GOMOD: $EXT_COMMON" >&2 + echo " $TOOLS_GOMOD: $TOOLS_COMMON" >&2 + FAILED=1 +elif [[ -n "$EXT_COMMON" ]]; then + log "go-flare-common $EXT_COMMON (extension == tools)" +fi + +# --- 3. tee-proxy: tools/go.mod must match proxy/Dockerfile's build ARG --- +TOOLS_PROXY="$(pin "$TOOLS_GOMOD" github.com/flare-foundation/tee-proxy)" +if [[ -z "$TEE_PROXY_VERSION" ]]; then + warn "no ARG TEE_PROXY_VERSION in proxy/Dockerfile — skipping cross-check" +elif [[ -z "$TOOLS_PROXY" ]]; then + warn "no tee-proxy pin in $TOOLS_GOMOD — skipping cross-check" +elif [[ "$TOOLS_PROXY" != "$TEE_PROXY_VERSION" ]]; then + echo -e "${RED} tee-proxy mismatch:${NC}" >&2 + echo " $TOOLS_GOMOD: $TOOLS_PROXY" >&2 + echo " proxy/Dockerfile: $TEE_PROXY_VERSION" >&2 + FAILED=1 +else + log "tee-proxy $TOOLS_PROXY (tools == proxy/Dockerfile)" +fi + +# --- 4. Report the ref that language images will clone --- +log "TEE_NODE_REF $TEE_NODE_REF $([[ "$TEE_NODE_REF" != "$TEE_NODE_VERSION" ]] && echo '(commit SHA from pseudo-version)' || echo '(tag)')" + +if [[ "$FAILED" -ne 0 ]]; then + echo "" >&2 + die "version pins are inconsistent. Align them before building, or the Go and non-Go images will run different tee-node builds." +fi + +log "all version pins consistent" diff --git a/scripts/deploy-coston2.sh b/scripts/deploy-coston2.sh new file mode 100755 index 0000000..0141079 --- /dev/null +++ b/scripts/deploy-coston2.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Deploy Concord's Solidity relationship and facility contracts to Coston2. +# This only prepares and broadcasts deployment transactions; it does not claim +# that FCC registration or a live facility flow has completed. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +CONFIG_FILE="${CONCORD_NETWORK_CONFIG:-$PROJECT_DIR/config/networks/coston2.json}" + +die() { echo "deploy-coston2: $*" >&2; exit 1; } +command -v forge >/dev/null 2>&1 || die "forge (Foundry) is required" +command -v jq >/dev/null 2>&1 || die "jq is required" + +ASSETS_JSON="$($SCRIPT_DIR/resolve-coston2-assets.sh)" +export CHAIN_URL="${CHAIN_URL:-$(jq -r '.rpcUrl' "$CONFIG_FILE")}" +export FXRP_TOKEN="$(jq -r '.fxrp.token' <<<"$ASSETS_JSON")" +export USDT0_TOKEN="$(jq -r '.usdt0.token' <<<"$ASSETS_JSON")" + +: "${DEPLOYMENT_OWNER:?DEPLOYMENT_OWNER must be set to the broadcasting address}" +: "${ALLOCATION_VERIFIER:?ALLOCATION_VERIFIER must be set to the verifier address}" +: "${CONCORD_EXTENSION_ID:?CONCORD_EXTENSION_ID must be set to the registered FCC extension id}" +: "${TEE_EXTENSION_REGISTRY:?TEE_EXTENSION_REGISTRY must be set; on Coston2 this is normally the FlareTeeManager diamond}" +: "${TEE_MACHINE_REGISTRY:?TEE_MACHINE_REGISTRY must be set; on Coston2 this is normally the FlareTeeManager diamond}" + +cd "$PROJECT_DIR" +forge script script/DeployCoston2.s.sol:DeployCoston2 \ + --rpc-url "$CHAIN_URL" \ + --broadcast diff --git a/scripts/extension-post-setup.sh b/scripts/extension-post-setup.sh new file mode 100755 index 0000000..f3c5c2d --- /dev/null +++ b/scripts/extension-post-setup.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# extension-post-setup.sh — Extension-specific setup that runs AFTER post-build. +# +# This hook runs once the TEE node is live and registered on-chain in +# TeeMachineRegistry. Use it for setup that needs the TEE's on-chain identity +# to already exist — things that couldn't be done in extension-setup.sh because +# the TEE hadn't been registered yet. +# +# Typical uses: +# - Reading the TEE signing address from TeeMachineRegistry.getActiveTeeMachines +# and writing it to a contract so withdrawal / authorization signatures can +# be verified on-chain via ecrecover. +# - Granting the registered TEE node any extension-specific on-chain roles. +# - Seeding on-chain state that depends on knowing which TEE is active. +# +# The following variables are available (sourced from .env + config/extension.env): +# +# INSTRUCTION_SENDER — your deployed InstructionSender contract address +# EXTENSION_ID — your extension's ID on the TeeExtensionRegistry +# CHAIN_URL — chain RPC endpoint +# ADDRESSES_FILE — path to deployed-addresses.json +# DEPLOYMENT_PRIVATE_KEY — funded deployer/admin key +# +# Example: read the TEE address and register it with your contract +# +# TEE_MACHINE_REGISTRY="$(jq -r '.TeeMachineRegistry' "$ADDRESSES_FILE")" +# tee_addr=$(cast call "$TEE_MACHINE_REGISTRY" \ +# "getActiveTeeMachines(uint256)(address[],string[])" "$EXTENSION_ID" \ +# --rpc-url "$CHAIN_URL" | head -1 | tr -d '[]' | cut -d, -f1 | xargs) +# cast send "$INSTRUCTION_SENDER" "setTeeAddress(address)" "$tee_addr" \ +# --rpc-url "$CHAIN_URL" --private-key "$DEPLOYMENT_PRIVATE_KEY" +# +# Why this matters: +# Some on-chain verification requires the TEE's signing address to be known +# to the contract. Since that address is only derivable after the TEE +# registers itself, the wiring has to happen here — not in extension-setup.sh. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +GREEN='\033[0;32m'; NC='\033[0m' +log() { echo -e "${GREEN}[extension-post-setup]${NC} $*"; } + +# --- Load environment --- +if [[ -f "$PROJECT_DIR/.env" ]]; then + set -a; source "$PROJECT_DIR/.env"; set +a +fi +if [[ -f "$PROJECT_DIR/config/extension.env" ]]; then + source "$PROJECT_DIR/config/extension.env" +fi + +log "EXTENSION_ID: ${EXTENSION_ID:-}" +log "INSTRUCTION_SENDER: ${INSTRUCTION_SENDER:-}" +log "CHAIN_URL: ${CHAIN_URL:-}" + +# Concord keeps FCC machine evidence at the verifier boundary. This hook +# intentionally does not grant an EOA or TEE machine authority over facility +# balances; the verifier must validate the result before markAllocationVerified. +log "No post-registration authority wiring required for Concord." diff --git a/scripts/extension-setup.sh b/scripts/extension-setup.sh new file mode 100755 index 0000000..550d968 --- /dev/null +++ b/scripts/extension-setup.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# extension-setup.sh — Extension-specific setup that runs BEFORE Docker Compose. +# +# This hook runs between pre-build (contract deployment) and docker-up (starting +# the TEE). Use it for any setup whose output the extension needs at startup — +# for example: +# - Deploying auxiliary contracts (ERC20 tokens, oracles, vaults) +# - Writing config files the extension reads at init (pairs, feeds, pools) +# - Minting test tokens, setting allowances, seeding initial state +# +# The following variables are available (sourced from .env + config/extension.env): +# +# INSTRUCTION_SENDER — your deployed InstructionSender contract address +# EXTENSION_ID — your extension's ID on the TeeExtensionRegistry +# CHAIN_URL — chain RPC endpoint +# ADDRESSES_FILE — path to deployed-addresses.json +# DEPLOYMENT_PRIVATE_KEY — funded deployer key +# +# Example: deploy a helper contract and write its address to a config file +# +# cd "$PROJECT_DIR/tools" +# HELPER_ADDR=$(go run ./cmd/deploy-helper -a "$ADDRESSES_FILE" -c "$CHAIN_URL") +# echo "HELPER_CONTRACT=$HELPER_ADDR" > "$PROJECT_DIR/config/helper.env" +# +# Why this matters: +# The extension container reads config at startup. Anything it needs must be +# written to disk BEFORE `docker compose up`. If you deploy contracts after +# the container starts, you'll need to restart it — this hook prevents that. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +GREEN='\033[0;32m'; NC='\033[0m' +log() { echo -e "${GREEN}[extension-setup]${NC} $*"; } + +# --- Load environment --- +if [[ -f "$PROJECT_DIR/.env" ]]; then + set -a; source "$PROJECT_DIR/.env"; set +a +fi +if [[ -f "$PROJECT_DIR/config/extension.env" ]]; then + source "$PROJECT_DIR/config/extension.env" +fi + +log "EXTENSION_ID: ${EXTENSION_ID:-}" +log "INSTRUCTION_SENDER: ${INSTRUCTION_SENDER:-}" +log "CHAIN_URL: ${CHAIN_URL:-}" + +# Concord's facility contracts are deployed explicitly by +# scripts/deploy-coston2.sh. Provider funding and treasury actions happen after +# the Makkari/CoFill result is verified, so this hook intentionally does not +# mutate economic state. +log "No pre-TEE economic setup required for Concord." diff --git a/scripts/generate-bindings.sh b/scripts/generate-bindings.sh new file mode 100755 index 0000000..9bf75c3 --- /dev/null +++ b/scripts/generate-bindings.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# generate-bindings.sh — Compile Solidity contracts and generate Go bindings. +# +# Prerequisites: forge (Foundry), jq +# +# Usage: ./scripts/generate-bindings.sh +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +# --- Contract name and Go package --- +CONTRACT_FILE="ConcordInstructionSender.sol" +CONTRACT_NAME="ConcordInstructionSender" +GO_PKG="concordsender" +BINDINGS_DIR="$PROJECT_DIR/tools/pkg/contracts/$GO_PKG" + +cd "$PROJECT_DIR" + +echo "=== Step 1: Compile Solidity contracts ===" +forge build + +# Verify the contract name in the source matches what we expect +if ! grep -q "contract ${CONTRACT_NAME}" "$PROJECT_DIR/contracts/${CONTRACT_FILE}" 2>/dev/null; then + echo "" + echo "ERROR: Contract name '${CONTRACT_NAME}' not found in contracts/${CONTRACT_FILE}." + echo "Make sure the contract name in ${CONTRACT_FILE} matches CONTRACT_NAME in this script." + exit 1 +fi + +echo "=== Step 2: Extract ABI and BIN ===" +FORGE_OUT="$PROJECT_DIR/out/${CONTRACT_FILE}/${CONTRACT_NAME}.json" +if [[ ! -f "$FORGE_OUT" ]]; then + echo "ERROR: forge output not found at $FORGE_OUT" + echo "Check that CONTRACT_NAME matches your Solidity contract name." + exit 1 +fi + +mkdir -p "$BINDINGS_DIR" + +# Extract ABI (JSON array) +jq '.abi' "$FORGE_OUT" > "$BINDINGS_DIR/${CONTRACT_NAME}.abi" + +# Extract bytecode (hex string, strip 0x prefix) +jq -r '.bytecode.object' "$FORGE_OUT" | sed 's/^0x//' > "$BINDINGS_DIR/${CONTRACT_NAME}.bin" + +echo " ABI → $BINDINGS_DIR/${CONTRACT_NAME}.abi" +echo " BIN → $BINDINGS_DIR/${CONTRACT_NAME}.bin" + +echo "=== Step 3: Generate Go bindings ===" +cd "$PROJECT_DIR/tools" +go generate ./pkg/contracts/$GO_PKG/ + +echo "=== Done ===" +echo "Generated: $BINDINGS_DIR/autogen.go" diff --git a/scripts/lib/language.sh b/scripts/lib/language.sh new file mode 100755 index 0000000..4bc8d23 --- /dev/null +++ b/scripts/lib/language.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# +# language.sh — resolve the selected extension language by directory convention. +# +# Sourced by other scripts; not executable on its own. +# +# A directory is an extension implementation iff it contains a `language.env` +# manifest. That marker (rather than the mere presence of a Dockerfile) is what +# distinguishes go/ from proxy/, which also ships a Dockerfile but is not a +# language. Adding a language means creating /language.env plus a +# conforming Dockerfile — no script in this repo needs to change. +# +# See docs/extension-contract.md §8. + +# list_languages — print available language directory names. +list_languages() { + local root="${1:?list_languages requires the project dir}" + (cd "$root" && ls -d */language.env 2>/dev/null | cut -d/ -f1) +} + +# load_language [language] — validate and export the selection. +# +# Exports: +# LANGUAGE selected directory name +# LANGUAGE_NAME human-readable name from the manifest +# EXTENSION_DIR absolute path to the language directory +# EXTENSION_DOCKERFILE path relative to the project root, for compose +# LANGUAGE_TEST_CMD unit-test command +# LANGUAGE_SETUP_CMD optional pre-test install step +load_language() { + local root="${1:?load_language requires the project dir}" + local lang="${2:-${LANGUAGE:-go}}" + local manifest="$root/$lang/language.env" + + if [[ ! -f "$manifest" ]]; then + local available + available="$(list_languages "$root" | tr '\n' ' ')" + echo "[language] ERROR: unknown LANGUAGE '$lang' — no $lang/language.env found." >&2 + echo "[language] Available: ${available:-}" >&2 + echo "[language] To add one, see docs/extension-contract.md §8." >&2 + return 1 + fi + + # Clear first so a manifest that omits a field does not inherit the value + # from a previously loaded language. + LANGUAGE_NAME="" + LANGUAGE_DOCKERFILE="" + LANGUAGE_SETUP_CMD="" + LANGUAGE_BUILD_CMD="" + LANGUAGE_TEST_CMD="" + LANGUAGE_RUN_CMD="" + + # shellcheck disable=SC1090 + source "$manifest" + + LANGUAGE="$lang" + LANGUAGE_NAME="${LANGUAGE_NAME:-$lang}" + LANGUAGE_DOCKERFILE="${LANGUAGE_DOCKERFILE:-Dockerfile}" + EXTENSION_DIR="$root/$lang" + EXTENSION_DOCKERFILE="$lang/$LANGUAGE_DOCKERFILE" + + if [[ ! -f "$root/$EXTENSION_DOCKERFILE" ]]; then + echo "[language] ERROR: $lang/language.env declares LANGUAGE_DOCKERFILE=$LANGUAGE_DOCKERFILE, but $root/$EXTENSION_DOCKERFILE does not exist." >&2 + return 1 + fi + + export LANGUAGE LANGUAGE_NAME EXTENSION_DIR EXTENSION_DOCKERFILE + export LANGUAGE_SETUP_CMD LANGUAGE_BUILD_CMD LANGUAGE_TEST_CMD LANGUAGE_RUN_CMD +} diff --git a/scripts/lib/versions.sh b/scripts/lib/versions.sh new file mode 100755 index 0000000..45f1822 --- /dev/null +++ b/scripts/lib/versions.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# +# versions.sh — single source of truth for dependency versions. +# +# Sourced by other scripts; not executable on its own. Derives the tee-node +# version from the Go module pin so that non-Go language images (which build +# tee-node from source) can never drift from the Go image. +# +# Exports: +# TEE_NODE_VERSION full module pin, e.g. v0.0.21-0.20260619120252-31fc839ae6d2 +# TEE_NODE_REF git-resolvable ref: the commit SHA for a pseudo-version, +# or the tag itself for a plain tag +# TEE_PROXY_VERSION tee-proxy tag used by proxy/Dockerfile +# +# Why TEE_NODE_REF exists: Go pseudo-versions embed an abbreviated commit SHA +# (v0.0.21-0.-) and are NOT git tags. `git clone --branch` +# cannot resolve a SHA, and `git fetch ` needs a full 40-char SHA plus +# server-side allowAnySHA1InWant. A blobless partial clone followed by checkout +# handles tags and abbreviated SHAs alike — see docker/node-base.Dockerfile. + +# Resolve the extension module's go.mod. Works both before and after the Go +# implementation moves into go/ (Phase 1), so this file needs no update. +_versions_gomod() { + local root="$1" + if [[ -f "$root/go/go.mod" ]]; then + echo "$root/go/go.mod" + elif [[ -f "$root/go.mod" ]]; then + echo "$root/go.mod" + else + return 1 + fi +} + +# Extract a module's version from a go.mod require line. +_versions_pin() { + local gomod="$1" module="$2" + grep -E "^[[:space:]]*${module//\//\\/}[[:space:]]+v" "$gomod" 2>/dev/null \ + | head -1 | awk '{print $2}' +} + +# Convert a module version to a git-resolvable ref. +# +# Pseudo-versions carry a 14-digit UTC timestamp followed by a 12-hex commit +# prefix. The separator before the timestamp varies by form: +# v0.0.0-20260619120252-31fc839ae6d2 (no prior tag) +# v0.0.21-0.20260619120252-31fc839ae6d2 (after a release tag) +# v0.0.21-rc.1.0.20260619120252-31fc839ae6d2 (after a prerelease tag) +# so anchor on the timestamp+sha suffix rather than on the separator. +_versions_ref() { + local version="$1" + if [[ "$version" =~ [0-9]{14}-([0-9a-f]{12})$ ]]; then + echo "${BASH_REMATCH[1]}" + else + echo "$version" + fi +} + +# load_versions — sets and exports the version variables. +load_versions() { + local root="${1:?load_versions requires the project dir}" + local gomod + gomod="$(_versions_gomod "$root")" || { + echo "[versions] ERROR: no go.mod found under $root (looked in go/ and .)" >&2 + return 1 + } + + TEE_NODE_VERSION="$(_versions_pin "$gomod" "github.com/flare-foundation/tee-node")" + if [[ -z "$TEE_NODE_VERSION" ]]; then + echo "[versions] ERROR: no tee-node require line in $gomod" >&2 + return 1 + fi + TEE_NODE_REF="$(_versions_ref "$TEE_NODE_VERSION")" + + TEE_PROXY_VERSION="$(grep -E '^ARG TEE_PROXY_VERSION=' "$root/proxy/Dockerfile" 2>/dev/null | head -1 | cut -d= -f2)" + + export TEE_NODE_VERSION TEE_NODE_REF TEE_PROXY_VERSION +} diff --git a/scripts/post-build.sh b/scripts/post-build.sh new file mode 100755 index 0000000..094eea2 --- /dev/null +++ b/scripts/post-build.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +# post-build.sh — Register TEE version and TEE machine on-chain. +# +# Run this AFTER Docker Compose brings up the extension TEE + proxy + Redis. +# +# Inputs (env vars): +# EXT_PROXY_URL — extension proxy URL (auto-detected: :6674 for Docker, :6664 for local) +# NORMAL_PROXY_URL — normal/FTDC proxy URL (default: http://localhost:6662) +# CHAIN_URL — chain RPC URL (default: http://127.0.0.1:8545) +# ADDRESSES_FILE — path to deployed-addresses.json (auto-detected if unset) +# TEE_VERSION — version string (default: v0.1.0) +# LOCAL_MODE — skip attestation (default: true) +# WAIT_TIMEOUT — service wait timeout in seconds (default: 120) +# EXTENSION_OWNER_KEY — private key override for AddTeeVersion (optional) +# REGISTER_TEE_COMMAND — register-tee step letters (default: rRap) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'; NC='\033[0m' +log() { echo -e "${GREEN}[post-build]${NC} $*"; } +step() { echo -e "\n${CYAN}=== Step $1: $2 ===${NC}"; } +die() { echo -e "${RED}[post-build] ERROR:${NC} $*" >&2; exit 1; } + +# --- Load .env from project root (if present) --- +if [[ -f "$PROJECT_DIR/.env" ]]; then + set -a + source "$PROJECT_DIR/.env" + set +a +fi + +# Auto-detect proxy port: use Docker port (6674) if ext-proxy container is running, +# otherwise fall back to local Go process port (6664). +if [[ -z "${EXT_PROXY_URL:-}" ]]; then + if docker compose -f "$PROJECT_DIR/docker-compose.yaml" ps ext-proxy --status running 2>/dev/null | grep -q ext-proxy; then + EXT_PROXY_URL="http://localhost:6674" + else + EXT_PROXY_URL="http://localhost:6664" + fi +fi +NORMAL_PROXY_URL="${NORMAL_PROXY_URL:-http://localhost:6662}" +CHAIN_URL="${CHAIN_URL:-http://127.0.0.1:8545}" +ADDRESSES_FILE="${ADDRESSES_FILE:-}" +# Resolve relative paths against PROJECT_DIR (not caller's cwd) +if [[ -n "$ADDRESSES_FILE" && "$ADDRESSES_FILE" != /* ]]; then + ADDRESSES_FILE="$PROJECT_DIR/$ADDRESSES_FILE" +fi +TEE_VERSION="${TEE_VERSION:-v0.1.0}" +LOCAL_MODE="${LOCAL_MODE:-true}" +WAIT_TIMEOUT="${WAIT_TIMEOUT:-120}" + +# --- Auto-detect addresses file --- +if [[ -z "$ADDRESSES_FILE" ]]; then + CHAIN="${CHAIN:-}" + if [[ -z "$CHAIN" ]]; then + [[ "$LOCAL_MODE" == "true" ]] && CHAIN="local" || CHAIN="coston2" + fi + case "$CHAIN" in + coston) candidate="$PROJECT_DIR/config/coston/deployed-addresses.json" ;; + coston2) candidate="$PROJECT_DIR/config/coston2/deployed-addresses.json" ;; + *) candidate="" ;; + esac + if [[ -n "$candidate" && -f "$candidate" ]]; then + ADDRESSES_FILE="$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate")" + fi + + # Fall back to sim_dump candidates (local devnet) + if [[ -z "$ADDRESSES_FILE" ]]; then + for candidate in \ + "$PROJECT_DIR/../../e2e/docker/sim_dump/deployed-addresses.json" \ + "$PROJECT_DIR/../docker/sim_dump/deployed-addresses.json" \ + "$PROJECT_DIR/../../docker/sim_dump/deployed-addresses.json" \ + "$PROJECT_DIR/../../../docker/sim_dump/deployed-addresses.json"; do + if [[ -f "$candidate" ]]; then + ADDRESSES_FILE="$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate")" + break + fi + done + fi + + [[ -n "$ADDRESSES_FILE" ]] || die "Cannot find deployed-addresses.json. Set ADDRESSES_FILE." +fi + +[[ -f "$ADDRESSES_FILE" ]] || die "Addresses file not found: $ADDRESSES_FILE" + +# Resolve to absolute path so it works after cd into tools/ +ADDRESSES_FILE="$(cd "$(dirname "$ADDRESSES_FILE")" && pwd)/$(basename "$ADDRESSES_FILE")" + +log "Extension proxy: $EXT_PROXY_URL" +log "Normal proxy: $NORMAL_PROXY_URL" +log "Chain URL: $CHAIN_URL" +log "Addresses file: $ADDRESSES_FILE" +log "TEE version: $TEE_VERSION" +log "Local mode: $LOCAL_MODE" + +# --- wait_for_url: poll until a URL responds with HTTP 200 --- +wait_for_url() { + local url="$1" + local label="${2:-$url}" + local timeout="${3:-$WAIT_TIMEOUT}" + local interval=2 + local elapsed=0 + + log "Waiting for $label ($url) ..." + while ! curl -sf -o /dev/null "$url" 2>/dev/null; do + elapsed=$((elapsed + interval)) + if [[ $elapsed -ge $timeout ]]; then + die "Timed out after ${timeout}s waiting for $label ($url)" + fi + sleep "$interval" + done + log "$label is ready" +} + +# --- Wait for services --- +wait_for_url "$EXT_PROXY_URL/info" "Extension proxy" +wait_for_url "$NORMAL_PROXY_URL/info" "Normal proxy" + +# --- Step 1: Allow TEE version on extension --- +step 1 "Allow TEE version" +cd "$PROJECT_DIR/tools" + +go run ./cmd/allow-tee-version \ + -a "$ADDRESSES_FILE" \ + -c "$CHAIN_URL" \ + -p "$EXT_PROXY_URL" \ + -version "$TEE_VERSION" \ + || die "Allow TEE version failed" + +# --- Step 2: Set TEE governance on extension --- +# Registers the governance signer set + threshold the TEE node signs with. +# Reads GOVERNANCE_SIGNERS / GOVERNANCE_THRESHOLD (default: deployer, threshold 1) +# and MUST match the same vars passed to the TEE node container, or register-tee +# fails with InvalidGovernanceHash. +step 2 "Set TEE governance" +go run ./cmd/set-governance \ + -a "$ADDRESSES_FILE" \ + -c "$CHAIN_URL" \ + -p "$EXT_PROXY_URL" \ + || die "Set TEE governance failed" + +# Export SIMULATED_TEE for register-tee (controls attestation mode) +export SIMULATED_TEE="${SIMULATED_TEE:-true}" +log "Simulated TEE: $SIMULATED_TEE" + +# --- Step 3: Register TEE on-chain --- +# rRap, not the default rap: capital R issues the challenge as its own step, so +# re-runs don't revert with Verification.ChallengeExpired. +step 3 "Register TEE machine" +go run ./cmd/register-tee \ + -a "$ADDRESSES_FILE" \ + -c "$CHAIN_URL" \ + -p "$EXT_PROXY_URL" \ + -h "${EXT_PROXY_HOST_URL:-$EXT_PROXY_URL}" \ + -ep "$NORMAL_PROXY_URL" \ + -state "$PROJECT_DIR/config/register-tee.state" \ + -command "${REGISTER_TEE_COMMAND:-rRap}" \ + || die "Register TEE failed" + +echo "" +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN} Post-build complete${NC}" +echo -e "${GREEN}========================================${NC}" diff --git a/scripts/pre-build.sh b/scripts/pre-build.sh new file mode 100755 index 0000000..4b77809 --- /dev/null +++ b/scripts/pre-build.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +# pre-build.sh — Deploy InstructionSender contract and register extension on-chain. +# +# Inputs (env vars): +# ADDRESSES_FILE — path to deployed-addresses.json (auto-detected if unset) +# CHAIN_URL — chain RPC URL (default: http://127.0.0.1:8545) +# DEPLOYMENT_PRIVATE_KEY — funded private key (default: Hardhat account) +# +# Outputs: +# config/extension.env — EXTENSION_ID and INSTRUCTION_SENDER +# config/deploy.log — stderr from Go deploy commands +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'; NC='\033[0m' +log() { echo -e "${GREEN}[pre-build]${NC} $*"; } +step() { echo -e "\n${CYAN}=== Step $1: $2 ===${NC}"; } +die() { echo -e "${RED}[pre-build] ERROR:${NC} $*" >&2; exit 1; } + +# --- Load .env from project root (if present) --- +if [[ -f "$PROJECT_DIR/.env" ]]; then + set -a + source "$PROJECT_DIR/.env" + set +a +fi + +ADDRESSES_FILE="${ADDRESSES_FILE:-}" +# Resolve relative paths against PROJECT_DIR (not caller's cwd) +if [[ -n "$ADDRESSES_FILE" && "$ADDRESSES_FILE" != /* ]]; then + ADDRESSES_FILE="$PROJECT_DIR/$ADDRESSES_FILE" +fi +CHAIN_URL="${CHAIN_URL:-http://127.0.0.1:8545}" +CONFIG_OUTPUT="$PROJECT_DIR/config/extension.env" +LOG_FILE="$PROJECT_DIR/config/deploy.log" + +# Auto-detect addresses file +if [[ -z "$ADDRESSES_FILE" ]]; then + LOCAL_MODE="${LOCAL_MODE:-true}" + CHAIN="${CHAIN:-}" + # If CHAIN is unset, derive from LOCAL_MODE for backward compat + if [[ -z "$CHAIN" ]]; then + [[ "$LOCAL_MODE" == "true" ]] && CHAIN="local" || CHAIN="coston2" + fi + case "$CHAIN" in + coston) candidate="$PROJECT_DIR/config/coston/deployed-addresses.json" ;; + coston2) candidate="$PROJECT_DIR/config/coston2/deployed-addresses.json" ;; + *) candidate="" ;; + esac + if [[ -n "$candidate" && -f "$candidate" ]]; then + ADDRESSES_FILE="$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate")" + fi + + # Fall back to sim_dump candidates (local devnet) + if [[ -z "$ADDRESSES_FILE" ]]; then + for candidate in \ + "$PROJECT_DIR/../../e2e/docker/sim_dump/deployed-addresses.json" \ + "$PROJECT_DIR/../docker/sim_dump/deployed-addresses.json" \ + "$PROJECT_DIR/../../docker/sim_dump/deployed-addresses.json" \ + "$PROJECT_DIR/../../../docker/sim_dump/deployed-addresses.json"; do + if [[ -f "$candidate" ]]; then + ADDRESSES_FILE="$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate")" + break + fi + done + fi + + [[ -n "$ADDRESSES_FILE" ]] || die "Cannot find deployed-addresses.json. Set ADDRESSES_FILE." +fi + +[[ -f "$ADDRESSES_FILE" ]] || die "Addresses file not found: $ADDRESSES_FILE" + +# Resolve to absolute path so it works after cd into tools/ +ADDRESSES_FILE="$(cd "$(dirname "$ADDRESSES_FILE")" && pwd)/$(basename "$ADDRESSES_FILE")" + +log "Chain URL: $CHAIN_URL" +log "Addresses file: $ADDRESSES_FILE" + +# --- Step 0: Generate Go bindings from Solidity contract --- +# Must run before the pre-flight check: the tools module imports the generated +# bindings, so on a fresh clone nothing in tools/ compiles until they exist. +step 0 "Generate Go bindings" +"$SCRIPT_DIR/generate-bindings.sh" || die "Binding generation failed" + +# --- Step 1: Pre-flight check --- +step 1 "Pre-flight check" + +# Dependency pins must agree before anything is built: non-Go language images +# compile tee-node from source, and drift against the Go module pin surfaces +# later as an opaque signature-verification failure. +"$SCRIPT_DIR/check-versions.sh" || die "dependency version pins are inconsistent" + +cd "$PROJECT_DIR/tools" +if ! go run ./cmd/deploy-contract -a "$ADDRESSES_FILE" -c "$CHAIN_URL" --preflight-only 2>&1; then + die "Pre-flight check failed — fix the issues above before deploying" +fi + +# --- Step 2: Deploy InstructionSender --- +step 2 "Deploy InstructionSender contract" +cd "$PROJECT_DIR/tools" +: > "$LOG_FILE" # truncate log file +INSTRUCTION_SENDER=$(go run ./cmd/deploy-contract -a "$ADDRESSES_FILE" -c "$CHAIN_URL" 2>"$LOG_FILE" | tail -1) || { + echo -e "${RED}Deploy failed. Logs:${NC}" >&2 + cat "$LOG_FILE" >&2 + die "Deploy failed — see output above" +} + +# Validate captured address +[[ "$INSTRUCTION_SENDER" =~ ^0x[0-9a-fA-F]{40}$ ]] || { + echo -e "${RED}deploy-contract output was not a valid address. Logs:${NC}" >&2 + cat "$LOG_FILE" >&2 + die "deploy-contract returned invalid address: '$INSTRUCTION_SENDER' (expected 0x + 40 hex chars)" +} + +log "InstructionSender deployed at: $INSTRUCTION_SENDER" + +# --- Step 3: Register extension --- +step 3 "Register extension on-chain" +EXTENSION_ID=$(go run ./cmd/register-extension -a "$ADDRESSES_FILE" -c "$CHAIN_URL" --instructionSender "$INSTRUCTION_SENDER" 2>>"$LOG_FILE" | tail -1) || { + echo -e "${RED}Registration failed. Logs:${NC}" >&2 + cat "$LOG_FILE" >&2 + die "Registration failed — see output above" +} + +# Validate captured extension ID +[[ "$EXTENSION_ID" =~ ^0x[0-9a-fA-F]{64}$ ]] || { + echo -e "${RED}register-extension output was not a valid ID. Logs:${NC}" >&2 + cat "$LOG_FILE" >&2 + die "register-extension returned invalid ID: '$EXTENSION_ID' (expected 0x + 64 hex chars)" +} + +log "Extension ID: $EXTENSION_ID" + +# --- Step 4: Write config --- +step 4 "Write config" +mkdir -p "$(dirname "$CONFIG_OUTPUT")" +cat > "$CONFIG_OUTPUT" <&2; exit 1; } +command -v cast >/dev/null 2>&1 || die "cast (Foundry) is required" +command -v jq >/dev/null 2>&1 || die "jq is required" +[[ -f "$CONFIG_FILE" ]] || die "network config not found: $CONFIG_FILE" + +RPC_URL="${CHAIN_URL:-$(jq -r '.rpcUrl' "$CONFIG_FILE")}" +REGISTRY="$(jq -r '.contractRegistry' "$CONFIG_FILE")" +EXPECTED_MANAGER="$(jq -r '.fxrp.assetManager' "$CONFIG_FILE")" +EXPECTED_FXRP="$(jq -r '.fxrp.token' "$CONFIG_FILE")" +USDT0="$(jq -r '.usdt0.token' "$CONFIG_FILE")" + +[[ "$RPC_URL" != "null" && "$REGISTRY" != "null" ]] || die "incomplete network config" + +ASSET_MANAGER="$(cast call "$REGISTRY" 'getContractAddressByName(string)(address)' AssetManagerFXRP --rpc-url "$RPC_URL")" +FXRP_TOKEN="$(cast call "$ASSET_MANAGER" 'fAsset()(address)' --rpc-url "$RPC_URL")" + +normalize() { tr '[:upper:]' '[:lower:]' <<<"$1"; } +[[ "$(normalize "$ASSET_MANAGER")" == "$(normalize "$EXPECTED_MANAGER")" ]] || \ + die "ContractRegistry returned AssetManagerFXRP=$ASSET_MANAGER, expected snapshot $EXPECTED_MANAGER; refresh config after checking official sources" +[[ "$(normalize "$FXRP_TOKEN")" == "$(normalize "$EXPECTED_FXRP")" ]] || \ + die "AssetManagerFXRP returned fAsset=$FXRP_TOKEN, expected snapshot $EXPECTED_FXRP; refresh config after checking official sources" + +FXRP_CODE="$(cast code "$FXRP_TOKEN" --rpc-url "$RPC_URL")" +USDT0_CODE="$(cast code "$USDT0" --rpc-url "$RPC_URL")" +[[ "$FXRP_CODE" != "0x" ]] || die "FXRP token has no bytecode: $FXRP_TOKEN" +[[ "$USDT0_CODE" != "0x" ]] || die "USDT0 token has no bytecode: $USDT0" + +USDT0_SYMBOL="$(cast call "$USDT0" 'symbol()(string)' --rpc-url "$RPC_URL" | tr -d '\"')" +USDT0_DECIMALS="$(cast call "$USDT0" 'decimals()(uint8)' --rpc-url "$RPC_URL")" +[[ "$USDT0_SYMBOL" == "USDT0" ]] || die "configured liquidity token returned symbol $USDT0_SYMBOL" +[[ "$USDT0_DECIMALS" == "18" ]] || die "configured USDT0 token returned decimals $USDT0_DECIMALS" + +jq -n \ + --arg network "$(jq -r '.network' "$CONFIG_FILE")" \ + --argjson chainId "$(jq -r '.chainId' "$CONFIG_FILE")" \ + --arg rpcUrl "$RPC_URL" \ + --arg explorerUrl "$(jq -r '.explorerUrl' "$CONFIG_FILE")" \ + --arg contractRegistry "$REGISTRY" \ + --arg assetManager "$ASSET_MANAGER" \ + --arg fxrpToken "$FXRP_TOKEN" \ + --arg usdt0Token "$USDT0" \ + --arg usdt0Symbol "$USDT0_SYMBOL" \ + --argjson usdt0Decimals "$USDT0_DECIMALS" \ + '{network:$network,chainId:$chainId,rpcUrl:$rpcUrl,explorerUrl:$explorerUrl,contractRegistry:$contractRegistry,fxrp:{assetManager:$assetManager,token:$fxrpToken,decimals:6},usdt0:{token:$usdt0Token,symbol:$usdt0Symbol,decimals:$usdt0Decimals}}' diff --git a/scripts/start-services.sh b/scripts/start-services.sh new file mode 100755 index 0000000..1b77f6f --- /dev/null +++ b/scripts/start-services.sh @@ -0,0 +1,446 @@ +#!/usr/bin/env bash +# +# Start extension TEE node and proxy. +# +# By default, starts services via Docker Compose, picking the compose overlay +# from --chain (or env CHAIN, or legacy LOCAL_MODE): +# --chain local → docker-compose.yaml only (local devnet) +# --chain coston → + docker-compose.coston.yaml +# --chain coston2 → + docker-compose.coston2.yaml +# +# Pass --local to start services as background Go processes instead of Docker. +# +# On coston/coston2 this first syncs the Cloudflare tunnel +# (docker-compose.cloudflared.yaml): if it's already running (started by this +# or another extension), its current URL is written into .env as +# EXT_PROXY_URL — no flag needed. --tunnel additionally starts it when it's +# not already running. +# +# Usage: +# ./scripts/start-services.sh # local devnet, docker compose +# ./scripts/start-services.sh --chain coston # Coston, docker compose +# ./scripts/start-services.sh --local # local devnet, Go processes +# ./scripts/start-services.sh --chain coston2 --tunnel # also start the tunnel if missing +# +# By default the node and proxy are built from PINNED module/image versions +# (tee-node + tee-proxy), fully self-contained — no sibling repos required. +# To build them from on-disk sibling checkouts instead (while developing +# tee-node / tee-proxy), set USE_LOCAL_SIBLINGS=1: +# USE_LOCAL_SIBLINGS=1 ./scripts/start-services.sh --chain coston2 +# +# Prerequisites: +# - Infrastructure running (Hardhat, indexer, Redis, normal TEE + proxy) +# - config/extension.env exists (created by pre-build.sh), OR EXTENSION_ID is set +# - Redis will be started on :6382 automatically (separate from infrastructure Redis) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'; NC='\033[0m' +log() { echo -e "${GREEN}[start-services]${NC} $*"; } +die() { echo -e "${RED}[start-services] ERROR:${NC} $*" >&2; exit 1; } + +# --- Parse flags --- +USE_LOCAL=false +USE_TUNNEL=false +CHAIN="${CHAIN:-}" +while [[ $# -gt 0 ]]; do + case "$1" in + --local) USE_LOCAL=true; shift ;; + --tunnel) USE_TUNNEL=true; shift ;; + --chain) [[ $# -ge 2 ]] || die "--chain requires a value (local|coston|coston2)" + CHAIN="$2"; shift 2 ;; + --chain=*) CHAIN="${1#--chain=}"; shift ;; + *) die "Unknown argument: $1" ;; + esac +done + +# --- Load .env from project root (if present) --- +if [[ -f "$PROJECT_DIR/.env" ]]; then + set -a + source "$PROJECT_DIR/.env" + set +a +fi + +# --- Load extension config --- +CONFIG_FILE="$PROJECT_DIR/config/extension.env" +if [[ -f "$CONFIG_FILE" ]]; then + # shellcheck disable=SC1090 + source "$CONFIG_FILE" +fi + +EXTENSION_ID="${EXTENSION_ID:-}" +PROXY_PRIVATE_KEY="${PROXY_PRIVATE_KEY:-}" +LOCAL_MODE="${LOCAL_MODE:-true}" + +[[ -n "$PROXY_PRIVATE_KEY" ]] || die "PROXY_PRIVATE_KEY must be set in .env before starting services" + +# --- Resolve CHAIN (flag > env > legacy LOCAL_MODE) --- +if [[ -z "$CHAIN" ]]; then + if [[ "$LOCAL_MODE" == "true" ]]; then + CHAIN="local" + else + CHAIN="coston2" # legacy + fi +fi +case "$CHAIN" in + local|coston|coston2) ;; + *) die "Unknown --chain value: $CHAIN (valid: local, coston, coston2)" ;; +esac + +[[ -n "$EXTENSION_ID" ]] || die "EXTENSION_ID not set. Run pre-build.sh first or set it manually." + +# --- Resolve LANGUAGE by directory convention --------------------------------- +# Discovery is driven by /language.env; there is deliberately no +# hardcoded list here (see docs/extension-contract.md §8). +# shellcheck source=lib/language.sh +source "$SCRIPT_DIR/lib/language.sh" +load_language "$PROJECT_DIR" || die "could not resolve LANGUAGE" + +# --- Derive dependency versions from the Go module pin ------------------------ +# Language images that build tee-node from source consume TEE_NODE_REF; see +# scripts/lib/versions.sh for why a pseudo-version needs a SHA, not a tag. +# shellcheck source=lib/versions.sh +source "$SCRIPT_DIR/lib/versions.sh" +load_versions "$PROJECT_DIR" || die "could not derive dependency versions" + +# --- Guard: --local runs Go binaries directly, so it is Go-only --------------- +if [[ "$USE_LOCAL" == "true" && "$LANGUAGE" != "go" ]]; then + die "--local mode builds and runs Go binaries in-process and only supports LANGUAGE=go (got '$LANGUAGE').\n Use Docker Compose mode instead: ./scripts/start-services.sh --chain $CHAIN" +fi + +# --- Resolve local-siblings toggle --- +case "${USE_LOCAL_SIBLINGS:-}" in + 1|true|yes|on) USE_LOCAL_SIBLINGS=true ;; + *) USE_LOCAL_SIBLINGS=false ;; +esac + +# The sibling build compiles tee-node into the Go binary; other languages build +# tee-node as a separate binary from a pinned ref and have no sibling path. +if [[ "$USE_LOCAL_SIBLINGS" == "true" && "$LANGUAGE" != "go" ]]; then + die "USE_LOCAL_SIBLINGS is only supported for LANGUAGE=go (got '$LANGUAGE').\n Other languages build tee-node from the pinned ref ($TEE_NODE_REF).\n To test a local tee-node there, push it and update the pin in go/go.mod." +fi + +log "Chain: $CHAIN" +log "Language: $LANGUAGE ($EXTENSION_DOCKERFILE)" +log "Extension ID: $EXTENSION_ID" +log "Local mode: $LOCAL_MODE" +log "Local siblings: $USE_LOCAL_SIBLINGS" +log "tee-node ref: $TEE_NODE_REF" + +# --- Manage go.work for host builds (git-ignored, fully managed here) --- +# Toggle on: host `go` builds (e.g. --local mode) resolve the on-disk sibling +# tee-node / tee-proxy. Toggle off: remove any stale go.work so host builds fall +# back to the pinned module versions in go.mod / tools/go.mod. +GOWORK_FILE="$PROJECT_DIR/go.work" +if [[ "$USE_LOCAL_SIBLINGS" == "true" ]]; then + SIBLINGS_ROOT="$(cd "$PROJECT_DIR/../.." && pwd)" + for sib in tee-node tee-proxy; do + [[ -d "$SIBLINGS_ROOT/$sib" ]] || die "USE_LOCAL_SIBLINGS set but $SIBLINGS_ROOT/$sib is missing.\n Clone it into $SIBLINGS_ROOT/, or unset USE_LOCAL_SIBLINGS to use pinned modules." + done + log "Writing $GOWORK_FILE (host builds use sibling tee-node / tee-proxy)" + GO_VERSION=$(grep '^go ' "$PROJECT_DIR/go/go.mod" | awk '{print $2}') + cat > "$GOWORK_FILE" < to isolate it." + fi + + if docker compose "${proj[@]}" -f "$cf_compose" ps -q cloudflared 2>/dev/null | grep -q .; then + log "Cloudflare tunnel already running — reusing it." + elif [[ "$USE_TUNNEL" == "true" ]]; then + docker compose "${proj[@]}" -f "$cf_compose" up -d || die "Failed to start cloudflared" + else + # Otherwise the only symptom is the /info wait timing out, which does not + # point at the tunnel at all. + log "NOTE: no tunnel running and --tunnel not passed — EXT_PROXY_URL must" + log " already be reachable by Flare, or the proxy wait below times out." + return + fi + TUNNEL_ACTIVE=true + + # Named tunnel has a fixed hostname already in .env — nothing to discover. + if [[ -n "${TUNNEL_ARGS:-}" ]]; then + log "Named tunnel up — keeping EXT_PROXY_URL=${EXT_PROXY_URL:-}" + return + fi + + # A restarted container keeps its old logs, so scan only from this start — + # otherwise tail -1 hands back the previous run's dead hostname. + local cid started + local -a since=() + cid=$(docker compose "${proj[@]}" -f "$cf_compose" ps -q cloudflared 2>/dev/null | head -1 || true) + started=$(docker inspect -f '{{.State.StartedAt}}' "$cid" 2>/dev/null | cut -c1-19 || true) + [[ -n "$started" ]] && since=(--since "${started}Z") + + log "Reading the quick-tunnel URL..." + local url="" i + for ((i = 0; i < 30; i++)); do + # `|| true` is load-bearing: grep exits 1 until the URL appears, and + # pipefail would kill the loop on iteration 1 instead of retrying. + url=$(docker compose "${proj[@]}" -f "$cf_compose" logs "${since[@]}" cloudflared 2>/dev/null \ + | grep -o 'https://[a-z0-9-]*\.trycloudflare\.com' | tail -1 || true) + [[ -n "$url" ]] && break + sleep 1 + done + [[ -n "$url" ]] || die "cloudflared printed no *.trycloudflare.com URL within 30s.\n Check: docker compose ${proj[*]} -f $cf_compose logs cloudflared" + + export EXT_PROXY_URL="$url" + # post-build.sh and test.sh re-source .env, so the file is how the fresh URL + # reaches them — exporting alone would be overwritten. + if [[ -f "$PROJECT_DIR/.env" ]]; then + if grep -q '^EXT_PROXY_URL=' "$PROJECT_DIR/.env"; then + sed -i.bak "s|^EXT_PROXY_URL=.*|EXT_PROXY_URL=$url|" "$PROJECT_DIR/.env" + rm -f "$PROJECT_DIR/.env.bak" + else + echo "EXT_PROXY_URL=$url" >> "$PROJECT_DIR/.env" + fi + log "Tunnel URL: $url (written to .env)" + else + log "Tunnel URL: $url (no .env to update)" + fi +} + +if [[ "$CHAIN" == "local" ]]; then + [[ "$USE_TUNNEL" == "true" ]] && log "--tunnel ignored on --chain local (the proxy is reachable on localhost)" +else + sync_tunnel +fi + +# ============================================================ +# Docker Compose mode (default) +# ============================================================ +if [[ "$USE_LOCAL" == "false" ]]; then + log "Starting services with Docker Compose..." + + # Dockerfile expects SOURCE_DATE_EPOCH for reproducible builds — see REPRODUCIBILITY.md. + # Without it, `touch -h -d @${SOURCE_DATE_EPOCH}` in the builder stage fails with "invalid date format '@'". + if [[ -z "${SOURCE_DATE_EPOCH:-}" ]]; then + if SOURCE_DATE_EPOCH=$(git -C "$PROJECT_DIR" log -1 --format=%ct 2>/dev/null) && [[ -n "$SOURCE_DATE_EPOCH" ]]; then + export SOURCE_DATE_EPOCH + else + export SOURCE_DATE_EPOCH=0 + fi + fi + log "SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH" + + # --- Build the shared tee-node base image if this language needs it --- + # Two-process languages run a prebuilt tee-node `server` binary; the Go + # image links tee-node as a library and needs nothing here. + if grep -q 'tee-node-base' "$PROJECT_DIR/$EXTENSION_DOCKERFILE" 2>/dev/null; then + log "Language '$LANGUAGE' needs the shared tee-node base image" + "$SCRIPT_DIR/build-node-base.sh" || die "failed to build tee-node base image" + fi + + # --- Build tee-proxy image locally if no remote registry is configured --- + if [[ -z "${REGISTRY:-}" ]]; then + if ! docker image inspect local/tee-proxy >/dev/null 2>&1; then + if [[ "$USE_LOCAL_SIBLINGS" == "true" ]]; then + # Dev build: use the on-disk sibling tee-proxy checkout. + TEE_PROXY_DIR="$SIBLINGS_ROOT/tee-proxy" + [[ -d "$TEE_PROXY_DIR" ]] || die "USE_LOCAL_SIBLINGS set but tee-proxy repo not present at $TEE_PROXY_DIR.\n Clone it into $SIBLINGS_ROOT/, or unset USE_LOCAL_SIBLINGS to build the pinned proxy." + log "Building local/tee-proxy from sibling checkout $TEE_PROXY_DIR (USE_LOCAL_SIBLINGS)..." + docker build -f "$TEE_PROXY_DIR/Dockerfile" -t local/tee-proxy "$TEE_PROXY_DIR" || die "Failed to build tee-proxy image" + else + # Default: pinned, self-contained build from the in-repo Dockerfile + # (clones tee-proxy at the version in proxy/Dockerfile, matching tools/go.mod). + PROXY_DOCKERFILE="$PROJECT_DIR/proxy/Dockerfile" + [[ -f "$PROXY_DOCKERFILE" ]] || die "Image local/tee-proxy not found and proxy Dockerfile missing at $PROXY_DOCKERFILE.\n Set REGISTRY in .env to pull from a remote registry, restore proxy/Dockerfile, or use USE_LOCAL_SIBLINGS=1 to build from a sibling checkout." + log "Building local/tee-proxy from $PROXY_DOCKERFILE (pinned, self-cloning)..." + docker build -f "$PROXY_DOCKERFILE" -t local/tee-proxy "$PROJECT_DIR/proxy" || die "Failed to build tee-proxy image" + fi + log "local/tee-proxy image built successfully" + else + log "local/tee-proxy image already exists (use 'docker rmi local/tee-proxy' to force rebuild)" + fi + fi + + COMPOSE_FILES=("-f" "$PROJECT_DIR/docker-compose.yaml") + + # Toggle: swap the pinned self-contained node build for the sibling-based one. + if [[ "$USE_LOCAL_SIBLINGS" == "true" ]]; then + log "USE_LOCAL_SIBLINGS — node built from on-disk tee-node via Dockerfile.siblings" + COMPOSE_FILES+=("-f" "$PROJECT_DIR/docker-compose.siblings.yaml") + fi + + case "$CHAIN" in + local) ;; + coston) + log "Coston mode — attaching docker-compose.coston.yaml" + COMPOSE_FILES+=("-f" "$PROJECT_DIR/docker-compose.coston.yaml") + ;; + coston2) + log "Coston2 mode — attaching docker-compose.coston2.yaml" + COMPOSE_FILES+=("-f" "$PROJECT_DIR/docker-compose.coston2.yaml") + ;; + esac + + # Compose bind-mounts this as the proxy's config.toml; if missing, docker + # makes a directory there and `up` dies with an opaque rootfs error. + if [[ "$CHAIN" == "local" ]]; then + PROXY_CFG="extension_proxy.docker.toml" + else + PROXY_CFG="extension_proxy.$CHAIN.docker.toml" + fi + if [[ -d "$PROJECT_DIR/config/proxy/$PROXY_CFG" ]]; then + die "config/proxy/$PROXY_CFG is a directory — docker created it on an earlier run when the config was missing.\n rm -rf config/proxy/$PROXY_CFG && cp config/proxy/$PROXY_CFG.example config/proxy/$PROXY_CFG" + elif [[ ! -f "$PROJECT_DIR/config/proxy/$PROXY_CFG" ]]; then + die "config/proxy/$PROXY_CFG not found (it is gitignored — a fresh clone only has the .example).\n cp config/proxy/$PROXY_CFG.example config/proxy/$PROXY_CFG # then fill in the [db] credentials" + fi + + docker compose "${COMPOSE_FILES[@]}" up -d --build || die "docker compose up failed" + + # Wait for proxy to be ready + E2E="$SCRIPT_DIR/e2e.sh" + EXT_PROXY_URL="${EXT_PROXY_URL:-http://localhost:6674}" + log "Waiting for extension proxy at $EXT_PROXY_URL/info ..." + "$E2E" wait-for-url "$EXT_PROXY_URL/info" 120 + + # Validate EXTENSION_ID is recognized by proxy + log "Validating EXTENSION_ID against proxy..." + PROXY_INFO=$(curl -sf "$EXT_PROXY_URL/info" 2>/dev/null || true) + if [[ -n "$PROXY_INFO" ]]; then + if ! echo "$PROXY_INFO" | grep -q "$EXTENSION_ID" 2>/dev/null; then + echo -e "${RED}WARNING: EXTENSION_ID $EXTENSION_ID not found in proxy /info response${NC}" >&2 + echo -e "${RED}The proxy may be filtering for a different extension. Check config.${NC}" >&2 + fi + fi + + echo "" + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN} Services started (Docker Compose)${NC}" + echo -e "${GREEN}========================================${NC}" + echo "" + echo -e "${CYAN}Mode${NC}" + case "$CHAIN" in + local) echo " Local devnet" ;; + coston) echo " Coston testnet (chain_id=16)" ;; + coston2) echo " Coston2 testnet (chain_id=114)" ;; + esac + echo "" + echo -e "${CYAN}Services${NC}" + echo " redis, ext-proxy, extension-tee" + [[ "$TUNNEL_ACTIVE" == "true" ]] && echo " cloudflared (tunnel)" + echo " Proxy URL: $EXT_PROXY_URL" + echo "" + echo -e "${CYAN}Commands${NC}" + echo " Logs: docker compose ${COMPOSE_FILES[*]} logs -f" + echo " Stop: ./scripts/stop-services.sh --chain $CHAIN" + # The tunnel survives stop-services.sh on purpose — killing it rotates the URL. + [[ "$TUNNEL_ACTIVE" == "true" ]] && \ + echo " Stop tunnel too: ./scripts/stop-services.sh --chain $CHAIN --tunnel" + exit 0 +fi + +# ============================================================ +# Local Go process mode (--local) +# ============================================================ +log "Starting services as local Go processes (--local)..." + +E2E="$SCRIPT_DIR/e2e.sh" +PID_DIR="$PROJECT_DIR/out/pids" +LOG_DIR="$PROJECT_DIR/out/logs" + +# --- Build Go binaries (once) so we run the actual binary, not `go run` --- +BIN_DIR="$PROJECT_DIR/out/bin" +mkdir -p "$BIN_DIR" +log "Building Go binaries..." +# start-tee links the Go extension in-process, so it lives in the extension +# module; start-proxy is deployment tooling and lives in tools/. +cd "$EXTENSION_DIR" +go build -o "$BIN_DIR/start-tee" ./cmd/start-tee +cd "$PROJECT_DIR/tools" +go build -o "$BIN_DIR/start-proxy" ./cmd/start-proxy + +# --- Start extension TEE --- +log "Starting extension TEE node..." +EXTENSION_ID="$EXTENSION_ID" "$E2E" start ext-tee "$PID_DIR/ext-tee.pid" "$LOG_DIR/ext-tee.log" \ + "$BIN_DIR/start-tee" -extensionID "$EXTENSION_ID" + +log "Waiting for extension TEE to initialize..." +sleep 5 + +# --- Start extension Redis on port 6382 via Docker Compose --- +log "Starting Redis via Docker Compose..." +docker compose -f "$PROJECT_DIR/docker-compose.yaml" up -d redis +log "Waiting for Redis on :6382..." +retries=0 +while ! docker compose -f "$PROJECT_DIR/docker-compose.yaml" exec -T redis redis-cli ping > /dev/null 2>&1; do + retries=$((retries + 1)) + if [ $retries -ge 15 ]; then + die "Redis container failed to become healthy" + fi + sleep 1 +done +log "Redis on :6382 ready" + +# --- Start extension proxy --- +log "Starting extension proxy..." +PROXY_PRIVATE_KEY="$PROXY_PRIVATE_KEY" "$E2E" start ext-proxy "$PID_DIR/ext-proxy.pid" "$LOG_DIR/ext-proxy.log" \ + "$BIN_DIR/start-proxy" + +cd "$PROJECT_DIR" + +# --- Wait for proxy to be ready --- +if [[ "$EXT_PROXY_URL" != *"localhost"* && "$EXT_PROXY_URL" != *"127.0.0.1"* ]]; then + log "NOTE: EXT_PROXY_URL=$EXT_PROXY_URL (not localhost) — health check targets this URL" +fi +log "Waiting for extension proxy..." +"$E2E" wait-for-url "http://localhost:6664/info" 60 + +# --- Summary --- +EXT_TEE_PID=$(cat "$PID_DIR/ext-tee.pid" 2>/dev/null || echo "?") +EXT_PROXY_PID=$(cat "$PID_DIR/ext-proxy.pid" 2>/dev/null || echo "?") + +echo "" +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN} Services started (local Go processes)${NC}" +echo -e "${GREEN}========================================${NC}" +echo "" +echo -e "${CYAN}Processes${NC}" +echo " Extension Redis Docker container (port 6382)" +echo " Extension TEE PID $EXT_TEE_PID" +echo " Extension Proxy PID $EXT_PROXY_PID" +echo " Proxy URL http://localhost:6664" +echo "" +echo -e "${CYAN}Logs${NC}" +echo " Redis log docker compose logs redis" +echo " TEE log $LOG_DIR/ext-tee.log" +echo " Proxy log $LOG_DIR/ext-proxy.log" +echo "" +echo -e "${CYAN}Commands${NC}" +echo " Status: $SCRIPT_DIR/e2e.sh status $PID_DIR" +echo " Stop: $SCRIPT_DIR/stop-services.sh --local" diff --git a/scripts/stop-services.sh b/scripts/stop-services.sh new file mode 100755 index 0000000..38ef59c --- /dev/null +++ b/scripts/stop-services.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# +# Stop extension services. +# +# By default, stops Docker Compose services, picking the compose overlay from +# --chain (or env CHAIN, or legacy LOCAL_MODE): +# --chain local → docker-compose.yaml only +# --chain coston → + docker-compose.coston.yaml +# --chain coston2 → + docker-compose.coston2.yaml +# +# Pass --local to stop background Go processes instead. +# +# Pass --tunnel to also stop the shared Cloudflare tunnel (compose project +# "tunnel"), after everything behind it. Without it the tunnel is left running: +# other extensions reuse the same container, and stopping it rotates their URL. +# +# Usage: +# ./scripts/stop-services.sh # local devnet, docker compose +# ./scripts/stop-services.sh --chain coston # Coston, docker compose +# ./scripts/stop-services.sh --local # background Go processes +# ./scripts/stop-services.sh --chain coston2 --tunnel # also stop the tunnel +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +RED='\033[0;31m'; GREEN='\033[0;32m'; NC='\033[0m' +log() { echo -e "${GREEN}[stop-services]${NC} $*"; } +die() { echo -e "${RED}[stop-services] ERROR:${NC} $*" >&2; exit 1; } + +# --- Parse flags --- +USE_LOCAL=false +USE_TUNNEL=false +CHAIN="${CHAIN:-}" +while [[ $# -gt 0 ]]; do + case "$1" in + --local) USE_LOCAL=true; shift ;; + --tunnel) USE_TUNNEL=true; shift ;; + --chain) [[ $# -ge 2 ]] || die "--chain requires a value (local|coston|coston2)" + CHAIN="$2"; shift 2 ;; + --chain=*) CHAIN="${1#--chain=}"; shift ;; + *) die "Unknown argument: $1" ;; + esac +done + +# --- Load .env from project root (if present) --- +if [[ -f "$PROJECT_DIR/.env" ]]; then + set -a + source "$PROJECT_DIR/.env" + set +a +fi + +LOCAL_MODE="${LOCAL_MODE:-true}" + +# --- Resolve CHAIN (flag > env > legacy LOCAL_MODE) --- +if [[ -z "$CHAIN" ]]; then + if [[ "$LOCAL_MODE" == "true" ]]; then + CHAIN="local" + else + CHAIN="coston2" # legacy + fi +fi +case "$CHAIN" in + local|coston|coston2) ;; + *) die "Unknown --chain value: $CHAIN (valid: local, coston, coston2)" ;; +esac + +if [[ "$USE_LOCAL" == "true" ]]; then + # --- Stop background Go processes --- + E2E="$SCRIPT_DIR/e2e.sh" + PID_DIR="$PROJECT_DIR/out/pids" + + log "Stopping background Go processes..." + "$E2E" stop-all "$PID_DIR" +else + # --- Stop Docker Compose services --- + COMPOSE_FILES=("-f" "$PROJECT_DIR/docker-compose.yaml") + + # Mirror start-services.sh: include the siblings overlay when active so + # compose resolves the same project/services that were started. + case "${USE_LOCAL_SIBLINGS:-}" in + 1|true|yes|on) COMPOSE_FILES+=("-f" "$PROJECT_DIR/docker-compose.siblings.yaml") ;; + esac + + case "$CHAIN" in + local) ;; + coston) COMPOSE_FILES+=("-f" "$PROJECT_DIR/docker-compose.coston.yaml") ;; + coston2) COMPOSE_FILES+=("-f" "$PROJECT_DIR/docker-compose.coston2.yaml") ;; + esac + + # docker-compose.yaml interpolates SOURCE_DATE_EPOCH as a build arg. It's + # irrelevant on `down`, but compose still warns when it's unset — silence it. + export SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-0}" + + log "Stopping Docker Compose services (chain: $CHAIN)..." + docker compose "${COMPOSE_FILES[@]}" down +fi + +# Stopped last and only with --tunnel: other extensions reuse this container, +# so tearing it down rotates their URL too. +CF_COMPOSE="$PROJECT_DIR/docker-compose.cloudflared.yaml" +if [[ -f "$CF_COMPOSE" ]]; then + CF_PROJ=() + [[ "$USE_LOCAL" == "true" ]] && CF_PROJ=(-p tunnel-local) + if [[ "$USE_TUNNEL" == "true" ]]; then + if docker compose "${CF_PROJ[@]}" -f "$CF_COMPOSE" ps -q cloudflared 2>/dev/null | grep -q .; then + log "Stopping the shared Cloudflare tunnel (last)..." + docker compose "${CF_PROJ[@]}" -f "$CF_COMPOSE" down || log "WARNING: failed to stop cloudflared" + else + log "No tunnel running — nothing to stop." + fi + elif docker compose "${CF_PROJ[@]}" -f "$CF_COMPOSE" ps -q cloudflared 2>/dev/null | grep -q .; then + log "Leaving the shared Cloudflare tunnel running (pass --tunnel to stop it)." + fi +fi + +log "Done." diff --git a/scripts/update-tee-url.sh b/scripts/update-tee-url.sh new file mode 100755 index 0000000..16e3934 --- /dev/null +++ b/scripts/update-tee-url.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# update-tee-url.sh — repoint the on-chain TEE machine record at a new proxy URL. +# +# Run this whenever the public tunnel URL changes (ngrok / cloudflared restart): +# Flare's TEE infrastructure delivers instructions to the URL registered in the +# MachineManager, so a dead tunnel means every on-chain op (deposit, withdraw +# request, session-key bind) silently times out at pollAction even though the +# chain leg succeeded. post-build.sh CANNOT fix this — its pre-registration +# step (the only writer of the URL) is skipped once the machine exists, and a +# full re-registration trips ChallengeExpired on a long-running TEE. This calls +# MachineManager.updateTeeMachineSettings directly instead — no attestation, +# one transaction, ledger untouched. +# +# Usage: ./scripts/update-tee-url.sh [new-url] +# (default: EXT_PROXY_URL from .env — update that first, then run this) +# +# Requires: foundry (cast), python, curl; DEPLOYMENT_PRIVATE_KEY + CHAIN_URL +# in .env; the local ext-proxy running (its /info supplies the TEE pubkey). +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +set -a; source "$PROJECT_DIR/.env"; set +a + +URL="${1:-${EXT_PROXY_URL:?EXT_PROXY_URL not set}}" +RPC="${CHAIN_URL:?CHAIN_URL not set}" +KEY="0x${DEPLOYMENT_PRIVATE_KEY#0x}" +ADDRESSES_FILE="${ADDRESSES_FILE:-./config/coston2/deployed-addresses.json}" +[[ "$ADDRESSES_FILE" != /* ]] && ADDRESSES_FILE="$PROJECT_DIR/$ADDRESSES_FILE" +LOCAL_INFO="${LOCAL_INFO_URL:-http://localhost:6674/info}" + +FTM=$(python -c " +import json, sys +entries = json.load(open(sys.argv[1])) +print(next(e['address'] for e in entries if e['name'] == 'FlareTeeManager')) +" "$ADDRESSES_FILE") + +# teeId = the EVM address of the TEE's secp256k1 pubkey (keccak(X||Y)[12:]). +XY=$(curl -sf -m 10 "$LOCAL_INFO" | python -c " +import json,sys +k = json.load(sys.stdin)['teeInfo']['publicKey'] +print(k['x'][2:] + k['y'][2:]) +") +TEE_ID=0x$(cast keccak "0x$XY" | tail -c 41) + +CURRENT=$(cast call "$FTM" "getTeeMachine(address)((address,address,string))" "$TEE_ID" --rpc-url "$RPC") +PROXY_ID=$(echo "$CURRENT" | sed -E 's/^\([^,]+, ([^,]+),.*$/\1/') +echo "machine: $TEE_ID" +echo "current: $CURRENT" +echo "new url: $URL" + +cast send "$FTM" "updateTeeMachineSettings(address,address,string)" \ + "$TEE_ID" "$PROXY_ID" "$URL" \ + --private-key "$KEY" --rpc-url "$RPC" > /dev/null + +cast call "$FTM" "getTeeMachine(address)((address,address,string))" "$TEE_ID" --rpc-url "$RPC" \ + | grep -qF "$URL" && echo "OK — on-chain machine URL updated." \ + || { echo "ERROR: on-chain URL does not match after update" >&2; exit 1; } diff --git a/scripts/use-chain.sh b/scripts/use-chain.sh new file mode 100755 index 0000000..a826268 --- /dev/null +++ b/scripts/use-chain.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# +# use-chain.sh — Activate a chain-specific .env file. +# +# Reads .env. from the project root and copies it to .env. All +# deployment scripts (pre-build, post-build, test, full-setup) auto-load .env. +# +# Usage: +# ./scripts/use-chain.sh Copy .env. → .env (e.g. coston). +# ./scripts/use-chain.sh --list List available .env. files. +# ./scripts/use-chain.sh -h | --help Show this help. +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +SCRIPT_NAME="$(basename "$0")" + +log() { echo "[use-chain] $*"; } +die() { echo "[use-chain] ERROR: $*" >&2; exit 1; } + +usage() { + cat < Copy .env. → .env (e.g. coston, coston2). + $SCRIPT_NAME --list List available .env. files. + $SCRIPT_NAME -h | --help Show this help. + +The active .env is auto-loaded by every deployment script in scripts/. +EOF +} + +# Discover .env. files in the project root, skipping helper files. +list_chains() { + local any=0 f name + for f in "$PROJECT_DIR"/.env.*; do + [[ -f "$f" ]] || continue + name="${f##*/.env.}" + case "$name" in example|local|template|sample) continue ;; esac + echo " $name" + any=1 + done + [[ $any -eq 1 ]] || echo " (none — create .env. from .env.example)" +} + +# Extract a value from $DST for the given KEY: +# - strip the "KEY=" prefix +# - strip trailing " # comment" +# - trim trailing whitespace +# - strip surrounding double quotes +get_val() { + grep -E "^$1=" "$DST" 2>/dev/null | head -1 \ + | sed -E "s/^[^=]*=//; s/[[:space:]]+#.*$//; s/[[:space:]]+$//" \ + | tr -d '"' +} + +# --- Argument parsing --- + +[[ $# -ge 1 ]] || { usage; exit 1; } +case "$1" in + -h|--help) usage; exit 0 ;; + --list) log "available chains:"; list_chains; exit 0 ;; + -*) die "unknown flag: $1 (try --help)" ;; +esac +[[ $# -eq 1 ]] || die "expected one chain name (got $#); try --help" + +CHAIN="$1" +SRC="$PROJECT_DIR/.env.$CHAIN" +DST="$PROJECT_DIR/.env" + +if [[ ! -f "$SRC" ]]; then + log "no .env.$CHAIN in $PROJECT_DIR" + log "available chains:" + list_chains + exit 1 +fi + +# --- Swap --- + +cp "$SRC" "$DST" || die "failed to copy $SRC → $DST" +log "active chain: $CHAIN" + +# --- Summary --- + +key_val=$(get_val DEPLOYMENT_PRIVATE_KEY) +log " CHAIN_URL = $(v=$(get_val CHAIN_URL); echo "${v:-}")" +log " EXT_PROXY_URL = $(v=$(get_val EXT_PROXY_URL); echo "${v:-}")" +log " NORMAL_PROXY_URL = $(v=$(get_val NORMAL_PROXY_URL); echo "${v:-}")" +log " INITIAL_OWNER = $(v=$(get_val INITIAL_OWNER); echo "${v:-}")" +log " DEPLOYMENT_PRIVATE_KEY = $([[ -n "$key_val" ]] && echo "" || echo "")" diff --git a/tools/cmd/allow-tee-version/main.go b/tools/cmd/allow-tee-version/main.go new file mode 100644 index 0000000..fce8396 --- /dev/null +++ b/tools/cmd/allow-tee-version/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "concord/tools/pkg/configs" + "concord/tools/pkg/fccutils" + "concord/tools/pkg/support" + "crypto/ecdsa" + "flag" + "github.com/ethereum/go-ethereum/crypto" + "github.com/flare-foundation/go-flare-common/pkg/logger" + "os" + "strings" +) + +func main() { + af := flag.String("a", configs.AddressesFile, "file with deployed addresses") + cf := flag.String("c", configs.ChainNodeURL, "chain node url") + pf := flag.String("p", configs.ExtensionProxyURL, "proxy url") + versionF := flag.String("version", "v0.1.0", "version") + flag.Parse() + + testSupport, err := support.DefaultSupport(*af, *cf) + if err != nil { + fccutils.FatalWithCause(err) + } + + // get teeID from proxy + teeInfo, err := fccutils.TeeInfo(*pf) + if err != nil { + fccutils.FatalWithCause(err) + } + + var privKey *ecdsa.PrivateKey + privKeyString := os.Getenv("EXTENSION_OWNER_KEY") + if privKeyString != "" { + if strings.HasPrefix(privKeyString, "0x") || strings.HasPrefix(privKeyString, "0X") { + privKeyString = privKeyString[2:] + } + privKey, err = crypto.HexToECDSA(privKeyString) + if err != nil { + fccutils.FatalWithCause(err) + } + } else { + privKey = testSupport.Prv + } + + keySource := "EXTENSION_OWNER_KEY" + if privKeyString == "" { + keySource = "DEPLOYMENT_PRIVATE_KEY (default)" + } + logger.Infof("Using key: %s (deployer: %s)", keySource, crypto.PubkeyToAddress(privKey.PublicKey).Hex()) + + teeID, _, err := fccutils.TeeProxyId(teeInfo) + if err != nil { + fccutils.FatalWithCause(err) + } + + logger.Infof("Code hash: %s (source: proxy /info)", teeInfo.MachineData.CodeHash.Hex()) + logger.Infof("Platform: %s (source: proxy /info)", teeInfo.MachineData.Platform.Hex()) + logger.Infof("Extension ID: %s", teeInfo.MachineData.ExtensionID.Big().String()) + logger.Infof("TEE ID: %s", teeID.Hex()) + logger.Infof("Version: %s", *versionF) + logger.Warnf("NOTE: Code hash is from proxy /info response — not independently verified against attestation") + + // Idempotency: skip if this codeHash+platform combo is already registered and active. + // Avoids sending a tx that would revert with VersionAlreadyExists() on re-runs. + supported, err := testSupport.TeeExtensionRegistry.IsCodeHashPlatformSupported( + nil, + teeInfo.MachineData.ExtensionID.Big(), + teeInfo.MachineData.CodeHash, + teeInfo.MachineData.Platform, + ) + if err != nil { + fccutils.FatalWithCause(err) + } + if supported { + logger.Infof("version already registered for this code hash + platform, skipping") + return + } + + err = fccutils.AddTeeVersion(testSupport, privKey, teeInfo.MachineData.ExtensionID.Big(), teeInfo.MachineData.CodeHash, teeInfo.MachineData.Platform, *versionF) + if err != nil { + if strings.Contains(err.Error(), "VersionAlreadyExists") { + logger.Infof("version already registered, skipping") + } else { + fccutils.FatalWithCause(err) + } + } +} diff --git a/tools/cmd/audit-deploy/main.go b/tools/cmd/audit-deploy/main.go new file mode 100644 index 0000000..bdd919d --- /dev/null +++ b/tools/cmd/audit-deploy/main.go @@ -0,0 +1,212 @@ +// audit-deploy: sanity-check deployed-addresses.json against a chain. +// - Code present at each address +// - For FlareTeeManager (diamond proxy): list system-supported platforms via the +// ExtensionManager facet, and for a given extensionId print the owner + +// supported codeHashes. +// - Replays the most recent failing call (addTeeVersion) as eth_call to decode +// custom errors when -addTeeVersion is provided. +package main + +import ( + "bytes" + "context" + "flag" + "fmt" + "math/big" + "os" + "strings" + + "concord/tools/pkg/fccutils" + "concord/tools/pkg/support" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/extensionmanager" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/machinemanager" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/verification" +) + +func main() { + af := flag.String("a", "./config/coston/deployed-addresses.json", "addresses file") + rpcURL := flag.String("rpc", "https://coston-api.flare.network/ext/C/rpc", "rpc url") + extID := flag.Int64("ext", -1, "extension id to inspect (e.g. 10)") + codeHash := flag.String("codeHash", "", "0x... code hash for addTeeVersion replay") + platform := flag.String("platform", "", "0x... platform for addTeeVersion replay") + from := flag.String("from", "0xDa50A19aF65655785ab1F4c6a5b0592AFC497C88", "from address for eth_call replay") + version := flag.String("version", "v0.1.0", "version string for addTeeVersion replay") + flag.Parse() + + addr, err := support.ParseAddresses(*af) + if err != nil { + fmt.Fprintf(os.Stderr, "parse addresses: %v\n", err) + os.Exit(1) + } + + cc, err := ethclient.Dial(*rpcURL) + if err != nil { + fmt.Fprintf(os.Stderr, "dial: %v\n", err) + os.Exit(1) + } + ctx := context.Background() + + chainID, err := cc.ChainID(ctx) + if err != nil { + fmt.Fprintf(os.Stderr, "chainId: %v\n", err) + os.Exit(1) + } + bn, _ := cc.BlockNumber(ctx) + fmt.Printf("Chain ID: %s block: %d\n", chainID.String(), bn) + + for _, e := range []struct { + name string + a common.Address + }{ + {"FlareTeeManager", addr.FlareTeeManager}, + {"FlareSystemsManager", addr.FlareSystemManager}, + {"Fdc2Hub", addr.Fdc2Hub}, + } { + code, err := cc.CodeAt(ctx, e.a, nil) + var status string + switch { + case err != nil: + status = "ERROR " + err.Error() + case e.a == (common.Address{}): + status = "ZERO ADDRESS" + case len(code) == 0: + status = "NO CODE" + default: + status = fmt.Sprintf("ok (%d bytes)", len(code)) + } + fmt.Printf(" %-25s %s %s\n", e.name, e.a.Hex(), status) + } + + if addr.FlareTeeManager == (common.Address{}) { + return + } + er, err := extensionmanager.NewExtensionManager(addr.FlareTeeManager, cc) + if err != nil { + fmt.Fprintf(os.Stderr, "bind ExtensionManager: %v\n", err) + return + } + opts := &bind.CallOpts{Context: ctx} + + plats, perr := er.GetSystemSupportedPlatforms(opts) + fmt.Printf("\n=== ExtensionManager facet @ FlareTeeManager %s ===\n", addr.FlareTeeManager.Hex()) + if perr != nil { + fmt.Printf(" getSystemSupportedPlatforms ERROR: %v\n", perr) + } else { + fmt.Printf(" systemSupportedPlatforms (%d):\n", len(plats)) + for _, p := range plats { + fmt.Printf(" %s %q\n", common.Hash(p).Hex(), bytes32ToString(p)) + } + } + cnt, cerr := er.NextPublicExtensionId(opts) + if cerr == nil { + fmt.Printf(" nextPublicExtensionId: %s\n", cnt.String()) + } + + if *extID >= 0 { + eid := big.NewInt(*extID) + fmt.Printf("\n=== Extension id=%s ===\n", eid.String()) + owner, err := er.GetExtensionOwner(opts, eid) + if err != nil { + fmt.Printf(" getExtensionOwner ERROR: %v\n", err) + } else { + fmt.Printf(" owner: %s\n", owner.Hex()) + } + ch, err := er.GetSupportedCodeHashes(opts, eid) + if err != nil { + fmt.Printf(" getSupportedCodeHashes ERROR: %v\n", err) + } else { + fmt.Printf(" supportedCodeHashes (%d):\n", len(ch)) + for _, h := range ch { + fmt.Printf(" %s\n", common.Hash(h).Hex()) + } + } + kt, err := er.GetSupportedKeyTypes(opts, eid) + if err == nil { + fmt.Printf(" supportedKeyTypes (%d):\n", len(kt)) + for _, k := range kt { + fmt.Printf(" %s %q\n", common.Hash(k).Hex(), bytes32ToString(k)) + } + } + } + + if *codeHash != "" && *platform != "" && *extID >= 0 { + fmt.Printf("\n=== addTeeVersion eth_call replay (decoding revert) ===\n") + replayAddTeeVersion(cc, addr.FlareTeeManager, common.HexToAddress(*from), big.NewInt(*extID), *version, common.HexToHash(*codeHash), common.HexToHash(*platform)) + } +} + +func replayAddTeeVersion(cc *ethclient.Client, to, from common.Address, extID *big.Int, version string, codeHash, platform common.Hash) { + emABI, err := extensionmanager.ExtensionManagerMetaData.GetAbi() + if err != nil { + fmt.Printf(" load ABI: %v\n", err) + return + } + versionBytes, verr := fccutils.StringToBytes32(version) + if verr != nil { + fmt.Printf(" version: %v\n", verr) + return + } + calldata, err := emABI.Pack("addTeeVersion", extID, versionBytes, codeHash, [][32]byte{platform}) + if err != nil { + fmt.Printf(" pack: %v\n", err) + return + } + msg := ethereum.CallMsg{From: from, To: &to, Data: calldata} + _, callErr := cc.CallContract(context.Background(), msg, nil) + if callErr == nil { + fmt.Println(" eth_call succeeded — tx-time-only revert (gas/state race?)") + return + } + revertData := fccutils.ExtractRevertData(callErr) + if len(revertData) < 4 { + fmt.Printf(" no/short revert data: %v\n", callErr) + return + } + selector := revertData[:4] + if bytes.Equal(selector, []byte{0x08, 0xc3, 0x79, 0xa0}) { + if reason, err := abi.UnpackRevert(revertData); err == nil { + fmt.Printf(" revert reason (string): %q\n", reason) + return + } + } + for _, src := range []struct { + name string + md *bind.MetaData + }{ + {"ExtensionManager", extensionmanager.ExtensionManagerMetaData}, + {"MachineManager", machinemanager.MachineManagerMetaData}, + {"Verification", verification.VerificationMetaData}, + } { + a, err := src.md.GetAbi() + if err != nil { + continue + } + for errName, eDef := range a.Errors { + if !bytes.Equal(eDef.ID[:4], selector) { + continue + } + args, unpackErr := eDef.Inputs.Unpack(revertData[4:]) + if unpackErr != nil { + fmt.Printf(" revert -> %s.%s (unpack failed: %v) data=0x%x\n", src.name, errName, unpackErr, revertData) + } else { + fmt.Printf(" revert -> %s.%s%v\n", src.name, errName, args) + } + return + } + } + fmt.Printf(" revert with unknown selector 0x%x (full: 0x%x)\n", selector, revertData) +} + +func bytes32ToString(b [32]byte) string { + end := len(b) + for end > 0 && b[end-1] == 0 { + end-- + } + return strings.TrimSpace(string(b[:end])) +} diff --git a/tools/cmd/deploy-contract/main.go b/tools/cmd/deploy-contract/main.go new file mode 100644 index 0000000..d4eeefb --- /dev/null +++ b/tools/cmd/deploy-contract/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + + "concord/tools/pkg/configs" + "concord/tools/pkg/fccutils" + "concord/tools/pkg/support" + instrutils "concord/tools/pkg/utils" + "concord/tools/pkg/validate" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/flare-foundation/go-flare-common/pkg/logger" +) + +func main() { + af := flag.String("a", configs.AddressesFile, "file with deployed addresses") + cf := flag.String("c", configs.ChainNodeURL, "chain node url") + outFile := flag.String("o", "", "write deployed address to this file (optional)") + preflightOnly := flag.Bool("preflight-only", false, "run validation checks and exit without deploying") + flag.Parse() + + testSupport, err := support.DefaultSupport(*af, *cf) + if err != nil { + fccutils.FatalWithCause(err) + } + + // --- Pre-flight validation --- + deployer := crypto.PubkeyToAddress(testSupport.Prv.PublicKey) + logger.Infof("Deployer: %s", deployer.Hex()) + logger.Infof("Chain ID: %s", testSupport.ChainID.String()) + logger.Infof("FlareTeeManager: %s", testSupport.Addresses.FlareTeeManager.Hex()) + + if err := validate.AddressNotZero(testSupport.Addresses.FlareTeeManager, "FlareTeeManager"); err != nil { + fccutils.FatalWithCause(err) + } + if err := validate.AddressHasCode(testSupport.ChainClient, testSupport.Addresses.FlareTeeManager, "FlareTeeManager"); err != nil { + fccutils.FatalWithCause(err) + } + if err := validate.KeyHasFunds(testSupport.ChainClient, testSupport.Prv, validate.MinDeployBalance); err != nil { + fccutils.FatalWithCause(err) + } + + if *preflightOnly { + logger.Infof("Pre-flight checks passed. Exiting without deploying.") + return + } + + logger.Infof("Deploying InstructionSender contract...") + address, _, err := instrutils.DeployInstructionSender(testSupport) + if err != nil { + fccutils.FatalWithCause(err) + } + + logger.Infof("InstructionSender deployed at: %s", address.Hex()) + + // Optionally write address to file for script consumption + if *outFile != "" { + os.MkdirAll(filepath.Dir(*outFile), 0755) + os.WriteFile(*outFile, []byte(address.Hex()), 0644) + } + + // Machine-readable output on stdout (for scripts) + fmt.Println(address.Hex()) +} diff --git a/tools/cmd/query-tee/main.go b/tools/cmd/query-tee/main.go new file mode 100644 index 0000000..e83a40c --- /dev/null +++ b/tools/cmd/query-tee/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "context" + "flag" + "fmt" + "math/big" + "os" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/machinemanager" +) + +func main() { + rpc := flag.String("rpc", "https://coston2-api.flare.network/ext/C/rpc", "rpc url") + reg := flag.String("reg", "0x5918Cd58e5caf755b8584649Aa24077822F87613", "TeeMachineRegistry address") + listExt := flag.Int64("ext", -1, "list active TEEs in extension id (e.g. 0 for FTDC, 1588 for user)") + flag.Parse() + + cc, err := ethclient.Dial(*rpc) + if err != nil { + fmt.Fprintf(os.Stderr, "dial: %v\n", err) + os.Exit(1) + } + mm, err := machinemanager.NewMachineManager(common.HexToAddress(*reg), cc) + if err != nil { + fmt.Fprintf(os.Stderr, "bind: %v\n", err) + os.Exit(1) + } + + opts := &bind.CallOpts{Context: context.Background()} + + if *listExt >= 0 { + ext := big.NewInt(*listExt) + fmt.Printf("\n=== Active TEEs for extensionId=%s ===\n", ext.String()) + out, err := mm.GetActiveTeeMachines(opts, ext) + if err != nil { + fmt.Printf("getActiveTeeMachines ERROR: %v\n", err) + } else { + for i, id := range out.TeeIds { + fmt.Printf(" %d: %s url=%q\n", i, id.Hex(), out.Urls[i]) + } + if len(out.TeeIds) == 0 { + fmt.Println(" (none)") + } + } + } + + for _, raw := range flag.Args() { + id := common.HexToAddress(raw) + fmt.Printf("\n=== TEE %s ===\n", id.Hex()) + + m, err := mm.GetTeeMachine(opts, id) + if err != nil { + fmt.Printf(" getTeeMachine ERROR: %v\n", err) + } else { + fmt.Printf(" getTeeMachine: teeId=%s teeProxyId=%s url=%q\n", m.TeeId.Hex(), m.TeeProxyId.Hex(), m.Url) + if m.TeeId == (common.Address{}) { + fmt.Println(" -> EMPTY/UNREGISTERED") + } + } + + st, err := mm.GetTeeMachineStatus(opts, id) + if err != nil { + fmt.Printf(" getTeeMachineStatus ERROR: %v\n", err) + } else { + fmt.Printf(" getTeeMachineStatus: %d\n", st) + } + + owner, err := mm.GetTeeMachineOwner(opts, id) + if err != nil { + fmt.Printf(" getTeeMachineOwner ERROR: %v\n", err) + } else { + fmt.Printf(" getTeeMachineOwner: %s\n", owner.Hex()) + } + + extID, err := mm.GetExtensionId(opts, id) + if err != nil { + fmt.Printf(" getExtensionId ERROR: %v\n", err) + } else { + fmt.Printf(" getExtensionId: %s\n", extID.String()) + } + + ts, err := mm.GetLastStatusChangeTs(opts, id) + if err != nil { + fmt.Printf(" getLastStatusChangeTs ERROR: %v\n", err) + } else { + fmt.Printf(" getLastStatusChangeTs: %s\n", ts.String()) + } + + spid, err := mm.GetInitialSigningPolicyId(opts, id) + if err != nil { + fmt.Printf(" getInitialSigningPolicyId ERROR: %v\n", err) + } else { + fmt.Printf(" getInitialSigningPolicyId: %d\n", spid) + } + } +} diff --git a/tools/cmd/register-extension/main.go b/tools/cmd/register-extension/main.go new file mode 100644 index 0000000..e556258 --- /dev/null +++ b/tools/cmd/register-extension/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "flag" + "fmt" + + "concord/tools/pkg/configs" + "concord/tools/pkg/fccutils" + "concord/tools/pkg/support" + "concord/tools/pkg/validate" + + "github.com/ethereum/go-ethereum/common" + "github.com/flare-foundation/go-flare-common/pkg/logger" +) + +func main() { + af := flag.String("a", configs.AddressesFile, "file with deployed addresses") + cf := flag.String("c", configs.ChainNodeURL, "chain node url") + instructionSenderF := flag.String("instructionSender", "", "InstructionSender contract address (required)") + governanceHashF := flag.String("governanceHash", "", "governance hash (optional)") + flag.Parse() + + if *instructionSenderF == "" { + logger.Fatal("--instructionSender flag is required") + } + + testSupport, err := support.DefaultSupport(*af, *cf) + if err != nil { + fccutils.FatalWithCause(err) + } + + governanceHash := common.HexToHash(*governanceHashF) + instructionSenderAddress := common.HexToAddress(*instructionSenderF) + + // Pre-flight: verify instruction sender has code on-chain + if err := validate.AddressHasCode(testSupport.ChainClient, instructionSenderAddress, "InstructionSender"); err != nil { + fccutils.FatalWithCause(err) + } + + logger.Infof("Registering extension with InstructionSender %s...", instructionSenderAddress.Hex()) + extensionID, err := fccutils.SetupExtension(testSupport, governanceHash, instructionSenderAddress, common.Address{}) + if err != nil { + fccutils.FatalWithCause(err) + } + + if extensionID == nil || extensionID.Sign() <= 0 { + logger.Warnf("WARNING: extension ID is %v — verify this is expected", extensionID) + } + + extensionIDHex := fmt.Sprintf("0x%064x", extensionID) + logger.Infof("Extension registered with ID: %s", extensionIDHex) + + // Machine-readable output on stdout + fmt.Println(extensionIDHex) +} diff --git a/tools/cmd/register-tee/main.go b/tools/cmd/register-tee/main.go new file mode 100644 index 0000000..958cebe --- /dev/null +++ b/tools/cmd/register-tee/main.go @@ -0,0 +1,73 @@ +package main + +import ( + "concord/tools/pkg/configs" + "concord/tools/pkg/fccutils" + "concord/tools/pkg/support" + "encoding/hex" + "flag" + "os" + + "github.com/flare-foundation/go-flare-common/pkg/logger" +) + +func main() { + af := flag.String("a", configs.AddressesFile, "file with deployed addresses") + cf := flag.String("c", configs.ChainNodeURL, "chain node url") + pf := flag.String("p", configs.ExtensionProxyURL, "extension proxy url (used to query TEE info)") + hf := flag.String("h", "", "host url to register on-chain (defaults to -p if not set)") + epf := flag.String("ep", "http://localhost:6662", "external proxy url (for FTDC)") + instructionF := flag.String("i", "", "instructionID") + command := flag.String("command", "rap", "steps to run: r=pre-register R=request attestation a=availability check p=promote (deployments want rRap)") + stateFile := flag.String("state", "../config/register-tee.state", "state file for resume support") + resume := flag.Bool("resume", false, "resume from state file (default: start fresh)") + + flag.Parse() + + // Default: start fresh. Only resume if --resume is explicitly passed. + if !*resume { + if err := os.Remove(*stateFile); err != nil && !os.IsNotExist(err) { + logger.Warnf("WARNING: failed to remove stale state file: %v", err) + } + } + + testSupport, err := support.DefaultSupport(*af, *cf) + if err != nil { + fccutils.FatalWithCause(err) + } + + // get teeID from proxy + teeInfo, err := fccutils.TeeInfo(*pf) + if err != nil { + fccutils.FatalWithCause(err) + } + + teeID, _, err := fccutils.TeeProxyId(teeInfo) + if err != nil { + fccutils.FatalWithCause(err) + } + + ftdcTeeID, _, err := fccutils.GetTeeProxyID(*epf) + if err != nil { + fccutils.FatalWithCause(err) + } + + // to check if things are ok + _, _, err = fccutils.GetCodeHashAndPlatform(teeInfo) + if err != nil { + fccutils.FatalWithCause(err) + } + + hostURL := *hf + if hostURL == "" { + hostURL = *pf + } + + logger.Infof("Registration of TEE with ID %s", hex.EncodeToString(teeID[:])) + err = fccutils.RegisterNode(testSupport, teeInfo, hostURL, *epf, ftdcTeeID, *command, *instructionF, *stateFile) + if err != nil { + fccutils.FatalWithCause(err) + } + + logger.Infof("Registered TEE node with id %s", teeID) +} diff --git a/tools/cmd/run-test/main.go b/tools/cmd/run-test/main.go new file mode 100644 index 0000000..844c1d1 --- /dev/null +++ b/tools/cmd/run-test/main.go @@ -0,0 +1,113 @@ +// Command run-test sends Concord's encrypted FCC operations through the +// official scaffold path and verifies the returned ActionResult envelopes. +// It deliberately does not materialize an allocation: that step belongs to +// the onchain CapitalFacility verifier boundary. +package main + +import ( + "encoding/json" + "flag" + "os" + "strings" + "time" + + "concord/tools/pkg/configs" + "concord/tools/pkg/fccutils" + "concord/tools/pkg/support" + instrutils "concord/tools/pkg/utils" + + "github.com/ethereum/go-ethereum/common" + "github.com/flare-foundation/go-flare-common/pkg/logger" + teeTypes "github.com/flare-foundation/tee-node/pkg/types" + "github.com/pkg/errors" +) + +func main() { + addressesFile := flag.String("a", configs.AddressesFile, "file with deployed addresses") + chainURL := flag.String("c", configs.ChainNodeURL, "chain RPC URL") + proxyURL := flag.String("p", configs.ExtensionProxyURL, "extension proxy URL") + instructionSender := flag.String("instructionSender", "", "ConcordInstructionSender address") + quoteFiles := flag.String("quotes", "", "comma-separated JSON files containing signed QuoteRequest payloads") + finalizeFile := flag.String("finalize", "", "JSON file containing FinalizeRoundRequest; quotes may be omitted when stored in the TEE") + outFile := flag.String("out", "", "write the verified finalization response to this file") + flag.Parse() + + if *instructionSender == "" || *quoteFiles == "" || *finalizeFile == "" { + logger.Fatal("--instructionSender, --quotes, and --finalize are required") + } + + s, err := support.DefaultSupport(*addressesFile, *chainURL) + if err != nil { + fccutils.FatalWithCause(err) + } + sender := common.HexToAddress(*instructionSender) + if err := instrutils.SetExtensionId(s, sender); err != nil && !strings.Contains(strings.ToLower(err.Error()), "already set") { + fccutils.FatalWithCause(errors.Errorf("setExtensionId failed: %s", err)) + } + + for _, path := range strings.Split(*quoteFiles, ",") { + path = strings.TrimSpace(path) + payload, err := os.ReadFile(path) + if err != nil { + fccutils.FatalWithCause(errors.Errorf("read quote %s: %s", path, err)) + } + encrypted, err := fccutils.EncryptForTEE(*proxyURL, payload) + if err != nil { + fccutils.FatalWithCause(err) + } + instructionID, txHash, err := instrutils.SendSubmitQuote(s, sender, encrypted) + if err != nil { + fccutils.FatalWithCause(err) + } + logger.Infof("SUBMIT_QUOTE sent: instruction=%s tx=%s", instructionID.Hex(), txHash.Hex()) + if _, err := verifyAction(*proxyURL, instructionID); err != nil { + fccutils.FatalWithCause(err) + } + } + + finalizePayload, err := os.ReadFile(*finalizeFile) + if err != nil { + fccutils.FatalWithCause(err) + } + encrypted, err := fccutils.EncryptForTEE(*proxyURL, finalizePayload) + if err != nil { + fccutils.FatalWithCause(err) + } + instructionID, txHash, err := instrutils.SendFinalizeRound(s, sender, encrypted) + if err != nil { + fccutils.FatalWithCause(err) + } + logger.Infof("FINALIZE_ROUND sent: instruction=%s tx=%s", instructionID.Hex(), txHash.Hex()) + response, err := verifyAction(*proxyURL, instructionID) + if err != nil { + fccutils.FatalWithCause(err) + } + if *outFile != "" { + if err := os.WriteFile(*outFile, response.Result.Data, 0600); err != nil { + fccutils.FatalWithCause(err) + } + logger.Infof("verified FCC allocation response written to %s", *outFile) + } +} + +func verifyAction(proxyURL string, instructionID common.Hash) (*teeTypes.ActionResponse, error) { + response, err := fccutils.ActionResult(proxyURL, instructionID) + if err != nil { + return nil, err + } + if response.Result.Status == 2 { + return nil, errors.New("FCC action remained pending after polling") + } + if response.Result.Status == 0 { + return nil, errors.Errorf("FCC action failed: %s", response.Result.Log) + } + if len(response.Result.Data) == 0 { + return nil, errors.New("FCC action returned no data") + } + var probe map[string]any + if err := json.Unmarshal(response.Result.Data, &probe); err != nil { + return nil, errors.Errorf("FCC result data is not JSON: %s", err) + } + logger.Infof("FCC action verified at %s: %d fields", time.Now().UTC().Format(time.RFC3339), len(probe)) + return response, nil +} diff --git a/tools/cmd/set-governance/main.go b/tools/cmd/set-governance/main.go new file mode 100644 index 0000000..64f6faf --- /dev/null +++ b/tools/cmd/set-governance/main.go @@ -0,0 +1,101 @@ +// set-governance: register the TEE governance signer set + threshold for this +// extension on-chain. +// +// Reads the signer set from GOVERNANCE_SIGNERS (comma-separated 0x addresses) +// and the threshold from GOVERNANCE_THRESHOLD. Both default to "the deployer +// alone, threshold 1" when unset, so a developer who configures nothing still +// gets a working setup. These MUST match the same env vars passed to the TEE +// node (it derives its governanceHash from them), or register-tee fails with +// InvalidGovernanceHash. +package main + +import ( + "crypto/ecdsa" + "flag" + "os" + "strconv" + "strings" + + "concord/tools/pkg/configs" + "concord/tools/pkg/fccutils" + "concord/tools/pkg/support" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/flare-foundation/go-flare-common/pkg/logger" +) + +func main() { + af := flag.String("a", configs.AddressesFile, "file with deployed addresses") + cf := flag.String("c", configs.ChainNodeURL, "chain node url") + pf := flag.String("p", configs.ExtensionProxyURL, "extension proxy url (used to query the extension id)") + flag.Parse() + + testSupport, err := support.DefaultSupport(*af, *cf) + if err != nil { + fccutils.FatalWithCause(err) + } + + // Extension owner key (falls back to the deployment key, like the other tools). + var privKey *ecdsa.PrivateKey + if privKeyString := os.Getenv("EXTENSION_OWNER_KEY"); privKeyString != "" { + privKeyString = strings.TrimPrefix(privKeyString, "0x") + privKeyString = strings.TrimPrefix(privKeyString, "0X") + privKey, err = crypto.HexToECDSA(privKeyString) + if err != nil { + fccutils.FatalWithCause(err) + } + } else { + privKey = testSupport.Prv + } + deployer := crypto.PubkeyToAddress(privKey.PublicKey) + + // Default governance signer: INITIAL_OWNER if set (this is what the node's + // compose env defaults to), else the deployer. Keeping both sides on the + // same default is what makes the node's governanceHash match the chain. + defaultSigner := deployer + if io := strings.TrimSpace(os.Getenv("INITIAL_OWNER")); io != "" { + defaultSigner = common.HexToAddress(io) + } + + // Resolve the extension id from the proxy /info. + teeInfo, err := fccutils.TeeInfo(*pf) + if err != nil { + fccutils.FatalWithCause(err) + } + extensionID := teeInfo.MachineData.ExtensionID.Big() + + signers := parseSigners(os.Getenv("GOVERNANCE_SIGNERS"), defaultSigner) + threshold := parseThreshold(os.Getenv("GOVERNANCE_THRESHOLD")) + + logger.Infof("Extension ID: %s", extensionID.String()) + logger.Infof("Governance signers: %v", signers) + logger.Infof("Governance threshold: %d", threshold) + + if err := fccutils.SetTeeGovernance(testSupport, privKey, extensionID, signers, threshold); err != nil { + fccutils.FatalWithCause(err) + } +} + +// parseSigners parses a comma-separated list of 0x addresses, falling back to +// the deployer as the sole signer when empty. +func parseSigners(raw string, fallback common.Address) []common.Address { + var signers []common.Address + for _, part := range strings.Split(raw, ",") { + if part = strings.TrimSpace(part); part != "" { + signers = append(signers, common.HexToAddress(part)) + } + } + if len(signers) == 0 { + return []common.Address{fallback} + } + return signers +} + +// parseThreshold parses GOVERNANCE_THRESHOLD, defaulting to 1 on empty/invalid. +func parseThreshold(raw string) uint64 { + if v, err := strconv.ParseUint(strings.TrimSpace(raw), 10, 64); err == nil && v > 0 { + return v + } + return 1 +} diff --git a/tools/cmd/start-proxy/main.go b/tools/cmd/start-proxy/main.go new file mode 100644 index 0000000..10f9acb --- /dev/null +++ b/tools/cmd/start-proxy/main.go @@ -0,0 +1,90 @@ +// TEMPORARY: This command starts the extension proxy as a Go process. +// It will be replaced by a Docker container once the Dockerfile is implemented. +// See EXTENSION-TEMPLATE-SPEC.md §5 for the Docker approach. +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "path/filepath" + "runtime" + "syscall" + + "concord/tools/pkg/fccutils" + + "github.com/flare-foundation/go-flare-common/pkg/logger" + proxyConfig "github.com/flare-foundation/tee-proxy/pkg/config" + initProxy "github.com/flare-foundation/tee-proxy/pkg/init" + "github.com/joho/godotenv" +) + +func main() { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + loadEnv() + + proxyConfigFile := findProxyConfig() + + initProxy.Init(ctx, proxyConfigFile) + logger.Infof("Started extension proxy") + + err := logProxyAndTeeIds(proxyConfigFile) + if err != nil { + logger.Warnf("Failed to log proxy and tee IDs: %v", err) + } + + <-ctx.Done() + logger.Infof("Received shutdown signal, shutting down") +} + +func projectRoot() string { + _, thisFile, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(thisFile), "..", "..", "..") +} + +func loadEnv() { + // Try project-root .env first (works even when CWD is tools/). + rootEnv := filepath.Join(projectRoot(), ".env") + if err := godotenv.Load(rootEnv); err != nil { + // Fallback to CWD .env. + if err := godotenv.Load(); err != nil { + fmt.Printf("Warning: Error loading .env file: %v\n", err) + } + } +} + +func findProxyConfig() string { + // Allow override via PROXY_CONFIG env var. + if envConfig := os.Getenv("PROXY_CONFIG"); envConfig != "" { + return envConfig + } + + // Try project-root relative path first. + candidate := filepath.Join(projectRoot(), "config", "proxy", "extension_proxy.toml") + if _, err := os.Stat(candidate); err == nil { + return candidate + } + + // Fallback to working directory relative. + return "./config/proxy/extension_proxy.toml" +} + +func logProxyAndTeeIds(configFile string) error { + config, err := proxyConfig.Read(configFile) + if err != nil { + return fmt.Errorf("failed to read proxy config: %w", err) + } + + proxyURL := fmt.Sprintf("http://localhost:%s", config.Ports.External) + + teeID, proxyID, err := fccutils.GetTeeProxyID(proxyURL) + if err != nil { + return fmt.Errorf("failed to extract teeID and proxyID: %w", err) + } + + logger.Infof("Proxy started - TeeID: %s, ProxyID: %s", teeID.Hex(), proxyID.Hex()) + return nil +} diff --git a/tools/cmd/verify-deploy/main.go b/tools/cmd/verify-deploy/main.go new file mode 100644 index 0000000..ec4f501 --- /dev/null +++ b/tools/cmd/verify-deploy/main.go @@ -0,0 +1,105 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "concord/tools/pkg/configs" + "concord/tools/pkg/support" + "concord/tools/pkg/validate" + + "github.com/ethereum/go-ethereum/common" +) + +// stepsNeedingChain are the step values that require a live chain client. +// services, tee-version, tee-machine, and test only read env vars / hit HTTP endpoints. +var stepsNeedingChain = map[string]bool{ + "deploy": true, + "register": true, + "all": true, +} + +func main() { + af := flag.String("a", configs.AddressesFile, "file with deployed addresses") + cf := flag.String("c", configs.ChainNodeURL, "chain node url") + configPath := flag.String("config", "", "path to extension.env for post-deploy validation") + step := flag.String("step", "all", "which step to check: deploy, register, services, tee-version, tee-machine, test, or all") + checksFilter := flag.String("checks", "", "comma-separated check IDs to include (e.g. D5,R2); all others are hidden") + jsonOut := flag.Bool("json", false, "output as JSON instead of colored terminal") + flag.Parse() + + report := &validate.Report{} + + switch *step { + case "deploy", "register", "all": + s, err := support.DefaultSupport(*af, *cf) + if err != nil { + fmt.Fprintf(os.Stderr, "Error initializing support: %v\n", err) + fmt.Fprintf(os.Stderr, "\nHints:\n") + fmt.Fprintf(os.Stderr, " - Check that the addresses file exists: %s\n", *af) + fmt.Fprintf(os.Stderr, " - Check that the chain node is running: %s\n", *cf) + fmt.Fprintf(os.Stderr, " - If using a custom key, ensure DEPLOYMENT_PRIVATE_KEY is set in .env\n") + os.Exit(1) + } + + addresses := map[string]common.Address{ + "FlareTeeManager": s.Addresses.FlareTeeManager, + } + + switch *step { + case "deploy": + validate.RegisterDeployChecks(report, s.ChainClient, s.Prv, addresses, *configPath) + case "register": + validate.RegisterRegistrationChecks(report, s.ChainClient, s.Prv, s.TeeExtensionRegistry, s.TeeOwnerAllowlist, *configPath) + case "all": + validate.RegisterDeployChecks(report, s.ChainClient, s.Prv, addresses, *configPath) + validate.RegisterRegistrationChecks(report, s.ChainClient, s.Prv, s.TeeExtensionRegistry, s.TeeOwnerAllowlist, *configPath) + validate.RegisterServicesChecks(report, *configPath) + validate.RegisterTeeVersionChecks(report, *configPath) + validate.RegisterTeeMachineChecks(report, *configPath) + validate.RegisterTestChecks(report, *configPath) + } + + case "services": + validate.RegisterServicesChecks(report, *configPath) + case "tee-version": + validate.RegisterTeeVersionChecks(report, *configPath) + case "tee-machine": + validate.RegisterTeeMachineChecks(report, *configPath) + case "test": + validate.RegisterTestChecks(report, *configPath) + default: + fmt.Fprintf(os.Stderr, "Unknown step: %q\n", *step) + fmt.Fprintf(os.Stderr, "Valid steps: deploy, register, services, tee-version, tee-machine, test, all\n") + os.Exit(1) + } + + // If --checks is set, filter results to only the requested IDs. + if *checksFilter != "" { + allowed := make(map[string]bool) + for _, id := range strings.Split(*checksFilter, ",") { + if t := strings.TrimSpace(id); t != "" { + allowed[t] = true + } + } + filtered := report.Results[:0] + for _, res := range report.Results { + if allowed[res.ID] { + filtered = append(filtered, res) + } + } + report.Results = filtered + } + + if *jsonOut { + report.PrintJSON() + } else { + report.Print() + } + + if report.HasFailures() { + os.Exit(1) + } +} diff --git a/tools/go.mod b/tools/go.mod new file mode 100644 index 0000000..4e5fe74 --- /dev/null +++ b/tools/go.mod @@ -0,0 +1,86 @@ +module concord/tools + +go 1.25.1 + +require ( + github.com/ethereum/go-ethereum v1.17.4 + github.com/flare-foundation/go-flare-common v1.2.2-0.20260727094511-09a10067e6a4 + github.com/flare-foundation/tee-node v0.0.24 + github.com/flare-foundation/tee-proxy v0.0.18 + github.com/joho/godotenv v1.5.1 + github.com/pkg/errors v0.9.1 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/redis/go-redis/v9 v9.12.1 // indirect + golang.org/x/text v0.40.0 // indirect + gorm.io/driver/mysql v1.6.0 // indirect + gorm.io/gorm v1.31.0 // indirect +) + +require ( + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/firestore v1.21.0 // indirect + cloud.google.com/go/longrunning v0.7.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251106012722-c7be33e82a11 // indirect + github.com/bits-and-blooms/bitset v1.24.3 // indirect + github.com/consensys/gnark-crypto v0.19.2 // indirect + github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect + github.com/deckarep/golang-set/v2 v2.8.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fjl/jsonw v0.1.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/supranational/blst v0.3.16 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.54.0 // indirect + golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/time v0.14.0 // indirect + gonum.org/v1/gonum v0.17.0 // indirect + google.golang.org/api v0.256.0 // indirect + google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/grpc v1.79.1 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect +) diff --git a/tools/go.sum b/tools/go.sum new file mode 100644 index 0000000..1960a25 --- /dev/null +++ b/tools/go.sum @@ -0,0 +1,324 @@ +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= +cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/firestore v1.21.0 h1:BhopUsx7kh6NFx77ccRsHhrtkbJUmDAxNY3uapWdjcM= +cloud.google.com/go/firestore v1.21.0/go.mod h1:1xH6HNcnkf/gGyR8udd6pFO4Z7GWJSwLKQMx/u6UrP4= +cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= +cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251106012722-c7be33e82a11 h1:cP8UbFCldZ6uVbZnI3/EI4FSdO9NaYnx4hY+tyW6FbU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251106012722-c7be33e82a11/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= +github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.24.3 h1:Bte86SlO3lwPQqww+7BE9ZuUCKIjfqnG5jtEyqA9y9Y= +github.com/bits-and-blooms/bitset v1.24.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/btcsuite/btcd/btcec/v2 v2.3.5 h1:dpAlnAwmT1yIBm3exhT1/8iUSD98RDJM5vqJVQDQLiU= +github.com/btcsuite/btcd/btcec/v2 v2.3.5/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/gnark-crypto v0.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80= +github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= +github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= +github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= +github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= +github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= +github.com/ethereum/go-ethereum v1.17.4 h1:uA4q+qiLp7QImBsjdRbINu8iX6OEVmj4DPc5/E5Fsxc= +github.com/ethereum/go-ethereum v1.17.4/go.mod h1:qMdgwqqRAen+aT8P7KKQKi0Qt6RzG4cfejVAbCpJgqA= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/fjl/jsonw v0.1.0 h1:V3MyR79fjLpn/+bMgvegdGUIhoJOzjmqWcKDgcOmY1I= +github.com/fjl/jsonw v0.1.0/go.mod h1:2KMLevM6FXEJnfhtk7naXu9vZdVfOma1GlnGdPRlumU= +github.com/flare-foundation/go-flare-common v1.2.2-0.20260727094511-09a10067e6a4 h1:B+WFq+c1k1Y1OcJh2jrojcvo4d9lslPNn1ze5IrebbM= +github.com/flare-foundation/go-flare-common v1.2.2-0.20260727094511-09a10067e6a4/go.mod h1:H9I3pwQAyiOK+jC7tFHYRZa5qxDeOldW+5VpRiUs6rA= +github.com/flare-foundation/tee-node v0.0.24 h1:NZxm3EFlSvzkvwKIB61B0P+lSTVkaDQ4lnYzDU07DFE= +github.com/flare-foundation/tee-node v0.0.24/go.mod h1:4BLiHby94bTeiARRXqXFtFPF850KPG0YpvOaOduv4rA= +github.com/flare-foundation/tee-proxy v0.0.18 h1:ij10h/5svrDYqoEJEIj5+kNVLx5zJLTdRdnr6OZ0ALU= +github.com/flare-foundation/tee-proxy v0.0.18/go.mod h1:kU3lvYeBCIcm4hftggEFb7KCmzpclFXZgMXD6Uw+OL4= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac= +github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= +github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/stun/v3 v3.1.2 h1:86IhD8wFn6IDW4b1/0QzoQS+f5PeA8OHHRn8UZW5ErY= +github.com/pion/stun/v3 v3.1.2/go.mod h1:H7gDic7nNwlUL05pbs6T1dtaBehh/KjupxfWw3ZI7cA= +github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= +github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg= +github.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= +golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= +google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= +gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/tools/pkg/configs/configs.go b/tools/pkg/configs/configs.go new file mode 100644 index 0000000..f4c5364 --- /dev/null +++ b/tools/pkg/configs/configs.go @@ -0,0 +1,34 @@ +package configs + +import ( + "encoding/json" + "os" +) + +const ( + ExtensionProxyURL = "http://localhost:6664" + ChainNodeURL = "http://127.0.0.1:8545" +) + +const ( + AddressesFile = "../docker/sim_dump/deployed-addresses.json" + ExtensionProxyConfigFile = "./configs/proxy/extension_proxy.toml" +) + +const ( + ExtConfigurationPort = 5501 // port on tee for setting the configurations (proxyURL, initialOwner, extensionID) + ExtProxyInternalPort = 6663 // internal port for tee to get actions from the queue from the proxy + ExtensionServerPort = 7701 // port on the tee that the extension server calls for signing, encrypting, etc. + ExtensionPort = 7702 // the port on the extension server that the tee calls to send non system actions +) + +func ReadAddresses[T any](filePath string, dest *T) error { + file, err := os.ReadFile(filePath) + if err != nil { + return err + } + + err = json.Unmarshal(file, dest) + + return err +} diff --git a/tools/pkg/contracts/concordsender/ConcordInstructionSender.abi b/tools/pkg/contracts/concordsender/ConcordInstructionSender.abi new file mode 100644 index 0000000..2ed6066 --- /dev/null +++ b/tools/pkg/contracts/concordsender/ConcordInstructionSender.abi @@ -0,0 +1,141 @@ +[ + { + "type": "constructor", + "inputs": [ + { + "name": "extensionRegistry_", + "type": "address", + "internalType": "contract ITeeExtensionRegistry" + }, + { + "name": "machineRegistry_", + "type": "address", + "internalType": "contract ITeeMachineRegistry" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "OP_COMMAND_FINALIZE_ROUND", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "OP_COMMAND_SUBMIT_QUOTE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "OP_TYPE_CONCORD", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "TEE_EXTENSION_REGISTRY", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract ITeeExtensionRegistry" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "TEE_MACHINE_REGISTRY", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract ITeeMachineRegistry" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "extensionId", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "sendFinalizeRound", + "inputs": [ + { + "name": "encryptedMessage", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "instructionId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "sendSubmitQuote", + "inputs": [ + { + "name": "encryptedMessage", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [ + { + "name": "instructionId", + "type": "bytes32", + "internalType": "bytes32" + } + ], + "stateMutability": "payable" + }, + { + "type": "function", + "name": "setExtensionId", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + } +] diff --git a/tools/pkg/contracts/concordsender/ConcordInstructionSender.bin b/tools/pkg/contracts/concordsender/ConcordInstructionSender.bin new file mode 100644 index 0000000..d67a663 --- /dev/null +++ b/tools/pkg/contracts/concordsender/ConcordInstructionSender.bin @@ -0,0 +1 @@ +60c06040523461005e5761001a61001461016d565b9061042d565b610022610063565b6111ac6104da8239608051818181610156015281816107e7015281816108700152610feb015260a05181818161040d0152610ee501526111ac90f35b610069565b60405190565b5f80fd5b601f801991011690565b634e487b7160e01b5f52604160045260245ffd5b906100959061006d565b810190811060018060401b038211176100ad57604052565b610077565b906100c56100be610063565b928361008b565b565b5f80fd5b60018060a01b031690565b6100df906100cb565b90565b6100eb906100d6565b90565b6100f7816100e2565b036100fe57565b5f80fd5b9050519061010f826100ee565b565b61011a906100d6565b90565b61012681610111565b0361012d57565b5f80fd5b9050519061013e8261011d565b565b9190604083820312610168578061015c610165925f8601610102565b93602001610131565b90565b6100c7565b61018b61168680380380610180816100b2565b928339810190610140565b9091565b90565b6101a66101a16101ab926100cb565b61018f565b6100cb565b90565b6101b790610192565b90565b6101c3906101ae565b90565b90565b6101dd6101d86101e2926101c6565b61018f565b6100cb565b90565b6101ee906101c9565b90565b60209181520190565b5f7f657874656e73696f6e207265676973747279206973207a65726f000000000000910152565b61022e601a6020926101f1565b610237816101fa565b0190565b6102509060208101905f818303910152610221565b90565b1561025a57565b610262610063565b62461bcd60e51b8152806102786004820161023b565b0390fd5b610285906101ae565b90565b5f7f6d616368696e65207265676973747279206973207a65726f0000000000000000910152565b6102bc60186020926101f1565b6102c581610288565b0190565b6102de9060208101905f8183039101526102af565b90565b156102e857565b6102f0610063565b62461bcd60e51b815280610306600482016102c9565b0390fd5b90565b61032161031c610326926101c6565b61018f565b61030a565b90565b5f7f657874656e73696f6e20726567697374727920686173206e6f20636f64650000910152565b61035d601e6020926101f1565b61036681610329565b0190565b61037f9060208101905f818303910152610350565b90565b1561038957565b610391610063565b62461bcd60e51b8152806103a76004820161036a565b0390fd5b5f7f6d616368696e6520726567697374727920686173206e6f20636f646500000000910152565b6103df601c6020926101f1565b6103e8816103ab565b0190565b6104019060208101905f8183039101526103d2565b90565b1561040b57565b610413610063565b62461bcd60e51b815280610429600482016103ec565b0390fd5b61045a610439826101ba565b61045361044d6104485f6101e5565b6100d6565b916100d6565b1415610253565b6104876104668361027c565b61048061047a6104755f6101e5565b6100d6565b916100d6565b14156102e1565b6104ac610493826101ba565b3b6104a66104a05f61030d565b9161030a565b11610382565b6104d16104b88361027c565b3b6104cb6104c55f61030d565b9161030a565b11610404565b60805260a05256fe60806040526004361015610013575b6104be565b61001d5f356100ac565b806315e3a16b146100a757806334759513146100a25780633efb5f101461009d57806341553c091461009857806341be4dc81461009357806362d7076e1461008e578063aa5032c614610089578063d77798a9146100845763dae5e5180361000e57610492565b61045d565b6103d8565b61039e565b610344565b6102eb565b610296565b6101dc565b61011f565b60e01c90565b60405190565b5f80fd5b5f80fd5b5f9103126100ca57565b6100bc565b6d119253905312569157d493d5539160921b90565b6100ec6100cf565b90565b6100f76100e4565b90565b90565b610106906100fa565b9052565b919061011d905f602085019401906100fd565b565b3461014f5761012f3660046100c0565b61014b61013a6100ef565b6101426100b2565b9182918261010a565b0390f35b6100b8565b7f000000000000000000000000000000000000000000000000000000000000000090565b60018060a01b031690565b90565b61019a61019561019f92610178565b610183565b610178565b90565b6101ab90610186565b90565b6101b7906101a2565b90565b6101c3906101ae565b9052565b91906101da905f602085019401906101ba565b565b3461020c576101ec3660046100c0565b6102086101f7610154565b6101ff6100b2565b918291826101c7565b0390f35b6100b8565b5f80fd5b5f80fd5b5f80fd5b5f80fd5b909182601f8301121561025b5781359167ffffffffffffffff831161025657602001926001830284011161025157565b61021d565b610219565b610215565b90602082820312610291575f82013567ffffffffffffffff811161028c576102889201610221565b9091565b610211565b6100bc565b6102be6102ad6102a7366004610260565b906104c6565b6102b56100b2565b9182918261010a565b0390f35b6b5355424d49545f51554f544560a01b90565b6102dd6102c2565b90565b6102e86102d5565b90565b3461031b576102fb3660046100c0565b6103176103066102e0565b61030e6100b2565b9182918261010a565b0390f35b6100b8565b6610d3d390d3d49160ca1b90565b610336610320565b90565b61034161032e565b90565b34610374576103543660046100c0565b61037061035f610339565b6103676100b2565b9182918261010a565b0390f35b6100b8565b90565b61038590610379565b9052565b919061039c905f6020850194019061037c565b565b346103ce576103ae3660046100c0565b6103ca6103b9610513565b6103c16100b2565b91829182610389565b0390f35b6100b8565b5f0190565b34610406576103e83660046100c0565b6103f06107b9565b6103f86100b2565b80610402816103d3565b0390f35b6100b8565b7f000000000000000000000000000000000000000000000000000000000000000090565b610438906101a2565b90565b6104449061042f565b9052565b919061045b905f6020850194019061043b565b565b3461048d5761046d3660046100c0565b61048961047861040b565b6104806100b2565b91829182610448565b0390f35b6100b8565b6104ba6104a96104a3366004610260565b90610993565b6104b16100b2565b9182918261010a565b0390f35b5f80fd5b5f90565b6104e3916104d26104c2565b506104db6102d5565b919091610eaf565b90565b5f90565b5f1c90565b90565b6104fe610503916104ea565b6104ef565b90565b61051090546104f2565b90565b61051b6104e6565b506105255f610506565b90565b90565b61053f61053a61054492610528565b610183565b610379565b90565b60209181520190565b5f7f457874656e73696f6e20494420616c7265616479207365742e00000000000000910152565b6105846019602092610547565b61058d81610550565b0190565b6105a69060208101905f818303910152610577565b90565b156105b057565b6105b86100b2565b62461bcd60e51b8152806105ce60048201610591565b0390fd5b601f801991011690565b634e487b7160e01b5f52604160045260245ffd5b906105fa906105d2565b810190811067ffffffffffffffff82111761061457604052565b6105dc565b60e01b90565b61062881610379565b0361062f57565b5f80fd5b905051906106408261061f565b565b9060208282031261065b57610658915f01610633565b90565b6100bc565b6106686100b2565b3d5f823e3d90fd5b90565b61068761068261068c92610670565b610183565b610379565b90565b61069b62010000610673565b90565b60016106aa9101610379565b90565b6106b690610178565b90565b6106c2816106ad565b036106c957565b5f80fd5b905051906106da826106b9565b565b906020828203126106f5576106f2915f016106cd565b90565b6100bc565b610703906101a2565b90565b5f1b90565b906107175f1991610706565b9181191691161790565b61073561073061073a92610379565b610183565b610379565b90565b90565b9061075561075061075c92610721565b61073d565b825461070b565b9055565b5f7f457874656e73696f6e204944206e6f7420666f756e642e000000000000000000910152565b6107946017602092610547565b61079d81610760565b0190565b6107b69060208101905f818303910152610787565b90565b6107dd6107c55f610506565b6107d76107d15f61052b565b91610379565b146105a9565b610821602061080b7f00000000000000000000000000000000000000000000000000000000000000006101ae565b6327582ad5906108196100b2565b938492610619565b82528180610831600482016103d3565b03915afa90811561098e575f91610960575b5061084c61068f565b5b8061086061085a84610379565b91610379565b101561093e576108be60206108947f00000000000000000000000000000000000000000000000000000000000000006101ae565b632c177358906108b385926108a76100b2565b95869485938493610619565b835260048301610389565b03915afa908115610939575f9161090b575b506108eb6108e56108e0306106fa565b6106ad565b916106ad565b146108fe576108f99061069e565b61084d565b61090991505f610740565b565b61092c915060203d8111610932575b61092481836105f0565b8101906106dc565b5f6108d0565b503d61091a565b610660565b6109466100b2565b62461bcd60e51b81528061095c600482016107a1565b0390fd5b610981915060203d8111610987575b61097981836105f0565b810190610642565b5f610843565b503d61096f565b610660565b6109b09161099f6104c2565b506109a86100e4565b919091610eaf565b90565b5090565b5f7f656e63727970746564206d65737361676520697320656d707479000000000000910152565b6109eb601a602092610547565b6109f4816109b7565b0190565b610a0d9060208101905f8183039101526109de565b90565b15610a1757565b610a1f6100b2565b62461bcd60e51b815280610a35600482016109f8565b0390fd5b90610a4c610a456100b2565b92836105f0565b565b67ffffffffffffffff8111610a665760208091020190565b6105dc565b90929192610a80610a7b82610a4e565b610a39565b9381855260208086019202830192818411610abd57915b838310610aa45750505050565b60208091610ab284866106cd565b815201920191610a97565b61021d565b9080601f83011215610ae057816020610add93519101610a6b565b90565b610215565b90602082820312610b15575f82015167ffffffffffffffff8111610b1057610b0d9201610ac2565b90565b610211565b6100bc565b90565b610b31610b2c610b3692610b1a565b610183565b610379565b90565b610b4290610b1d565b9052565b916020610b67929493610b6060408201965f83019061037c565b0190610b39565b565b90610b7b610b7683610a4e565b610a39565b918252565b369037565b90610baa610b9283610b69565b92602080610ba08693610a4e565b9201910390610b80565b565b610bb660c0610a39565b90565b90610bc3906100fa565b9052565b5f80fd5b67ffffffffffffffff8111610be957610be56020916105d2565b0190565b6105dc565b90825f939282370152565b90929192610c0e610c0982610bcb565b610a39565b93818552602085019082840111610c2a57610c2892610bee565b565b610bc7565b610c3a913691610bf9565b90565b52565b52565b67ffffffffffffffff1690565b610c64610c5f610c6992610528565b610183565b610c43565b90565b90610c7690610c43565b9052565b90610c84906106ad565b9052565b610c91816100fa565b03610c9857565b5f80fd5b90505190610ca982610c88565b565b90602082820312610cc457610cc1915f01610c9c565b90565b6100bc565b5190565b60209181520190565b60200190565b610ce5906106ad565b9052565b90610cf681602093610cdc565b0190565b60200190565b90610d1d610d17610d1084610cc9565b8093610ccd565b92610cd6565b905f5b818110610d2d5750505090565b909192610d46610d406001928651610ce9565b94610cfa565b9101919091610d20565b610d59906100fa565b9052565b5190565b60209181520190565b90825f9392825e0152565b610d94610d9d602093610da293610d8b81610d5d565b93848093610d61565b95869101610d6a565b6105d2565b0190565b60209181520190565b90610dcc610dc6610dbf84610cc9565b8093610da6565b92610cd6565b905f5b818110610ddc5750505090565b909192610df5610def6001928651610ce9565b94610cfa565b9101919091610dcf565b610e0890610c43565b9052565b90610e819060a080610e64610e5260c08501610e2e5f8901515f880190610d50565b610e4060208901516020880190610d50565b60408801518682036040880152610d75565b60608701518582036060870152610daf565b94610e7760808201516080860190610dff565b0151910190610cdc565b90565b9091610e9e610eac9360408401908482035f860152610d00565b916020818403910152610e0c565b90565b90610eb86104c2565b50610ee0610ec78285906109b3565b610ed9610ed35f61052b565b91610379565b1415610a10565b610f097f000000000000000000000000000000000000000000000000000000000000000061042f565b5f63feeabcbf91610f1861113c565b90610f36600194610f41610f2a6100b2565b96879586948594610619565b845260048401610b46565b03915afa9081156110b5575f91611093575b50925f610f5f9061052b565b610f6890610b85565b90610f7161032e565b93929091905f923394610f82610bac565b965f880190610f9091610bb9565b6020870190610f9e91610bb9565b610fa791610c2f565b6040850190610fb591610c3d565b6060840190610fc391610c40565b610fcc90610c50565b6080830190610fda91610c6c565b60a0820190610fe891610c7a565b907f0000000000000000000000000000000000000000000000000000000000000000611013906101ae565b63f731df5391349192919093916110286100b2565b809581946110368394610619565b8352600483019161104692610e84565b03915a94602095f190811561108e575f91611060575b5090565b611081915060203d8111611087575b61107981836105f0565b810190610cab565b5f61105c565b503d61106f565b610660565b6110af91503d805f833e6110a781836105f0565b810190610ae5565b5f610f53565b610660565b5f7f457874656e73696f6e204944206973206e6f74207365742e0000000000000000910152565b6110ee6018602092610547565b6110f7816110ba565b0190565b6111109060208101905f8183039101526110e1565b90565b1561111a57565b6111226100b2565b62461bcd60e51b815280611138600482016110fb565b0390fd5b6111446104e6565b5061116a6111515f610506565b61116361115d5f61052b565b91610379565b1415611113565b6111735f610506565b9056fea2646970667358221220cd918206f70c305fa7ef2530529e648621bf02adf3c8e8e04ab01f66708df78564736f6c634300081b0033 diff --git a/tools/pkg/contracts/concordsender/autogen.go b/tools/pkg/contracts/concordsender/autogen.go new file mode 100644 index 0000000..3aed29e --- /dev/null +++ b/tools/pkg/contracts/concordsender/autogen.go @@ -0,0 +1,452 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package concordsender + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// ConcordInstructionSenderMetaData contains all meta data concerning the ConcordInstructionSender contract. +var ConcordInstructionSenderMetaData = &bind.MetaData{ + ABI: "[{\"type\":\"constructor\",\"inputs\":[{\"name\":\"extensionRegistry_\",\"type\":\"address\",\"internalType\":\"contractITeeExtensionRegistry\"},{\"name\":\"machineRegistry_\",\"type\":\"address\",\"internalType\":\"contractITeeMachineRegistry\"}],\"stateMutability\":\"nonpayable\"},{\"type\":\"function\",\"name\":\"OP_COMMAND_FINALIZE_ROUND\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"OP_COMMAND_SUBMIT_QUOTE\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"OP_TYPE_CONCORD\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"TEE_EXTENSION_REGISTRY\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"contractITeeExtensionRegistry\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"TEE_MACHINE_REGISTRY\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"address\",\"internalType\":\"contractITeeMachineRegistry\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"extensionId\",\"inputs\":[],\"outputs\":[{\"name\":\"\",\"type\":\"uint256\",\"internalType\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"sendFinalizeRound\",\"inputs\":[{\"name\":\"encryptedMessage\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[{\"name\":\"instructionId\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"sendSubmitQuote\",\"inputs\":[{\"name\":\"encryptedMessage\",\"type\":\"bytes\",\"internalType\":\"bytes\"}],\"outputs\":[{\"name\":\"instructionId\",\"type\":\"bytes32\",\"internalType\":\"bytes32\"}],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"setExtensionId\",\"inputs\":[],\"outputs\":[],\"stateMutability\":\"nonpayable\"}]", + Bin: "0x60c06040523461005e5761001a61001461016d565b9061042d565b610022610063565b6111ac6104da8239608051818181610156015281816107e7015281816108700152610feb015260a05181818161040d0152610ee501526111ac90f35b610069565b60405190565b5f80fd5b601f801991011690565b634e487b7160e01b5f52604160045260245ffd5b906100959061006d565b810190811060018060401b038211176100ad57604052565b610077565b906100c56100be610063565b928361008b565b565b5f80fd5b60018060a01b031690565b6100df906100cb565b90565b6100eb906100d6565b90565b6100f7816100e2565b036100fe57565b5f80fd5b9050519061010f826100ee565b565b61011a906100d6565b90565b61012681610111565b0361012d57565b5f80fd5b9050519061013e8261011d565b565b9190604083820312610168578061015c610165925f8601610102565b93602001610131565b90565b6100c7565b61018b61168680380380610180816100b2565b928339810190610140565b9091565b90565b6101a66101a16101ab926100cb565b61018f565b6100cb565b90565b6101b790610192565b90565b6101c3906101ae565b90565b90565b6101dd6101d86101e2926101c6565b61018f565b6100cb565b90565b6101ee906101c9565b90565b60209181520190565b5f7f657874656e73696f6e207265676973747279206973207a65726f000000000000910152565b61022e601a6020926101f1565b610237816101fa565b0190565b6102509060208101905f818303910152610221565b90565b1561025a57565b610262610063565b62461bcd60e51b8152806102786004820161023b565b0390fd5b610285906101ae565b90565b5f7f6d616368696e65207265676973747279206973207a65726f0000000000000000910152565b6102bc60186020926101f1565b6102c581610288565b0190565b6102de9060208101905f8183039101526102af565b90565b156102e857565b6102f0610063565b62461bcd60e51b815280610306600482016102c9565b0390fd5b90565b61032161031c610326926101c6565b61018f565b61030a565b90565b5f7f657874656e73696f6e20726567697374727920686173206e6f20636f64650000910152565b61035d601e6020926101f1565b61036681610329565b0190565b61037f9060208101905f818303910152610350565b90565b1561038957565b610391610063565b62461bcd60e51b8152806103a76004820161036a565b0390fd5b5f7f6d616368696e6520726567697374727920686173206e6f20636f646500000000910152565b6103df601c6020926101f1565b6103e8816103ab565b0190565b6104019060208101905f8183039101526103d2565b90565b1561040b57565b610413610063565b62461bcd60e51b815280610429600482016103ec565b0390fd5b61045a610439826101ba565b61045361044d6104485f6101e5565b6100d6565b916100d6565b1415610253565b6104876104668361027c565b61048061047a6104755f6101e5565b6100d6565b916100d6565b14156102e1565b6104ac610493826101ba565b3b6104a66104a05f61030d565b9161030a565b11610382565b6104d16104b88361027c565b3b6104cb6104c55f61030d565b9161030a565b11610404565b60805260a05256fe60806040526004361015610013575b6104be565b61001d5f356100ac565b806315e3a16b146100a757806334759513146100a25780633efb5f101461009d57806341553c091461009857806341be4dc81461009357806362d7076e1461008e578063aa5032c614610089578063d77798a9146100845763dae5e5180361000e57610492565b61045d565b6103d8565b61039e565b610344565b6102eb565b610296565b6101dc565b61011f565b60e01c90565b60405190565b5f80fd5b5f80fd5b5f9103126100ca57565b6100bc565b6d119253905312569157d493d5539160921b90565b6100ec6100cf565b90565b6100f76100e4565b90565b90565b610106906100fa565b9052565b919061011d905f602085019401906100fd565b565b3461014f5761012f3660046100c0565b61014b61013a6100ef565b6101426100b2565b9182918261010a565b0390f35b6100b8565b7f000000000000000000000000000000000000000000000000000000000000000090565b60018060a01b031690565b90565b61019a61019561019f92610178565b610183565b610178565b90565b6101ab90610186565b90565b6101b7906101a2565b90565b6101c3906101ae565b9052565b91906101da905f602085019401906101ba565b565b3461020c576101ec3660046100c0565b6102086101f7610154565b6101ff6100b2565b918291826101c7565b0390f35b6100b8565b5f80fd5b5f80fd5b5f80fd5b5f80fd5b909182601f8301121561025b5781359167ffffffffffffffff831161025657602001926001830284011161025157565b61021d565b610219565b610215565b90602082820312610291575f82013567ffffffffffffffff811161028c576102889201610221565b9091565b610211565b6100bc565b6102be6102ad6102a7366004610260565b906104c6565b6102b56100b2565b9182918261010a565b0390f35b6b5355424d49545f51554f544560a01b90565b6102dd6102c2565b90565b6102e86102d5565b90565b3461031b576102fb3660046100c0565b6103176103066102e0565b61030e6100b2565b9182918261010a565b0390f35b6100b8565b6610d3d390d3d49160ca1b90565b610336610320565b90565b61034161032e565b90565b34610374576103543660046100c0565b61037061035f610339565b6103676100b2565b9182918261010a565b0390f35b6100b8565b90565b61038590610379565b9052565b919061039c905f6020850194019061037c565b565b346103ce576103ae3660046100c0565b6103ca6103b9610513565b6103c16100b2565b91829182610389565b0390f35b6100b8565b5f0190565b34610406576103e83660046100c0565b6103f06107b9565b6103f86100b2565b80610402816103d3565b0390f35b6100b8565b7f000000000000000000000000000000000000000000000000000000000000000090565b610438906101a2565b90565b6104449061042f565b9052565b919061045b905f6020850194019061043b565b565b3461048d5761046d3660046100c0565b61048961047861040b565b6104806100b2565b91829182610448565b0390f35b6100b8565b6104ba6104a96104a3366004610260565b90610993565b6104b16100b2565b9182918261010a565b0390f35b5f80fd5b5f90565b6104e3916104d26104c2565b506104db6102d5565b919091610eaf565b90565b5f90565b5f1c90565b90565b6104fe610503916104ea565b6104ef565b90565b61051090546104f2565b90565b61051b6104e6565b506105255f610506565b90565b90565b61053f61053a61054492610528565b610183565b610379565b90565b60209181520190565b5f7f457874656e73696f6e20494420616c7265616479207365742e00000000000000910152565b6105846019602092610547565b61058d81610550565b0190565b6105a69060208101905f818303910152610577565b90565b156105b057565b6105b86100b2565b62461bcd60e51b8152806105ce60048201610591565b0390fd5b601f801991011690565b634e487b7160e01b5f52604160045260245ffd5b906105fa906105d2565b810190811067ffffffffffffffff82111761061457604052565b6105dc565b60e01b90565b61062881610379565b0361062f57565b5f80fd5b905051906106408261061f565b565b9060208282031261065b57610658915f01610633565b90565b6100bc565b6106686100b2565b3d5f823e3d90fd5b90565b61068761068261068c92610670565b610183565b610379565b90565b61069b62010000610673565b90565b60016106aa9101610379565b90565b6106b690610178565b90565b6106c2816106ad565b036106c957565b5f80fd5b905051906106da826106b9565b565b906020828203126106f5576106f2915f016106cd565b90565b6100bc565b610703906101a2565b90565b5f1b90565b906107175f1991610706565b9181191691161790565b61073561073061073a92610379565b610183565b610379565b90565b90565b9061075561075061075c92610721565b61073d565b825461070b565b9055565b5f7f457874656e73696f6e204944206e6f7420666f756e642e000000000000000000910152565b6107946017602092610547565b61079d81610760565b0190565b6107b69060208101905f818303910152610787565b90565b6107dd6107c55f610506565b6107d76107d15f61052b565b91610379565b146105a9565b610821602061080b7f00000000000000000000000000000000000000000000000000000000000000006101ae565b6327582ad5906108196100b2565b938492610619565b82528180610831600482016103d3565b03915afa90811561098e575f91610960575b5061084c61068f565b5b8061086061085a84610379565b91610379565b101561093e576108be60206108947f00000000000000000000000000000000000000000000000000000000000000006101ae565b632c177358906108b385926108a76100b2565b95869485938493610619565b835260048301610389565b03915afa908115610939575f9161090b575b506108eb6108e56108e0306106fa565b6106ad565b916106ad565b146108fe576108f99061069e565b61084d565b61090991505f610740565b565b61092c915060203d8111610932575b61092481836105f0565b8101906106dc565b5f6108d0565b503d61091a565b610660565b6109466100b2565b62461bcd60e51b81528061095c600482016107a1565b0390fd5b610981915060203d8111610987575b61097981836105f0565b810190610642565b5f610843565b503d61096f565b610660565b6109b09161099f6104c2565b506109a86100e4565b919091610eaf565b90565b5090565b5f7f656e63727970746564206d65737361676520697320656d707479000000000000910152565b6109eb601a602092610547565b6109f4816109b7565b0190565b610a0d9060208101905f8183039101526109de565b90565b15610a1757565b610a1f6100b2565b62461bcd60e51b815280610a35600482016109f8565b0390fd5b90610a4c610a456100b2565b92836105f0565b565b67ffffffffffffffff8111610a665760208091020190565b6105dc565b90929192610a80610a7b82610a4e565b610a39565b9381855260208086019202830192818411610abd57915b838310610aa45750505050565b60208091610ab284866106cd565b815201920191610a97565b61021d565b9080601f83011215610ae057816020610add93519101610a6b565b90565b610215565b90602082820312610b15575f82015167ffffffffffffffff8111610b1057610b0d9201610ac2565b90565b610211565b6100bc565b90565b610b31610b2c610b3692610b1a565b610183565b610379565b90565b610b4290610b1d565b9052565b916020610b67929493610b6060408201965f83019061037c565b0190610b39565b565b90610b7b610b7683610a4e565b610a39565b918252565b369037565b90610baa610b9283610b69565b92602080610ba08693610a4e565b9201910390610b80565b565b610bb660c0610a39565b90565b90610bc3906100fa565b9052565b5f80fd5b67ffffffffffffffff8111610be957610be56020916105d2565b0190565b6105dc565b90825f939282370152565b90929192610c0e610c0982610bcb565b610a39565b93818552602085019082840111610c2a57610c2892610bee565b565b610bc7565b610c3a913691610bf9565b90565b52565b52565b67ffffffffffffffff1690565b610c64610c5f610c6992610528565b610183565b610c43565b90565b90610c7690610c43565b9052565b90610c84906106ad565b9052565b610c91816100fa565b03610c9857565b5f80fd5b90505190610ca982610c88565b565b90602082820312610cc457610cc1915f01610c9c565b90565b6100bc565b5190565b60209181520190565b60200190565b610ce5906106ad565b9052565b90610cf681602093610cdc565b0190565b60200190565b90610d1d610d17610d1084610cc9565b8093610ccd565b92610cd6565b905f5b818110610d2d5750505090565b909192610d46610d406001928651610ce9565b94610cfa565b9101919091610d20565b610d59906100fa565b9052565b5190565b60209181520190565b90825f9392825e0152565b610d94610d9d602093610da293610d8b81610d5d565b93848093610d61565b95869101610d6a565b6105d2565b0190565b60209181520190565b90610dcc610dc6610dbf84610cc9565b8093610da6565b92610cd6565b905f5b818110610ddc5750505090565b909192610df5610def6001928651610ce9565b94610cfa565b9101919091610dcf565b610e0890610c43565b9052565b90610e819060a080610e64610e5260c08501610e2e5f8901515f880190610d50565b610e4060208901516020880190610d50565b60408801518682036040880152610d75565b60608701518582036060870152610daf565b94610e7760808201516080860190610dff565b0151910190610cdc565b90565b9091610e9e610eac9360408401908482035f860152610d00565b916020818403910152610e0c565b90565b90610eb86104c2565b50610ee0610ec78285906109b3565b610ed9610ed35f61052b565b91610379565b1415610a10565b610f097f000000000000000000000000000000000000000000000000000000000000000061042f565b5f63feeabcbf91610f1861113c565b90610f36600194610f41610f2a6100b2565b96879586948594610619565b845260048401610b46565b03915afa9081156110b5575f91611093575b50925f610f5f9061052b565b610f6890610b85565b90610f7161032e565b93929091905f923394610f82610bac565b965f880190610f9091610bb9565b6020870190610f9e91610bb9565b610fa791610c2f565b6040850190610fb591610c3d565b6060840190610fc391610c40565b610fcc90610c50565b6080830190610fda91610c6c565b60a0820190610fe891610c7a565b907f0000000000000000000000000000000000000000000000000000000000000000611013906101ae565b63f731df5391349192919093916110286100b2565b809581946110368394610619565b8352600483019161104692610e84565b03915a94602095f190811561108e575f91611060575b5090565b611081915060203d8111611087575b61107981836105f0565b810190610cab565b5f61105c565b503d61106f565b610660565b6110af91503d805f833e6110a781836105f0565b810190610ae5565b5f610f53565b610660565b5f7f457874656e73696f6e204944206973206e6f74207365742e0000000000000000910152565b6110ee6018602092610547565b6110f7816110ba565b0190565b6111109060208101905f8183039101526110e1565b90565b1561111a57565b6111226100b2565b62461bcd60e51b815280611138600482016110fb565b0390fd5b6111446104e6565b5061116a6111515f610506565b61116361115d5f61052b565b91610379565b1415611113565b6111735f610506565b9056fea2646970667358221220cd918206f70c305fa7ef2530529e648621bf02adf3c8e8e04ab01f66708df78564736f6c634300081b0033", +} + +// ConcordInstructionSenderABI is the input ABI used to generate the binding from. +// Deprecated: Use ConcordInstructionSenderMetaData.ABI instead. +var ConcordInstructionSenderABI = ConcordInstructionSenderMetaData.ABI + +// ConcordInstructionSenderBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use ConcordInstructionSenderMetaData.Bin instead. +var ConcordInstructionSenderBin = ConcordInstructionSenderMetaData.Bin + +// DeployConcordInstructionSender deploys a new Ethereum contract, binding an instance of ConcordInstructionSender to it. +func DeployConcordInstructionSender(auth *bind.TransactOpts, backend bind.ContractBackend, extensionRegistry_ common.Address, machineRegistry_ common.Address) (common.Address, *types.Transaction, *ConcordInstructionSender, error) { + parsed, err := ConcordInstructionSenderMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(ConcordInstructionSenderBin), backend, extensionRegistry_, machineRegistry_) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &ConcordInstructionSender{ConcordInstructionSenderCaller: ConcordInstructionSenderCaller{contract: contract}, ConcordInstructionSenderTransactor: ConcordInstructionSenderTransactor{contract: contract}, ConcordInstructionSenderFilterer: ConcordInstructionSenderFilterer{contract: contract}}, nil +} + +// ConcordInstructionSender is an auto generated Go binding around an Ethereum contract. +type ConcordInstructionSender struct { + ConcordInstructionSenderCaller // Read-only binding to the contract + ConcordInstructionSenderTransactor // Write-only binding to the contract + ConcordInstructionSenderFilterer // Log filterer for contract events +} + +// ConcordInstructionSenderCaller is an auto generated read-only Go binding around an Ethereum contract. +type ConcordInstructionSenderCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ConcordInstructionSenderTransactor is an auto generated write-only Go binding around an Ethereum contract. +type ConcordInstructionSenderTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ConcordInstructionSenderFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type ConcordInstructionSenderFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ConcordInstructionSenderSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type ConcordInstructionSenderSession struct { + Contract *ConcordInstructionSender // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ConcordInstructionSenderCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type ConcordInstructionSenderCallerSession struct { + Contract *ConcordInstructionSenderCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// ConcordInstructionSenderTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type ConcordInstructionSenderTransactorSession struct { + Contract *ConcordInstructionSenderTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ConcordInstructionSenderRaw is an auto generated low-level Go binding around an Ethereum contract. +type ConcordInstructionSenderRaw struct { + Contract *ConcordInstructionSender // Generic contract binding to access the raw methods on +} + +// ConcordInstructionSenderCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type ConcordInstructionSenderCallerRaw struct { + Contract *ConcordInstructionSenderCaller // Generic read-only contract binding to access the raw methods on +} + +// ConcordInstructionSenderTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type ConcordInstructionSenderTransactorRaw struct { + Contract *ConcordInstructionSenderTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewConcordInstructionSender creates a new instance of ConcordInstructionSender, bound to a specific deployed contract. +func NewConcordInstructionSender(address common.Address, backend bind.ContractBackend) (*ConcordInstructionSender, error) { + contract, err := bindConcordInstructionSender(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &ConcordInstructionSender{ConcordInstructionSenderCaller: ConcordInstructionSenderCaller{contract: contract}, ConcordInstructionSenderTransactor: ConcordInstructionSenderTransactor{contract: contract}, ConcordInstructionSenderFilterer: ConcordInstructionSenderFilterer{contract: contract}}, nil +} + +// NewConcordInstructionSenderCaller creates a new read-only instance of ConcordInstructionSender, bound to a specific deployed contract. +func NewConcordInstructionSenderCaller(address common.Address, caller bind.ContractCaller) (*ConcordInstructionSenderCaller, error) { + contract, err := bindConcordInstructionSender(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &ConcordInstructionSenderCaller{contract: contract}, nil +} + +// NewConcordInstructionSenderTransactor creates a new write-only instance of ConcordInstructionSender, bound to a specific deployed contract. +func NewConcordInstructionSenderTransactor(address common.Address, transactor bind.ContractTransactor) (*ConcordInstructionSenderTransactor, error) { + contract, err := bindConcordInstructionSender(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &ConcordInstructionSenderTransactor{contract: contract}, nil +} + +// NewConcordInstructionSenderFilterer creates a new log filterer instance of ConcordInstructionSender, bound to a specific deployed contract. +func NewConcordInstructionSenderFilterer(address common.Address, filterer bind.ContractFilterer) (*ConcordInstructionSenderFilterer, error) { + contract, err := bindConcordInstructionSender(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &ConcordInstructionSenderFilterer{contract: contract}, nil +} + +// bindConcordInstructionSender binds a generic wrapper to an already deployed contract. +func bindConcordInstructionSender(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := ConcordInstructionSenderMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_ConcordInstructionSender *ConcordInstructionSenderRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _ConcordInstructionSender.Contract.ConcordInstructionSenderCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_ConcordInstructionSender *ConcordInstructionSenderRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ConcordInstructionSender.Contract.ConcordInstructionSenderTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_ConcordInstructionSender *ConcordInstructionSenderRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _ConcordInstructionSender.Contract.ConcordInstructionSenderTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_ConcordInstructionSender *ConcordInstructionSenderCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _ConcordInstructionSender.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_ConcordInstructionSender *ConcordInstructionSenderTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ConcordInstructionSender.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_ConcordInstructionSender *ConcordInstructionSenderTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _ConcordInstructionSender.Contract.contract.Transact(opts, method, params...) +} + +// OPCOMMANDFINALIZEROUND is a free data retrieval call binding the contract method 0x15e3a16b. +// +// Solidity: function OP_COMMAND_FINALIZE_ROUND() view returns(bytes32) +func (_ConcordInstructionSender *ConcordInstructionSenderCaller) OPCOMMANDFINALIZEROUND(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _ConcordInstructionSender.contract.Call(opts, &out, "OP_COMMAND_FINALIZE_ROUND") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// OPCOMMANDFINALIZEROUND is a free data retrieval call binding the contract method 0x15e3a16b. +// +// Solidity: function OP_COMMAND_FINALIZE_ROUND() view returns(bytes32) +func (_ConcordInstructionSender *ConcordInstructionSenderSession) OPCOMMANDFINALIZEROUND() ([32]byte, error) { + return _ConcordInstructionSender.Contract.OPCOMMANDFINALIZEROUND(&_ConcordInstructionSender.CallOpts) +} + +// OPCOMMANDFINALIZEROUND is a free data retrieval call binding the contract method 0x15e3a16b. +// +// Solidity: function OP_COMMAND_FINALIZE_ROUND() view returns(bytes32) +func (_ConcordInstructionSender *ConcordInstructionSenderCallerSession) OPCOMMANDFINALIZEROUND() ([32]byte, error) { + return _ConcordInstructionSender.Contract.OPCOMMANDFINALIZEROUND(&_ConcordInstructionSender.CallOpts) +} + +// OPCOMMANDSUBMITQUOTE is a free data retrieval call binding the contract method 0x41553c09. +// +// Solidity: function OP_COMMAND_SUBMIT_QUOTE() view returns(bytes32) +func (_ConcordInstructionSender *ConcordInstructionSenderCaller) OPCOMMANDSUBMITQUOTE(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _ConcordInstructionSender.contract.Call(opts, &out, "OP_COMMAND_SUBMIT_QUOTE") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// OPCOMMANDSUBMITQUOTE is a free data retrieval call binding the contract method 0x41553c09. +// +// Solidity: function OP_COMMAND_SUBMIT_QUOTE() view returns(bytes32) +func (_ConcordInstructionSender *ConcordInstructionSenderSession) OPCOMMANDSUBMITQUOTE() ([32]byte, error) { + return _ConcordInstructionSender.Contract.OPCOMMANDSUBMITQUOTE(&_ConcordInstructionSender.CallOpts) +} + +// OPCOMMANDSUBMITQUOTE is a free data retrieval call binding the contract method 0x41553c09. +// +// Solidity: function OP_COMMAND_SUBMIT_QUOTE() view returns(bytes32) +func (_ConcordInstructionSender *ConcordInstructionSenderCallerSession) OPCOMMANDSUBMITQUOTE() ([32]byte, error) { + return _ConcordInstructionSender.Contract.OPCOMMANDSUBMITQUOTE(&_ConcordInstructionSender.CallOpts) +} + +// OPTYPECONCORD is a free data retrieval call binding the contract method 0x41be4dc8. +// +// Solidity: function OP_TYPE_CONCORD() view returns(bytes32) +func (_ConcordInstructionSender *ConcordInstructionSenderCaller) OPTYPECONCORD(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _ConcordInstructionSender.contract.Call(opts, &out, "OP_TYPE_CONCORD") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// OPTYPECONCORD is a free data retrieval call binding the contract method 0x41be4dc8. +// +// Solidity: function OP_TYPE_CONCORD() view returns(bytes32) +func (_ConcordInstructionSender *ConcordInstructionSenderSession) OPTYPECONCORD() ([32]byte, error) { + return _ConcordInstructionSender.Contract.OPTYPECONCORD(&_ConcordInstructionSender.CallOpts) +} + +// OPTYPECONCORD is a free data retrieval call binding the contract method 0x41be4dc8. +// +// Solidity: function OP_TYPE_CONCORD() view returns(bytes32) +func (_ConcordInstructionSender *ConcordInstructionSenderCallerSession) OPTYPECONCORD() ([32]byte, error) { + return _ConcordInstructionSender.Contract.OPTYPECONCORD(&_ConcordInstructionSender.CallOpts) +} + +// TEEEXTENSIONREGISTRY is a free data retrieval call binding the contract method 0x34759513. +// +// Solidity: function TEE_EXTENSION_REGISTRY() view returns(address) +func (_ConcordInstructionSender *ConcordInstructionSenderCaller) TEEEXTENSIONREGISTRY(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _ConcordInstructionSender.contract.Call(opts, &out, "TEE_EXTENSION_REGISTRY") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// TEEEXTENSIONREGISTRY is a free data retrieval call binding the contract method 0x34759513. +// +// Solidity: function TEE_EXTENSION_REGISTRY() view returns(address) +func (_ConcordInstructionSender *ConcordInstructionSenderSession) TEEEXTENSIONREGISTRY() (common.Address, error) { + return _ConcordInstructionSender.Contract.TEEEXTENSIONREGISTRY(&_ConcordInstructionSender.CallOpts) +} + +// TEEEXTENSIONREGISTRY is a free data retrieval call binding the contract method 0x34759513. +// +// Solidity: function TEE_EXTENSION_REGISTRY() view returns(address) +func (_ConcordInstructionSender *ConcordInstructionSenderCallerSession) TEEEXTENSIONREGISTRY() (common.Address, error) { + return _ConcordInstructionSender.Contract.TEEEXTENSIONREGISTRY(&_ConcordInstructionSender.CallOpts) +} + +// TEEMACHINEREGISTRY is a free data retrieval call binding the contract method 0xd77798a9. +// +// Solidity: function TEE_MACHINE_REGISTRY() view returns(address) +func (_ConcordInstructionSender *ConcordInstructionSenderCaller) TEEMACHINEREGISTRY(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _ConcordInstructionSender.contract.Call(opts, &out, "TEE_MACHINE_REGISTRY") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// TEEMACHINEREGISTRY is a free data retrieval call binding the contract method 0xd77798a9. +// +// Solidity: function TEE_MACHINE_REGISTRY() view returns(address) +func (_ConcordInstructionSender *ConcordInstructionSenderSession) TEEMACHINEREGISTRY() (common.Address, error) { + return _ConcordInstructionSender.Contract.TEEMACHINEREGISTRY(&_ConcordInstructionSender.CallOpts) +} + +// TEEMACHINEREGISTRY is a free data retrieval call binding the contract method 0xd77798a9. +// +// Solidity: function TEE_MACHINE_REGISTRY() view returns(address) +func (_ConcordInstructionSender *ConcordInstructionSenderCallerSession) TEEMACHINEREGISTRY() (common.Address, error) { + return _ConcordInstructionSender.Contract.TEEMACHINEREGISTRY(&_ConcordInstructionSender.CallOpts) +} + +// ExtensionId is a free data retrieval call binding the contract method 0x62d7076e. +// +// Solidity: function extensionId() view returns(uint256) +func (_ConcordInstructionSender *ConcordInstructionSenderCaller) ExtensionId(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _ConcordInstructionSender.contract.Call(opts, &out, "extensionId") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// ExtensionId is a free data retrieval call binding the contract method 0x62d7076e. +// +// Solidity: function extensionId() view returns(uint256) +func (_ConcordInstructionSender *ConcordInstructionSenderSession) ExtensionId() (*big.Int, error) { + return _ConcordInstructionSender.Contract.ExtensionId(&_ConcordInstructionSender.CallOpts) +} + +// ExtensionId is a free data retrieval call binding the contract method 0x62d7076e. +// +// Solidity: function extensionId() view returns(uint256) +func (_ConcordInstructionSender *ConcordInstructionSenderCallerSession) ExtensionId() (*big.Int, error) { + return _ConcordInstructionSender.Contract.ExtensionId(&_ConcordInstructionSender.CallOpts) +} + +// SendFinalizeRound is a paid mutator transaction binding the contract method 0xdae5e518. +// +// Solidity: function sendFinalizeRound(bytes encryptedMessage) payable returns(bytes32 instructionId) +func (_ConcordInstructionSender *ConcordInstructionSenderTransactor) SendFinalizeRound(opts *bind.TransactOpts, encryptedMessage []byte) (*types.Transaction, error) { + return _ConcordInstructionSender.contract.Transact(opts, "sendFinalizeRound", encryptedMessage) +} + +// SendFinalizeRound is a paid mutator transaction binding the contract method 0xdae5e518. +// +// Solidity: function sendFinalizeRound(bytes encryptedMessage) payable returns(bytes32 instructionId) +func (_ConcordInstructionSender *ConcordInstructionSenderSession) SendFinalizeRound(encryptedMessage []byte) (*types.Transaction, error) { + return _ConcordInstructionSender.Contract.SendFinalizeRound(&_ConcordInstructionSender.TransactOpts, encryptedMessage) +} + +// SendFinalizeRound is a paid mutator transaction binding the contract method 0xdae5e518. +// +// Solidity: function sendFinalizeRound(bytes encryptedMessage) payable returns(bytes32 instructionId) +func (_ConcordInstructionSender *ConcordInstructionSenderTransactorSession) SendFinalizeRound(encryptedMessage []byte) (*types.Transaction, error) { + return _ConcordInstructionSender.Contract.SendFinalizeRound(&_ConcordInstructionSender.TransactOpts, encryptedMessage) +} + +// SendSubmitQuote is a paid mutator transaction binding the contract method 0x3efb5f10. +// +// Solidity: function sendSubmitQuote(bytes encryptedMessage) payable returns(bytes32 instructionId) +func (_ConcordInstructionSender *ConcordInstructionSenderTransactor) SendSubmitQuote(opts *bind.TransactOpts, encryptedMessage []byte) (*types.Transaction, error) { + return _ConcordInstructionSender.contract.Transact(opts, "sendSubmitQuote", encryptedMessage) +} + +// SendSubmitQuote is a paid mutator transaction binding the contract method 0x3efb5f10. +// +// Solidity: function sendSubmitQuote(bytes encryptedMessage) payable returns(bytes32 instructionId) +func (_ConcordInstructionSender *ConcordInstructionSenderSession) SendSubmitQuote(encryptedMessage []byte) (*types.Transaction, error) { + return _ConcordInstructionSender.Contract.SendSubmitQuote(&_ConcordInstructionSender.TransactOpts, encryptedMessage) +} + +// SendSubmitQuote is a paid mutator transaction binding the contract method 0x3efb5f10. +// +// Solidity: function sendSubmitQuote(bytes encryptedMessage) payable returns(bytes32 instructionId) +func (_ConcordInstructionSender *ConcordInstructionSenderTransactorSession) SendSubmitQuote(encryptedMessage []byte) (*types.Transaction, error) { + return _ConcordInstructionSender.Contract.SendSubmitQuote(&_ConcordInstructionSender.TransactOpts, encryptedMessage) +} + +// SetExtensionId is a paid mutator transaction binding the contract method 0xaa5032c6. +// +// Solidity: function setExtensionId() returns() +func (_ConcordInstructionSender *ConcordInstructionSenderTransactor) SetExtensionId(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ConcordInstructionSender.contract.Transact(opts, "setExtensionId") +} + +// SetExtensionId is a paid mutator transaction binding the contract method 0xaa5032c6. +// +// Solidity: function setExtensionId() returns() +func (_ConcordInstructionSender *ConcordInstructionSenderSession) SetExtensionId() (*types.Transaction, error) { + return _ConcordInstructionSender.Contract.SetExtensionId(&_ConcordInstructionSender.TransactOpts) +} + +// SetExtensionId is a paid mutator transaction binding the contract method 0xaa5032c6. +// +// Solidity: function setExtensionId() returns() +func (_ConcordInstructionSender *ConcordInstructionSenderTransactorSession) SetExtensionId() (*types.Transaction, error) { + return _ConcordInstructionSender.Contract.SetExtensionId(&_ConcordInstructionSender.TransactOpts) +} diff --git a/tools/pkg/contracts/concordsender/concordsender.go b/tools/pkg/contracts/concordsender/concordsender.go new file mode 100644 index 0000000..488cd55 --- /dev/null +++ b/tools/pkg/contracts/concordsender/concordsender.go @@ -0,0 +1,6 @@ +//go:generate go run github.com/ethereum/go-ethereum/cmd/abigen --abi=ConcordInstructionSender.abi --bin=ConcordInstructionSender.bin --pkg=concordsender --type=ConcordInstructionSender --out=autogen.go + +// This directory is generated from contracts/ConcordInstructionSender.sol by +// scripts/generate-bindings.sh. Keep only the directive in source control; +// generated ABI, bytecode, and Go bindings are build artifacts. +package concordsender diff --git a/tools/pkg/fccutils/common.go b/tools/pkg/fccutils/common.go new file mode 100644 index 0000000..1cdee6d --- /dev/null +++ b/tools/pkg/fccutils/common.go @@ -0,0 +1,53 @@ +package fccutils + +import ( + "os" + + "github.com/ethereum/go-ethereum/common" + "github.com/flare-foundation/tee-node/pkg/types" + "github.com/pkg/errors" +) + +func GetTeeProxyID(proxyURL string) (common.Address, common.Address, error) { + teeInfo, err := TeeInfo(proxyURL) + if err != nil { + return common.Address{}, common.Address{}, err + } + + teeID, proxyID, err := TeeProxyId(teeInfo) + if err != nil { + return common.Address{}, common.Address{}, err + } + + return teeID, proxyID, nil +} + +func GetCodeHashAndPlatform(teeInfo *types.SignedTeeInfoResponse) (common.Hash, common.Hash, error) { + simulatedTee := os.Getenv("SIMULATED_TEE") == "true" + codeHash := TeeCodeHash + platform := TestPlatform + var err error + if !simulatedTee { + codeHash, platform, err = CodeHashAndPlatform(string(teeInfo.TeeInfoResponse.Attestation)) + if err != nil { + return common.Hash{}, common.Hash{}, err + } + } + + if codeHash != teeInfo.MachineData.CodeHash { + return common.Hash{}, common.Hash{}, errors.Errorf("code hashes do not match: %s, %s", codeHash, teeInfo.MachineData.CodeHash) + } + if platform != teeInfo.MachineData.Platform { + return common.Hash{}, common.Hash{}, errors.Errorf("platforms do not matc: %s, %s", platform, teeInfo.MachineData.Platform) + } + + return codeHash, platform, nil +} + +func HashArrayToBytes32Array(hashes []common.Hash) [][32]byte { + bytes32Array := make([][32]byte, len(hashes)) + for i, hash := range hashes { + bytes32Array[i] = [32]byte(hash) + } + return bytes32Array +} diff --git a/tools/pkg/fccutils/diag.go b/tools/pkg/fccutils/diag.go new file mode 100644 index 0000000..1fa4fdd --- /dev/null +++ b/tools/pkg/fccutils/diag.go @@ -0,0 +1,130 @@ +package fccutils + +import ( + "bytes" + "context" + stderrors "errors" + "fmt" + + "concord/tools/pkg/support" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rpc" + "github.com/flare-foundation/go-flare-common/pkg/contracts/fdc2/fdc2hub" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/extensionmanager" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/machinemanager" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/verification" + "github.com/flare-foundation/go-flare-common/pkg/logger" +) + +// diagAvailabilityCheckRevert runs an eth_call against TeeVerification with the same +// calldata as RequestAvailabilityCheckAttestation, then decodes the revert payload and +// matches the selector against custom errors from Verification, Fdc2Hub, MachineManager, +// and ExtensionManager. Purely diagnostic — never returns; logs whatever it can resolve. +func diagAvailabilityCheckRevert( + s *support.Support, + opts *bind.TransactOpts, + teeID common.Address, + instructionID [32]byte, + externalTeeID, proofOwner, claimBackAddress common.Address, +) { + verABI, err := verification.VerificationMetaData.GetAbi() + if err != nil { + logger.Warnf("diag: load Verification ABI: %v", err) + return + } + calldata, err := verABI.Pack( + "requestAvailabilityCheckAttestation", + teeID, instructionID, externalTeeID, proofOwner, claimBackAddress, + ) + if err != nil { + logger.Warnf("diag: pack calldata: %v", err) + return + } + + from := crypto.PubkeyToAddress(s.Prv.PublicKey) + msg := ethereum.CallMsg{ + From: from, + To: &s.Addresses.FlareTeeManager, + Value: opts.Value, + Data: calldata, + } + logger.Infof("diag: eth_call requestAvailabilityCheckAttestation from=%s to=%s value=%s", + from.Hex(), s.Addresses.FlareTeeManager.Hex(), opts.Value.String()) + logger.Infof("diag: teeID=%s instructionID=0x%x externalTeeID=%s proofOwner=%s claimBack=%s", + teeID.Hex(), instructionID, externalTeeID.Hex(), proofOwner.Hex(), claimBackAddress.Hex()) + + _, callErr := s.ChainClient.CallContract(context.Background(), msg, nil) + if callErr == nil { + logger.Warnf("diag: eth_call unexpectedly succeeded — revert may be tx-time/gas-related") + return + } + + revertData := ExtractRevertData(callErr) + if len(revertData) == 0 { + logger.Warnf("diag: eth_call error has no revert data: %v", callErr) + return + } + if len(revertData) < 4 { + logger.Warnf("diag: revert data too short to contain a selector: 0x%x", revertData) + return + } + selector := revertData[:4] + + // Standard solidity Error(string) — selector 0x08c379a0 + if bytes.Equal(selector, []byte{0x08, 0xc3, 0x79, 0xa0}) { + if reason, err := abi.UnpackRevert(revertData); err == nil { + logger.Errorf("diag: revert reason (string): %q", reason) + return + } + } + + for _, src := range []struct { + name string + md *bind.MetaData + }{ + {"Verification", verification.VerificationMetaData}, + {"Fdc2Hub", fdc2hub.Fdc2HubMetaData}, + {"MachineManager", machinemanager.MachineManagerMetaData}, + {"ExtensionManager", extensionmanager.ExtensionManagerMetaData}, + } { + a, err := src.md.GetAbi() + if err != nil { + continue + } + for errName, eDef := range a.Errors { + if !bytes.Equal(eDef.ID[:4], selector) { + continue + } + args, unpackErr := eDef.Inputs.Unpack(revertData[4:]) + if unpackErr != nil { + logger.Errorf("diag: revert -> %s.%s (arg unpack failed: %v) data=0x%x", src.name, errName, unpackErr, revertData) + } else { + logger.Errorf("diag: revert -> %s.%s%s", src.name, errName, fmt.Sprintf("(%v)", args)) + } + return + } + } + + logger.Errorf("diag: revert with unknown selector 0x%x; full data: 0x%x", selector, revertData) +} + +func ExtractRevertData(err error) []byte { + if err == nil { + return nil + } + var dataErr rpc.DataError + if stderrors.As(err, &dataErr) { + switch d := dataErr.ErrorData().(type) { + case string: + return common.FromHex(d) + case []byte: + return d + } + } + return nil +} diff --git a/tools/pkg/fccutils/ecies.go b/tools/pkg/fccutils/ecies.go new file mode 100644 index 0000000..5847881 --- /dev/null +++ b/tools/pkg/fccutils/ecies.go @@ -0,0 +1,41 @@ +package fccutils + +import ( + "github.com/ethereum/go-ethereum/crypto" + teetypes "github.com/flare-foundation/tee-node/pkg/types" + teeutils "github.com/flare-foundation/tee-node/pkg/utils" + "github.com/pkg/errors" +) + +// EncryptForTEE fetches the current machine public key from the configured +// extension proxy and applies the exact ECIES parameters used by the official +// tee-node. The resulting bytes are safe to pass as the InstructionSender +// message; the extension decrypts them only inside the TEE. +func EncryptForTEE(proxyURL string, plaintext []byte) ([]byte, error) { + if len(plaintext) == 0 { + return nil, errors.New("cannot encrypt an empty FCC payload") + } + info, err := TeeInfo(proxyURL) + if err != nil { + return nil, errors.Errorf("fetching TEE info: %s", err) + } + publicKey, err := teetypes.ParsePubKey(info.MachineData.PublicKey) + if err != nil { + return nil, errors.Errorf("parsing TEE public key: %s", err) + } + return teeutils.Encrypt(plaintext, publicKey) +} + +// MachineAddress derives the address represented by a TEE public key. It is +// useful for recording which machine key encrypted a submitted instruction. +func MachineAddress(proxyURL string) (string, error) { + info, err := TeeInfo(proxyURL) + if err != nil { + return "", err + } + publicKey, err := teetypes.ParsePubKey(info.MachineData.PublicKey) + if err != nil { + return "", err + } + return crypto.PubkeyToAddress(*publicKey).Hex(), nil +} diff --git a/tools/pkg/fccutils/encoding.go b/tools/pkg/fccutils/encoding.go new file mode 100644 index 0000000..942cba6 --- /dev/null +++ b/tools/pkg/fccutils/encoding.go @@ -0,0 +1,58 @@ +package fccutils + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/flare-foundation/go-flare-common/pkg/logger" + "github.com/flare-foundation/go-flare-common/pkg/tee/structs" + "github.com/flare-foundation/go-flare-common/pkg/tee/structs/fdc2" + "github.com/pkg/errors" +) + +var ( + PlatformIntel common.Hash = common.HexToHash("4743505f494e54454c5f54445800000000000000000000000000000000000000") // GCP_INTEL_TDX + PlatformAMD common.Hash = common.HexToHash("4743505f414d445f534556000000000000000000000000000000000000000000") // GCP_AMD_SEV + PlatformAMDESEV common.Hash = common.HexToHash("4743505f414d445f5345565f4553000000000000000000000000000000000000") // GCP_AMD_SEV_ES + TestPlatform common.Hash = common.HexToHash("544553545f504c4154464f524d00000000000000000000000000000000000000") // TEST_PLATFORM + TeeCodeHash common.Hash = common.HexToHash("194844cf417dde867073e5ab7199fa4d21fd82b5dbe2bdea8b3d7fc18d10fdc2") +) + +type StackTracer interface { + StackTrace() errors.StackTrace +} + +func FatalWithCause(err error) { + errCause, ok := err.(StackTracer) + if ok { + st := errCause.StackTrace() + logger.Fatalf("Error: %v %+v", err, st) + } else { + logger.Fatalf("Error: %v", err) + } +} + +func EncodeFTDCTeeAvailabilityCheckRequest(data fdc2.ITeeAvailabilityCheckRequestBody) ([]byte, error) { + return structs.Encode(fdc2.AttestationTypeArguments[fdc2.AvailabilityCheck].Request, data) +} + +func DecodeFTDCTeeAvailabilityCheckRequest(data []byte) (fdc2.ITeeAvailabilityCheckRequestBody, error) { + var request fdc2.ITeeAvailabilityCheckRequestBody + err := structs.DecodeTo(fdc2.AttestationTypeArguments[fdc2.AvailabilityCheck].Request, data, &request) + if err != nil { + return fdc2.ITeeAvailabilityCheckRequestBody{}, errors.Errorf("%s", err) + } + return request, nil +} + +func EncodeFTDCTeeAvailabilityCheckResponse(data fdc2.ITeeAvailabilityCheckResponseBody) ([]byte, error) { + return structs.Encode(fdc2.AttestationTypeArguments[fdc2.AvailabilityCheck].Response, data) +} + +func DecodeFTDCTeeAvailabilityCheckResponse(data []byte) (fdc2.ITeeAvailabilityCheckResponseBody, error) { + var request fdc2.ITeeAvailabilityCheckResponseBody + err := structs.DecodeTo(fdc2.AttestationTypeArguments[fdc2.AvailabilityCheck].Response, data, &request) + if err != nil { + return fdc2.ITeeAvailabilityCheckResponseBody{}, errors.Errorf("%s", err) + } + + return request, nil +} diff --git a/tools/pkg/fccutils/extension.go b/tools/pkg/fccutils/extension.go new file mode 100644 index 0000000..2cb46fa --- /dev/null +++ b/tools/pkg/fccutils/extension.go @@ -0,0 +1,221 @@ +package fccutils + +import ( + "concord/tools/pkg/support" + "context" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/extensionmanager" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/ownerallowlist" + "github.com/flare-foundation/go-flare-common/pkg/logger" + "github.com/flare-foundation/tee-node/pkg/wallets" + "github.com/pkg/errors" +) + +func SetupExtension( + s *support.Support, + governanceHash common.Hash, + instructionsSenderAddress, stateVerifierAddress common.Address, +) (*big.Int, error) { + callOpts := &bind.CallOpts{ + From: crypto.PubkeyToAddress(s.Prv.PublicKey), + Context: context.Background(), + } + deployerAddr := crypto.PubkeyToAddress(s.Prv.PublicKey) + + opts, err := bind.NewKeyedTransactorWithChainID(s.Prv, s.ChainID) + if err != nil { + return nil, err + } + + // Step 1: Register the extension + extRegistered, _, err := registerExtension(s, opts, instructionsSenderAddress, stateVerifierAddress) + if err != nil { + return nil, err + } + extensionID := extRegistered.ExtensionId + logger.Infof("Extension registered with ID: %s", extensionID.String()) + + // Step 2: Allow TEE machine owners for this extension + alreadyMachineOwner, err := s.TeeOwnerAllowlist.IsAllowedTeeMachineOwner(callOpts, extensionID, deployerAddr) + if err != nil { + return nil, errors.Errorf("failed checking TEE machine owner status: %s", err) + } + if alreadyMachineOwner { + logger.Infof("Deployer already allowed as TEE machine owner for extension %s, skipping", extensionID.String()) + } else { + _, err = allowTeeMachineOwners(s, opts, extensionID, []common.Address{deployerAddr}) + if err != nil { + return nil, errors.Errorf("failed adding TEE machine owners (extension exists as ID %s but owners not set): %s", extensionID.String(), err) + } + logger.Infof("TEE machine owners allowed for extension %s", extensionID.String()) + } + + // Step 3: Allow wallet project owners for this extension + alreadyProjectOwner, err := s.TeeOwnerAllowlist.IsAllowedTeeWalletProjectOwner(callOpts, extensionID, deployerAddr) + if err != nil { + return nil, errors.Errorf("failed checking wallet project owner status: %s", err) + } + if alreadyProjectOwner { + logger.Infof("Deployer already allowed as wallet project owner for extension %s, skipping", extensionID.String()) + } else { + _, err = allowTeeProjectManagerOwners(s, opts, extensionID, []common.Address{deployerAddr}) + if err != nil { + return nil, errors.Errorf("failed adding wallet project owners (extension exists as ID %s but owners not set): %s", extensionID.String(), err) + } + logger.Infof("Wallet project owners allowed for extension %s", extensionID.String()) + } + + // Step 4: Allow an EVM type of keys on the extension + isKeyTypeSupported, err := IsKeyTypeSupported(s, extensionID, wallets.EVMType) + if err != nil { + return nil, err + } + if isKeyTypeSupported { + logger.Infof("Key type %s already supported for extension %s, skipping", wallets.EVMType, extensionID.String()) + } else { + logger.Infof("Adding key type %s to extension %s", wallets.EVMType, extensionID) + err = AddSupportedKeyTypes(s, extensionID, []common.Hash{wallets.EVMType}) + if err != nil { + return nil, err + } + } + + return extensionID, nil +} + +func AddSupportedKeyTypes(s *support.Support, extensionId *big.Int, keyTypes []common.Hash) error { + opts, err := bind.NewKeyedTransactorWithChainID(s.Prv, s.ChainID) + if err != nil { + return errors.Errorf("%s", err) + } + + keyTypesBytes32 := HashArrayToBytes32Array(keyTypes) + + err = addSupportedKeyTypesTx(s, opts, extensionId, keyTypesBytes32) + if err != nil { + return errors.Errorf("%s", err) + } + + return nil +} + +func IsKeyTypeSupported(s *support.Support, extensionId *big.Int, keyType common.Hash) (bool, error) { + callOpts := &bind.CallOpts{ + From: crypto.PubkeyToAddress(s.Prv.PublicKey), + Context: context.Background(), + } + + isSupported, err := isKeyTypeSupportedCall(s, callOpts, extensionId, keyType) + if err != nil { + return false, errors.Errorf("%s", err) + } + + return isSupported, nil +} + +func registerExtension( + s *support.Support, opts *bind.TransactOpts, instructionsSenderAddress, stateVerifierAddress common.Address, +) ( + *extensionmanager.ExtensionManagerTeeExtensionRegistered, *extensionmanager.ExtensionManagerTeeExtensionContractsSet, error, +) { + tx, err := s.TeeExtensionRegistry.Register(opts, stateVerifierAddress, instructionsSenderAddress) + if err != nil { + return nil, nil, errors.Errorf("TeeExtensionRegistry.Register failed: %s", err) + } + + receipt, err := support.CheckTx(tx, s.ChainClient) + if err != nil { + return nil, nil, errors.Errorf("Register transaction failed: %s", err) + } + + if len(receipt.Logs) < 2 { + return nil, nil, errors.Errorf( + "expected at least 2 logs from Register() transaction, got %d — "+ + "the registry contract may have changed or be behind a proxy", + len(receipt.Logs), + ) + } + + extensionRegistered, err := s.TeeExtensionRegistry.ParseTeeExtensionRegistered(*receipt.Logs[0]) + if err != nil { + return nil, nil, errors.Errorf("failed to parse TeeExtensionRegistered event: %s", err) + } + + if extensionRegistered.ExtensionId == nil || extensionRegistered.ExtensionId.Sign() == 0 { + logger.Warnf("WARNING: extension ID is 0 — this may cause issues with setExtensionId() sentinel logic") + } + + extensionContractsSet, err := s.TeeExtensionRegistry.ParseTeeExtensionContractsSet(*receipt.Logs[1]) + if err != nil { + return nil, nil, errors.Errorf("failed to parse TeeExtensionContractsSet event: %s", err) + } + + return extensionRegistered, extensionContractsSet, nil +} + +func allowTeeMachineOwners(s *support.Support, opts *bind.TransactOpts, extensionId *big.Int, owners []common.Address) (*ownerallowlist.OwnerAllowlistAllowedTeeMachineOwnersAdded, error) { + tx, err := s.TeeOwnerAllowlist.AddAllowedTeeMachineOwners(opts, extensionId, owners) + if err != nil { + return nil, errors.Errorf("AddAllowedTeeMachineOwners failed: %s", err) + } + + receipt, err := support.CheckTx(tx, s.ChainClient) + if err != nil { + return nil, errors.Errorf("AddAllowedTeeMachineOwners transaction failed: %s", err) + } + + if len(receipt.Logs) == 0 { + return nil, errors.New("no logs in AddAllowedTeeMachineOwners transaction — unexpected") + } + + ownersAdded, err := s.TeeOwnerAllowlist.ParseAllowedTeeMachineOwnersAdded(*receipt.Logs[0]) + if err != nil { + return nil, errors.Errorf("failed to parse AllowedTeeMachineOwnersAdded event: %s", err) + } + + return ownersAdded, nil +} + +func allowTeeProjectManagerOwners(s *support.Support, opts *bind.TransactOpts, extensionId *big.Int, owners []common.Address) (*ownerallowlist.OwnerAllowlistAllowedTeeWalletProjectOwnersAdded, error) { + tx, err := s.TeeOwnerAllowlist.AddAllowedTeeWalletProjectOwners(opts, extensionId, owners) + if err != nil { + return nil, errors.Errorf("AddAllowedTeeWalletProjectOwners failed: %s", err) + } + + receipt, err := support.CheckTx(tx, s.ChainClient) + if err != nil { + return nil, errors.Errorf("AddAllowedTeeWalletProjectOwners transaction failed: %s", err) + } + + if len(receipt.Logs) == 0 { + return nil, errors.New("no logs in AddAllowedTeeWalletProjectOwners transaction — unexpected") + } + + ownersAdded, err := s.TeeOwnerAllowlist.ParseAllowedTeeWalletProjectOwnersAdded(*receipt.Logs[0]) + if err != nil { + return nil, errors.Errorf("failed to parse AllowedTeeWalletProjectOwnersAdded event: %s", err) + } + + return ownersAdded, nil +} + +func addSupportedKeyTypesTx(s *support.Support, opts *bind.TransactOpts, extensionId *big.Int, keyTypesBytes32 [][32]byte) error { + tx, err := s.TeeExtensionRegistry.AddSupportedKeyTypes(opts, extensionId, keyTypesBytes32) + if err != nil { + return errors.Errorf("%s", err) + } + + _, err = support.CheckTx(tx, s.ChainClient) + if err != nil { + return errors.Errorf("%s", err) + } + return nil +} + +func isKeyTypeSupportedCall(s *support.Support, opts *bind.CallOpts, extensionId *big.Int, keyType common.Hash) (bool, error) { + return s.TeeExtensionRegistry.IsKeyTypeSupported(opts, extensionId, keyType) +} diff --git a/tools/pkg/fccutils/governance.go b/tools/pkg/fccutils/governance.go new file mode 100644 index 0000000..bf2277f --- /dev/null +++ b/tools/pkg/fccutils/governance.go @@ -0,0 +1,67 @@ +package fccutils + +import ( + "context" + "crypto/ecdsa" + "math/big" + + "concord/tools/pkg/support" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/flare-foundation/go-flare-common/pkg/logger" + "github.com/flare-foundation/tee-node/pkg/types" + "github.com/pkg/errors" +) + +// SetTeeGovernance registers the TEE governance signer set + threshold for the +// extension via ExtensionGovernance.SetNewTeeGovernance. +// +// The node derives its governanceHash from the SAME (signers, threshold) pair +// (see governanceFromEnv in tee-node, fed by GOVERNANCE_SIGNERS / +// GOVERNANCE_THRESHOLD). MachineManager.register only accepts a TEE machine +// whose signed governanceHash matches the one registered here — so this MUST be +// kept consistent with the node's env. The scaffold reads both from .env to +// guarantee that. +// +// Idempotent: if the latest on-chain governance hash already equals the desired +// one, the transaction is skipped. +func SetTeeGovernance(s *support.Support, privKey *ecdsa.PrivateKey, extensionId *big.Int, signers []common.Address, threshold uint64) error { + if len(signers) == 0 { + return errors.New("at least one governance signer is required (set GOVERNANCE_SIGNERS)") + } + if threshold == 0 || threshold > uint64(len(signers)) { + return errors.Errorf("invalid GOVERNANCE_THRESHOLD %d for %d signer(s)", threshold, len(signers)) + } + + desiredHash, err := types.GovernanceHash(signers, threshold) + if err != nil { + return errors.Errorf("computing governance hash: %s", err) + } + + callOpts := &bind.CallOpts{Context: context.Background()} + if current, err := s.TeeExtensionGovernance.GetLatestTeeGovernanceHash(callOpts, extensionId); err == nil { + if common.Hash(current) == desiredHash { + logger.Infof("TEE governance already set for extension %s (hash %s), skipping", extensionId.String(), desiredHash.Hex()) + return nil + } + } + + opts, err := bind.NewKeyedTransactorWithChainID(privKey, s.ChainID) + if err != nil { + return errors.Errorf("%s", err) + } + + tx, err := s.TeeExtensionGovernance.SetNewTeeGovernance(opts, extensionId, signers, threshold) + if err != nil { + return errors.Errorf("SetNewTeeGovernance failed: %s", err) + } + + if _, err := support.CheckTx(tx, s.ChainClient); err != nil { + return errors.Errorf("%s", err) + } + + logger.Infof("TEE governance set for extension %s: %d signer(s), threshold %d (hash %s)", + extensionId.String(), len(signers), threshold, desiredHash.Hex()) + return nil +} diff --git a/tools/pkg/fccutils/policy_consistency.go b/tools/pkg/fccutils/policy_consistency.go new file mode 100644 index 0000000..0f7c48a --- /dev/null +++ b/tools/pkg/fccutils/policy_consistency.go @@ -0,0 +1,75 @@ +package fccutils + +import ( + "context" + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/flare-foundation/go-flare-common/pkg/logger" + + "concord/tools/pkg/support" +) + +// ErrPolicyOutOfSync indicates the FTDC proxy's latest signing policy does not +// match the on-chain current reward epoch. Callers can test for it with errors.Is. +var ErrPolicyOutOfSync = errors.New("FTDC proxy signing policy out of sync with on-chain reward epoch") + +// policyInSync reports whether the proxy's last signing policy id is acceptable +// for the given on-chain current reward epoch. The proxy is legitimately one +// epoch ahead during the ~2h window after a new signing policy is initialized +// but before its reward epoch starts (same tolerance as tee-node walletutils). +func policyInSync(onchainEpoch, proxyPolicyID uint64) bool { + return proxyPolicyID == onchainEpoch || proxyPolicyID == onchainEpoch+1 +} + +// outOfSyncError builds an actionable error explaining the mismatch and its +// consequence (the 404 "no round" rejection from data providers). +func outOfSyncError(onchainEpoch, proxyPolicyID uint64) error { + var rel string + if proxyPolicyID < onchainEpoch { + rel = fmt.Sprintf("proxy is %d epoch(s) behind", onchainEpoch-proxyPolicyID) + } else { + rel = fmt.Sprintf("proxy is %d epoch(s) ahead", proxyPolicyID-onchainEpoch) + } + return fmt.Errorf("%w: proxy signing policy %d vs on-chain current reward epoch %d (%s); "+ + "data providers will reject the availability check with 404 \"no round\"; "+ + "wait for the proxy to catch up to the current signing policy, or check the proxy's "+ + "initial_signing_policy_offset and C-chain indexer sync", + ErrPolicyOutOfSync, proxyPolicyID, onchainEpoch, rel) +} + +// CheckFTDCProxyPolicyConsistency compares the on-chain current reward epoch +// (FlareSystemsManager.getCurrentRewardEpochId) against the signing policy id +// reported by the FTDC proxy at ftdcProxyURL (TeeInfo.LastSigningPolicyID). +// +// It returns a wrapped ErrPolicyOutOfSync only on a CONFIRMED mismatch. If +// either lookup fails (RPC down, proxy not up yet) it logs a warning and +// returns nil, so the gate never turns a transient into a blocked registration. +func CheckFTDCProxyPolicyConsistency(s *support.Support, ftdcProxyURL string) error { + callOpts := &bind.CallOpts{Context: context.Background()} + onchain, err := s.FlareSystemManager.GetCurrentRewardEpochId(callOpts) + if err != nil { + logger.Warnf("policy consistency check skipped: could not query on-chain reward epoch: %v", err) + return nil + } + if onchain == nil { + logger.Warnf("policy consistency check skipped: on-chain reward epoch is nil") + return nil + } + + info, err := TeeInfo(ftdcProxyURL) + if err != nil { + logger.Warnf("policy consistency check skipped: could not fetch FTDC proxy info from %s: %v", ftdcProxyURL, err) + return nil + } + + onchainEpoch := onchain.Uint64() + proxyPolicyID := uint64(info.TeeInfo.LastSigningPolicyID) + if !policyInSync(onchainEpoch, proxyPolicyID) { + return outOfSyncError(onchainEpoch, proxyPolicyID) + } + + logger.Infof("policy consistency OK: FTDC proxy signing policy %d matches on-chain reward epoch %d (tolerance +1)", proxyPolicyID, onchainEpoch) + return nil +} diff --git a/tools/pkg/fccutils/policy_consistency_test.go b/tools/pkg/fccutils/policy_consistency_test.go new file mode 100644 index 0000000..5e4097c --- /dev/null +++ b/tools/pkg/fccutils/policy_consistency_test.go @@ -0,0 +1,47 @@ +package fccutils + +import ( + "errors" + "strings" + "testing" +) + +func TestPolicyInSync(t *testing.T) { + cases := []struct { + name string + onchain uint64 + proxy uint64 + want bool + }{ + {"exact match", 100, 100, true}, + {"proxy one ahead (rollover window)", 100, 101, true}, + {"proxy one behind", 100, 99, false}, + {"proxy two ahead", 100, 102, false}, + {"genesis", 0, 0, true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := policyInSync(tc.onchain, tc.proxy); got != tc.want { + t.Fatalf("policyInSync(%d, %d) = %v, want %v", tc.onchain, tc.proxy, got, tc.want) + } + }) + } +} + +func TestOutOfSyncError(t *testing.T) { + err := outOfSyncError(1236, 1234) // proxy behind + if !errors.Is(err, ErrPolicyOutOfSync) { + t.Fatalf("expected error to wrap ErrPolicyOutOfSync, got %v", err) + } + msg := err.Error() + for _, want := range []string{"1234", "1236", "behind", "404"} { + if !strings.Contains(msg, want) { + t.Errorf("error message %q missing %q", msg, want) + } + } + + ahead := outOfSyncError(1236, 1239) // proxy ahead + if !strings.Contains(ahead.Error(), "ahead") { + t.Errorf("expected 'ahead' in %q", ahead.Error()) + } +} diff --git a/tools/pkg/fccutils/registration.go b/tools/pkg/fccutils/registration.go new file mode 100644 index 0000000..604ac6e --- /dev/null +++ b/tools/pkg/fccutils/registration.go @@ -0,0 +1,425 @@ +package fccutils + +import ( + "concord/tools/pkg/support" + "context" + "crypto/ecdsa" + "encoding/hex" + "encoding/json" + "math/big" + "os" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/machinemanager" + "github.com/flare-foundation/go-flare-common/pkg/encoding" + "github.com/flare-foundation/go-flare-common/pkg/logger" + "github.com/flare-foundation/tee-node/pkg/fdc" + "github.com/flare-foundation/tee-node/pkg/types" + "github.com/pkg/errors" + "time" +) + +// registrationState tracks progress through the multi-step registration flow. +// Saved to a state file after each step so registration can resume after failures. +type registrationState struct { + CompletedSteps string `json:"completed_steps"` + TeeAttestInstructionID common.Hash `json:"tee_attest_instruction_id"` + InstructionID common.Hash `json:"instruction_id"` +} + +func loadState(path string) (*registrationState, error) { + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return ®istrationState{}, nil + } + return nil, errors.Errorf("failed to read state file: %s", err) + } + var state registrationState + if err := json.Unmarshal(data, &state); err != nil { + return nil, errors.Errorf("failed to parse state file: %s", err) + } + return &state, nil +} + +func saveState(path string, state *registrationState) error { + data, err := json.MarshalIndent(state, "", " ") + if err != nil { + return errors.Errorf("failed to marshal state: %s", err) + } + if err := os.WriteFile(path, data, 0644); err != nil { + return errors.Errorf("failed to write state file: %s", err) + } + return nil +} + +func RegisterNode(s *support.Support, teeInfo *types.SignedTeeInfoResponse, hostURL, ftdcTeeURL string, ftdcTee common.Address, command, instructionIDstring, stateFilePath string) error { + teeID, proxyID, err := TeeProxyId(teeInfo) + if err != nil { + return err + } + + // Load existing state for resume support + state, err := loadState(stateFilePath) + if err != nil { + return err + } + if state.CompletedSteps != "" { + logger.Infof("Resuming registration from state file (completed: %s)", state.CompletedSteps) + } + + var teeAttestInstructionID common.Hash + if strings.Contains(command, "r") { + if strings.Contains(state.CompletedSteps, "r") { + logger.Infof("Pre-registration already completed, skipping (from state file)") + teeAttestInstructionID = state.TeeAttestInstructionID + } else { + // Check if machine is already registered on-chain + callOpts := &bind.CallOpts{Context: context.Background()} + machineInfo, machineErr := s.TeeMachineRegistry.GetTeeMachine(callOpts, teeID) + if machineErr == nil && machineInfo.TeeId != (common.Address{}) { + // Already registered (e.g. a re-run): skip machine pre-registration, + // but the original attestation challenge is one-shot and may have + // expired, so request a FRESH attestation to get a valid challenge — + // otherwise the availability check below reverts with ChallengeExpired. + logger.Infof("TEE machine %s already registered on-chain, requesting fresh attestation", teeID.Hex()) + teeAttestInstructionID, err = RequestTeeAttestation(s, teeID) + if err != nil { + return err + } + } else { + _, teeAttestInstructionID, err = PreRegistration(s, hostURL, teeID, proxyID, teeInfo) + if err != nil { + return err + } + } + state.CompletedSteps += "r" + state.TeeAttestInstructionID = teeAttestInstructionID + if saveErr := saveState(stateFilePath, state); saveErr != nil { + logger.Warnf("WARNING: failed to save state: %v", saveErr) + } + } + time.Sleep(1 * time.Second) + } + + if strings.Contains(command, "R") { + if strings.Contains(state.CompletedSteps, "R") { + logger.Infof("TEE attestation request already completed, skipping (from state file)") + teeAttestInstructionID = state.TeeAttestInstructionID + } else { + teeAttestInstructionID, err = RequestTeeAttestation(s, teeID) + if err != nil { + return err + } + state.CompletedSteps += "R" + state.TeeAttestInstructionID = teeAttestInstructionID + if saveErr := saveState(stateFilePath, state); saveErr != nil { + logger.Warnf("WARNING: failed to save state: %v", saveErr) + } + } + time.Sleep(1 * time.Second) + } + + var instructionID common.Hash + if strings.Contains(command, "a") { + if strings.Contains(state.CompletedSteps, "a") { + logger.Infof("FTDC availability check already completed, skipping (from state file)") + instructionID = state.InstructionID + } else { + // Pre-flight: the availability check relies on the FTDC proxy's data + // providers cosigning an instruction tied to the current signing policy. + // If the proxy's signing policy is out of sync with the on-chain reward + // epoch, that cosign is rejected with 404 "no round" and the FDC proof + // never materializes. Fail fast here with a clear message instead of + // after a wasted on-chain tx + ~30s of polling. + if err := CheckFTDCProxyPolicyConsistency(s, ftdcTeeURL); err != nil { + return err + } + + instructionID, err = RequestFTDCAvailabilityCheck(s, teeID, ftdcTee, teeAttestInstructionID) + if err != nil { + return err + } + state.CompletedSteps += "a" + state.InstructionID = instructionID + if saveErr := saveState(stateFilePath, state); saveErr != nil { + logger.Warnf("WARNING: failed to save state: %v", saveErr) + } + } + time.Sleep(1 * time.Second) + } else { + instructionID = common.HexToHash(instructionIDstring) + } + + if strings.Contains(command, "p") { + toProductionProof, err := GetFTDCAvailabilityCheckResult(ftdcTeeURL, instructionID) + if err != nil { + return err + } + + err = ToProduction(s, toProductionProof) + if err != nil { + return err + } + } + + // All steps completed — delete state file + os.Remove(stateFilePath) + return nil +} + +func PreRegistration( + s *support.Support, + hostURL string, + teeID common.Address, + proxyID common.Address, + teeInfo *types.SignedTeeInfoResponse, +) ([32]byte, common.Hash, error) { + opts, err := bind.NewKeyedTransactorWithChainID(s.Prv, s.ChainID) + if err != nil { + return [32]byte{}, common.Hash{}, errors.Errorf("%s", err) + } + opts.Value = big.NewInt(int64(1000000000)) + + teeMachineDataRegistry := machinemanager.IMachineManagerTeeMachineData{ + ExtensionId: new(big.Int).SetBytes(teeInfo.MachineData.ExtensionID.Bytes()), + InitialOwner: teeInfo.MachineData.InitialOwner, + CodeHash: teeInfo.MachineData.CodeHash, + Platform: teeInfo.MachineData.Platform, + PublicKey: machinemanager.PublicKey{X: teeInfo.MachineData.PublicKey.X, Y: teeInfo.MachineData.PublicKey.Y}, + // Must carry the node's governanceHash: register() verifies the TEE + // signature over keccak(abi.encode(machineData)) including this field, + // so a zero value here would fail with InvalidTeePublicKeyOrSignature. + GovernanceHash: teeInfo.MachineData.GovernanceHash, + } + + if len(teeInfo.DataSignature) != 65 { + return [32]byte{}, common.Hash{}, errors.New("signature error") + } + sigVRS, err := encoding.TransformSignatureRSVtoVRS(teeInfo.DataSignature) + if err != nil { + return [32]byte{}, common.Hash{}, errors.Errorf("%s", err) + } + + signature := machinemanager.Signature{ + V: sigVRS[0], + R: [32]byte(sigVRS[1:33]), + S: [32]byte(sigVRS[33:65]), + } + + claimBackAddress := crypto.PubkeyToAddress(s.Prv.PublicKey) + tx, err := s.TeeMachineRegistry.Register(opts, teeMachineDataRegistry, signature, proxyID, hostURL, claimBackAddress) + if err != nil { + reason := DecodeRevertReason(err) + if parsed, abiErr := machinemanager.MachineManagerMetaData.GetAbi(); abiErr == nil { + if callData, packErr := parsed.Pack("register", teeMachineDataRegistry, signature, proxyID, hostURL, claimBackAddress); packErr == nil { + raw := SimulateAndDecodeRevert(s.ChainClient, claimBackAddress, s.Addresses.FlareTeeManager, opts.Value, callData) + if named := MatchCustomError(parsed, raw); named != "" { + reason = named + } else if reason == "" { + reason = raw + } + } + } + if reason == "" { + reason = err.Error() + } + return [32]byte{}, common.Hash{}, errors.Errorf("register() reverted: %s", reason) + } + + receipt, err := support.CheckTx(tx, s.ChainClient) + if err != nil { + return [32]byte{}, common.Hash{}, err + } + logger.Infof("(pre)registration of TEE with ID %s succeeded", hex.EncodeToString(teeID[:])) + + if len(receipt.Logs) < 2 { + return common.Hash{}, common.Hash{}, errors.New("unexpected logs, this should not happen") + } + attestEvent, err := s.TeeVerification.ParseTeeAttestationRequested(*receipt.Logs[1]) + if err != nil { + return [32]byte{}, common.Hash{}, errors.Errorf("failed to parse TeeAttestationRequested event: %s", err) + } + challenge := attestEvent.Challenge + + event, err := s.TeeVerification.ParseTeeInstructionsSent(*receipt.Logs[0]) + if err != nil { + return common.Hash{}, common.Hash{}, errors.Errorf("failed to parse TeeInstructionsSent event: %s", err) + } + instructionID := common.Hash(event.InstructionId) + logger.Infof("tee-attestation requested, instructionId: %s", hex.EncodeToString(instructionID[:])) + + return challenge, instructionID, nil +} + +func RequestTeeAttestation(s *support.Support, teeID common.Address) (common.Hash, error) { + opts, err := bind.NewKeyedTransactorWithChainID(s.Prv, s.ChainID) + if err != nil { + return [32]byte{}, errors.Errorf("%s", err) + } + opts.Value = big.NewInt(int64(1000000000)) + + claimBackAddress := crypto.PubkeyToAddress(s.Prv.PublicKey) + tx, err := s.TeeVerification.RequestTeeAttestation(opts, teeID, claimBackAddress) + if err != nil { + return [32]byte{}, errors.Errorf("error: %s", err) + } + + receipt, err := support.CheckTx(tx, s.ChainClient) + if err != nil { + return [32]byte{}, err + } + + if len(receipt.Logs) < 2 { + return common.Hash{}, errors.New("unexpected logs, this should not happen") + } + event, err := s.TeeVerification.ParseTeeInstructionsSent(*receipt.Logs[0]) + if err != nil { + return common.Hash{}, errors.Errorf("failed to parse TeeInstructionsSent event: %s", err) + } + instructionID := common.Hash(event.InstructionId) + logger.Infof("tee attestation requested, instructionId: %s", hex.EncodeToString(instructionID[:])) + + return instructionID, nil +} + +func RequestFTDCAvailabilityCheck(s *support.Support, teeID, externalTeeID common.Address, teeAttestInstructionID [32]byte) (common.Hash, error) { + opts, err := bind.NewKeyedTransactorWithChainID(s.Prv, s.ChainID) + if err != nil { + return common.Hash{}, errors.Errorf("%s", err) + } + opts.Value = big.NewInt(int64(1000000000)) + + claimBackAddress := crypto.PubkeyToAddress(s.Prv.PublicKey) + proofOwner := claimBackAddress + tx, err := s.TeeVerification.RequestAvailabilityCheckAttestation(opts, teeID, teeAttestInstructionID, externalTeeID, proofOwner, claimBackAddress) + if err != nil { + diagAvailabilityCheckRevert(s, opts, teeID, teeAttestInstructionID, externalTeeID, proofOwner, claimBackAddress) + return common.Hash{}, errors.Errorf("%s", err) + } + receipt, err := support.CheckTx(tx, s.ChainClient) + if err != nil { + return common.Hash{}, errors.Errorf("%s", err) + } + if len(receipt.Logs) == 0 { + return common.Hash{}, errors.New("no logs found in receipt") + } + event, err := s.TeeVerification.ParseTeeInstructionsSent(*receipt.Logs[0]) + if err != nil { + return common.Hash{}, errors.Errorf("failed to parse TeeInstructionsSent event: %s", err) + } + instructionID := common.Hash(event.InstructionId) + + logger.Infof("availability check sent, instructionId: %s", hex.EncodeToString(instructionID[:])) + + return instructionID, nil +} + +func GetFTDCAvailabilityCheckResult(hostURL string, instructionId common.Hash) (*machinemanager.ITeeAvailabilityCheckProof, error) { + actionResult, err := ActionResult(hostURL, instructionId) + if err != nil { + return nil, err + } + var ftdcProof fdc.ProveResponse + err = json.Unmarshal(actionResult.Result.Data, &ftdcProof) + if err != nil { + return nil, errors.Errorf("%s", err) + } + + header, err := fdc.DecodeResponse(ftdcProof.ResponseHeader) + if err != nil { + return nil, errors.Errorf("%s", err) + } + + request, err := DecodeFTDCTeeAvailabilityCheckRequest(ftdcProof.RequestBody) + if err != nil { + return nil, errors.Errorf("%s", err) + } + response, err := DecodeFTDCTeeAvailabilityCheckResponse(ftdcProof.ResponseBody) + if err != nil { + return nil, errors.Errorf("%s", err) + } + + toProductionProof := machinemanager.ITeeAvailabilityCheckProof{ + Signatures: machinemanager.IFdc2VerificationFdc2Signatures{SigningPolicySignatures: ftdcProof.DataProviderSignatures}, + Header: machinemanager.IFdc2HubFdc2ResponseHeader(header), + RequestBody: machinemanager.ITeeAvailabilityCheckRequestBody(request), + ResponseBody: machinemanager.ITeeAvailabilityCheckResponseBody{ + Status: response.Status, + TeeTimestamp: response.TeeTimestamp, + CodeHash: response.CodeHash, + Platform: response.Platform, + InitialSigningPolicyId: response.InitialSigningPolicyId, + LastSigningPolicyId: response.LastSigningPolicyId, + State: machinemanager.ITeeAvailabilityCheckTeeState(response.State), + }, + } + + logger.Infof("availability check proof obtained") + + return &toProductionProof, nil +} + +func ToProduction(s *support.Support, toProductionProof *machinemanager.ITeeAvailabilityCheckProof) error { + opts, err := bind.NewKeyedTransactorWithChainID(s.Prv, s.ChainID) + if err != nil { + return errors.Errorf("%s", err) + } + + tx, err := s.TeeMachineRegistry.ToProduction(opts, *toProductionProof) + if err != nil { + return errors.Errorf("%s", err) + } + _, err = support.CheckTx(tx, s.ChainClient) + if err != nil { + return errors.Errorf("%s", err) + } + + teeMachineInfo, err := s.TeeMachineRegistry.GetTeeMachine(nil, toProductionProof.RequestBody.TeeId) + if err != nil { + return errors.Errorf("%s", err) + } + if teeMachineInfo.TeeId != toProductionProof.RequestBody.TeeId { + return errors.New("tee machine not set up correctly") + } + + return nil +} + +// StringToBytes32 packs an ASCII string into a bytes32, left-aligned and +// zero-padded on the right. Errors if the string exceeds 32 bytes. +func StringToBytes32(s string) ([32]byte, error) { + var b [32]byte + if len(s) > 32 { + return b, errors.Errorf("version %q exceeds 32 bytes", s) + } + copy(b[:], s) + return b, nil +} + +func AddTeeVersion(s *support.Support, privKey *ecdsa.PrivateKey, extensionId *big.Int, codeHash common.Hash, platform common.Hash, version string) error { + opts, err := bind.NewKeyedTransactorWithChainID(privKey, s.ChainID) + if err != nil { + return errors.Errorf("%s", err) + } + + versionBytes, err := StringToBytes32(version) + if err != nil { + return err + } + + tx, err := s.TeeExtensionRegistry.AddTeeVersion(opts, extensionId, versionBytes, codeHash, [][32]byte{platform}) + if err != nil { + return errors.Errorf("TeeExtensionRegistry.AddTeeVersion failed: %s", err) + } + + _, err = support.CheckTx(tx, s.ChainClient) + if err != nil { + return errors.Errorf("%s", err) + } + + return nil +} diff --git a/tools/pkg/fccutils/registration_test.go b/tools/pkg/fccutils/registration_test.go new file mode 100644 index 0000000..5b105ed --- /dev/null +++ b/tools/pkg/fccutils/registration_test.go @@ -0,0 +1,214 @@ +package fccutils + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +func TestLoadState_NoFile(t *testing.T) { + state, err := loadState("/nonexistent/path/register-tee.state") + if err != nil { + t.Fatalf("expected no error for missing file, got: %v", err) + } + if state.CompletedSteps != "" { + t.Errorf("expected empty CompletedSteps, got %q", state.CompletedSteps) + } + if state.TeeAttestInstructionID != (common.Hash{}) { + t.Errorf("expected zero TeeAttestInstructionID, got %s", state.TeeAttestInstructionID.Hex()) + } + if state.InstructionID != (common.Hash{}) { + t.Errorf("expected zero InstructionID, got %s", state.InstructionID.Hex()) + } +} + +func TestLoadState_ValidFile(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "register-tee.state") + + expectedHash := common.HexToHash("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890") + state := registrationState{ + CompletedSteps: "rR", + TeeAttestInstructionID: expectedHash, + } + data, _ := json.MarshalIndent(state, "", " ") + if err := os.WriteFile(path, data, 0644); err != nil { + t.Fatal(err) + } + + loaded, err := loadState(path) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if loaded.CompletedSteps != "rR" { + t.Errorf("expected CompletedSteps %q, got %q", "rR", loaded.CompletedSteps) + } + if loaded.TeeAttestInstructionID != expectedHash { + t.Errorf("expected TeeAttestInstructionID %s, got %s", + expectedHash.Hex(), loaded.TeeAttestInstructionID.Hex()) + } +} + +func TestLoadState_InvalidJSON(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "register-tee.state") + if err := os.WriteFile(path, []byte("{invalid json"), 0644); err != nil { + t.Fatal(err) + } + + _, err := loadState(path) + if err == nil { + t.Fatal("expected error for invalid JSON, got nil") + } + if msg := err.Error(); msg == "" { + t.Error("expected non-empty error message") + } +} + +func TestLoadState_EmptyFile(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "register-tee.state") + if err := os.WriteFile(path, []byte(""), 0644); err != nil { + t.Fatal(err) + } + + _, err := loadState(path) + if err == nil { + t.Fatal("expected error for empty file, got nil") + } +} + +func TestSaveState_WritesCorrectly(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "register-tee.state") + + instrID := common.HexToHash("0xdeadbeef1234567890abcdef1234567890abcdef1234567890abcdef12345678") + state := ®istrationState{ + CompletedSteps: "rRa", + TeeAttestInstructionID: common.HexToHash("0xaaaa"), + InstructionID: instrID, + } + + err := saveState(path, state) + if err != nil { + t.Fatalf("saveState failed: %v", err) + } + + // Read back and verify + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to read state file: %v", err) + } + + var loaded registrationState + if err := json.Unmarshal(data, &loaded); err != nil { + t.Fatalf("failed to unmarshal saved state: %v", err) + } + + if loaded.CompletedSteps != "rRa" { + t.Errorf("expected CompletedSteps %q, got %q", "rRa", loaded.CompletedSteps) + } + if loaded.InstructionID != instrID { + t.Errorf("expected InstructionID %s, got %s", instrID.Hex(), loaded.InstructionID.Hex()) + } +} + +func TestSaveState_OverwritesExisting(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "register-tee.state") + + // Save initial state + state1 := ®istrationState{CompletedSteps: "r"} + if err := saveState(path, state1); err != nil { + t.Fatalf("first save failed: %v", err) + } + + // Overwrite with updated state + state2 := ®istrationState{CompletedSteps: "rR"} + if err := saveState(path, state2); err != nil { + t.Fatalf("second save failed: %v", err) + } + + // Read back — should see "rR", not "r" or "rrR" + loaded, err := loadState(path) + if err != nil { + t.Fatalf("loadState failed: %v", err) + } + if loaded.CompletedSteps != "rR" { + t.Errorf("expected %q after overwrite, got %q", "rR", loaded.CompletedSteps) + } +} + +func TestSaveState_ReadOnlyDir(t *testing.T) { + if os.Geteuid() == 0 { + t.Skip("root can write through directory mode bits") + } + dir := t.TempDir() + readOnlyDir := filepath.Join(dir, "readonly") + if err := os.Mkdir(readOnlyDir, 0555); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + os.Chmod(readOnlyDir, 0755) + }) + + path := filepath.Join(readOnlyDir, "register-tee.state") + state := ®istrationState{CompletedSteps: "r"} + err := saveState(path, state) + if err == nil { + t.Fatal("expected error writing to read-only directory, got nil") + } +} + +func TestSaveAndLoad_Roundtrip(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "register-tee.state") + + original := ®istrationState{ + CompletedSteps: "rRap", + TeeAttestInstructionID: common.HexToHash("0x1111111111111111111111111111111111111111111111111111111111111111"), + InstructionID: common.HexToHash("0x2222222222222222222222222222222222222222222222222222222222222222"), + } + + if err := saveState(path, original); err != nil { + t.Fatalf("saveState failed: %v", err) + } + + loaded, err := loadState(path) + if err != nil { + t.Fatalf("loadState failed: %v", err) + } + + if loaded.CompletedSteps != original.CompletedSteps { + t.Errorf("CompletedSteps: expected %q, got %q", original.CompletedSteps, loaded.CompletedSteps) + } + if loaded.TeeAttestInstructionID != original.TeeAttestInstructionID { + t.Errorf("TeeAttestInstructionID: expected %s, got %s", + original.TeeAttestInstructionID.Hex(), loaded.TeeAttestInstructionID.Hex()) + } + if loaded.InstructionID != original.InstructionID { + t.Errorf("InstructionID: expected %s, got %s", + original.InstructionID.Hex(), loaded.InstructionID.Hex()) + } +} + +func TestLoadState_ExtraFieldsIgnored(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "register-tee.state") + // State file with an extra field that doesn't exist in the struct + content := `{"completed_steps":"r","tee_attest_instruction_id":"0x0000000000000000000000000000000000000000000000000000000000000000","extra_field":"should be ignored"}` + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatal(err) + } + + loaded, err := loadState(path) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if loaded.CompletedSteps != "r" { + t.Errorf("expected CompletedSteps %q, got %q", "r", loaded.CompletedSteps) + } +} diff --git a/tools/pkg/fccutils/revert.go b/tools/pkg/fccutils/revert.go new file mode 100644 index 0000000..c1b429b --- /dev/null +++ b/tools/pkg/fccutils/revert.go @@ -0,0 +1,123 @@ +package fccutils + +import ( + "bytes" + "context" + "encoding/hex" + "errors" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +// DecodeRevertReason attempts to extract and decode the revert reason from +// an error returned by eth_call or eth_estimateGas. Returns the decoded +// reason string, or empty string if no revert data could be extracted. +func DecodeRevertReason(err error) string { + if err == nil { + return "" + } + + // go-ethereum wraps JSON-RPC errors with a type that exposes the + // error's "data" field via ErrorData(). The data field contains the + // ABI-encoded revert reason even when the error message is just + // "execution reverted". + type dataError interface { + ErrorData() interface{} + } + + var de dataError + if errors.As(err, &de) { + if data := de.ErrorData(); data != nil { + if hexStr, ok := data.(string); ok { + return decodeRevertHex(hexStr) + } + } + } + + return "" +} + +// SimulateAndDecodeRevert replays a call via eth_call and attempts to decode +// the revert reason. Use this as a fallback when DecodeRevertReason on the +// original error returns empty — some RPC nodes strip revert data from +// eth_estimateGas errors but include it in eth_call responses. +func SimulateAndDecodeRevert( + client *ethclient.Client, + from common.Address, + to common.Address, + value *big.Int, + data []byte, +) string { + toAddr := to + msg := ethereum.CallMsg{ + From: from, + To: &toAddr, + Value: value, + Data: data, + } + + result, err := client.CallContract(context.Background(), msg, nil) + if err != nil { + if reason := DecodeRevertReason(err); reason != "" { + return reason + } + // Return the raw error message as a last resort + return err.Error() + } + + // Some nodes return the ABI-encoded revert data in the result + // bytes instead of as an error + if len(result) >= 4 { + return decodeRevertHex(hex.EncodeToString(result)) + } + + return "" +} + +// MatchCustomError maps a raw revert payload (as returned by +// SimulateAndDecodeRevert for undecodable reverts, e.g. "0xabcdef12...") to the +// custom error name declared in parsedABI whose 4-byte selector matches. If the +// error carries arguments they are appended, e.g. "InvalidGovernanceHash(0x..)". +// Returns "" when no error in the ABI matches the selector. +func MatchCustomError(parsedABI *abi.ABI, revertHex string) string { + revertHex = strings.TrimPrefix(revertHex, "0x") + data, err := hex.DecodeString(revertHex) + if err != nil || len(data) < 4 { + return "" + } + selector := data[:4] + for name, e := range parsedABI.Errors { + if !bytes.Equal(e.ID.Bytes()[:4], selector) { + continue + } + if vals, uerr := e.Unpack(data); uerr == nil { + if argStr := fmt.Sprintf("%v", vals); argStr != "[]" && argStr != "" { + return name + argStr + } + } + return name + } + return "" +} + +func decodeRevertHex(hexStr string) string { + hexStr = strings.TrimPrefix(hexStr, "0x") + decoded, err := hex.DecodeString(hexStr) + if err != nil || len(decoded) < 4 { + return "" + } + + // Try to decode as Error(string) — selector 0x08c379a2 + if reason, unpackErr := abi.UnpackRevert(decoded); unpackErr == nil { + return reason + } + + // Return raw hex for custom errors we can't decode + return "0x" + hexStr +} diff --git a/tools/pkg/fccutils/revert_test.go b/tools/pkg/fccutils/revert_test.go new file mode 100644 index 0000000..4558e5b --- /dev/null +++ b/tools/pkg/fccutils/revert_test.go @@ -0,0 +1,249 @@ +package fccutils + +import ( + "encoding/hex" + "errors" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/crypto" +) + +// mockDataError implements the ErrorData() interface that go-ethereum uses +// for JSON-RPC errors containing revert data. +type mockDataError struct { + msg string + data interface{} +} + +func (e *mockDataError) Error() string { return e.msg } +func (e *mockDataError) ErrorData() interface{} { return e.data } + +// makeRevertHex creates a valid ABI-encoded Error(string) revert in hex form. +func makeRevertHex(msg string) string { + // Compute Error(string) selector via keccak256, matching what go-ethereum expects. + selector := crypto.Keccak256([]byte("Error(string)"))[:4] + stringTy, _ := abi.NewType("string", "", nil) + args := abi.Arguments{{Type: stringTy}} + encoded, _ := args.Pack(msg) + return hex.EncodeToString(append(selector, encoded...)) +} + +// --- decodeRevertHex tests --- + +func TestDecodeRevertHex_StandardError(t *testing.T) { + hexStr := makeRevertHex("Extension ID already set.") + result := decodeRevertHex(hexStr) + if result != "Extension ID already set." { + t.Errorf("expected %q, got %q", "Extension ID already set.", result) + } +} + +func TestDecodeRevertHex_WithPrefix(t *testing.T) { + hexStr := "0x" + makeRevertHex("Extension ID already set.") + result := decodeRevertHex(hexStr) + if result != "Extension ID already set." { + t.Errorf("expected %q, got %q", "Extension ID already set.", result) + } +} + +func TestDecodeRevertHex_CustomError(t *testing.T) { + // 4-byte custom error selector + some encoded data (not Error(string)) + customSelector := "deadbeef" + paddedArg := "0000000000000000000000000000000000000000000000000000000000000042" + hexStr := customSelector + paddedArg + result := decodeRevertHex(hexStr) + // Should fall back to returning "0x" + hex since it can't unpack as Error(string) + if result != "0x"+hexStr { + t.Errorf("expected %q, got %q", "0x"+hexStr, result) + } +} + +func TestDecodeRevertHex_TooShort(t *testing.T) { + // Only 2 bytes — less than the 4-byte minimum + result := decodeRevertHex("abcd") + if result != "" { + t.Errorf("expected empty string for short input, got %q", result) + } +} + +func TestDecodeRevertHex_Empty(t *testing.T) { + result := decodeRevertHex("") + if result != "" { + t.Errorf("expected empty string for empty input, got %q", result) + } +} + +func TestDecodeRevertHex_InvalidHex(t *testing.T) { + result := decodeRevertHex("not-valid-hex!") + if result != "" { + t.Errorf("expected empty string for invalid hex, got %q", result) + } +} + +func TestDecodeRevertHex_ZeroAddressRevert(t *testing.T) { + hexStr := makeRevertHex("TeeExtensionRegistry cannot be zero address") + result := decodeRevertHex(hexStr) + if result != "TeeExtensionRegistry cannot be zero address" { + t.Errorf("expected constructor revert message, got %q", result) + } +} + +func TestDecodeRevertHex_NoCodeRevert(t *testing.T) { + hexStr := makeRevertHex("TeeExtensionRegistry has no code") + result := decodeRevertHex(hexStr) + if result != "TeeExtensionRegistry has no code" { + t.Errorf("expected constructor revert message, got %q", result) + } +} + +func TestDecodeRevertHex_ExtensionIdNotFound(t *testing.T) { + hexStr := makeRevertHex("Extension ID not found.") + result := decodeRevertHex(hexStr) + if result != "Extension ID not found." { + t.Errorf("expected %q, got %q", "Extension ID not found.", result) + } +} + +func TestDecodeRevertHex_ExtensionIdNotSet(t *testing.T) { + hexStr := makeRevertHex("Extension ID is not set.") + result := decodeRevertHex(hexStr) + if result != "Extension ID is not set." { + t.Errorf("expected %q, got %q", "Extension ID is not set.", result) + } +} + +// --- DecodeRevertReason tests --- + +func TestDecodeRevertReason_NilError(t *testing.T) { + result := DecodeRevertReason(nil) + if result != "" { + t.Errorf("expected empty string for nil error, got %q", result) + } +} + +func TestDecodeRevertReason_PlainError(t *testing.T) { + result := DecodeRevertReason(errors.New("something went wrong")) + if result != "" { + t.Errorf("expected empty string for plain error (no ErrorData), got %q", result) + } +} + +func TestDecodeRevertReason_WithDataError(t *testing.T) { + revertHex := "0x" + makeRevertHex("Extension ID already set.") + err := &mockDataError{ + msg: "execution reverted", + data: revertHex, + } + result := DecodeRevertReason(err) + if result != "Extension ID already set." { + t.Errorf("expected %q, got %q", "Extension ID already set.", result) + } +} + +func TestDecodeRevertReason_WithDataError_NoPrefix(t *testing.T) { + revertHex := makeRevertHex("Extension ID not found.") + err := &mockDataError{ + msg: "execution reverted", + data: revertHex, + } + result := DecodeRevertReason(err) + if result != "Extension ID not found." { + t.Errorf("expected %q, got %q", "Extension ID not found.", result) + } +} + +func TestDecodeRevertReason_WithNilData(t *testing.T) { + err := &mockDataError{ + msg: "execution reverted", + data: nil, + } + result := DecodeRevertReason(err) + if result != "" { + t.Errorf("expected empty string for nil data, got %q", result) + } +} + +func TestDecodeRevertReason_WithNonStringData(t *testing.T) { + err := &mockDataError{ + msg: "execution reverted", + data: 42, + } + result := DecodeRevertReason(err) + if result != "" { + t.Errorf("expected empty string for non-string data, got %q", result) + } +} + +func TestDecodeRevertReason_WrappedError(t *testing.T) { + revertHex := "0x" + makeRevertHex("insufficient funds") + innerErr := &mockDataError{ + msg: "execution reverted", + data: revertHex, + } + // Wrap the error — errors.As should unwrap and find ErrorData + wrappedErr := errors.Join(errors.New("outer context"), innerErr) + result := DecodeRevertReason(wrappedErr) + if result != "insufficient funds" { + t.Errorf("expected %q, got %q", "insufficient funds", result) + } +} + +func TestDecodeRevertReason_CustomErrorSelector(t *testing.T) { + // Custom error with unknown selector — should return hex + err := &mockDataError{ + msg: "execution reverted", + data: "0xdeadbeef0000000000000000000000000000000000000000000000000000000000000042", + } + result := DecodeRevertReason(err) + if result == "" { + t.Error("expected non-empty result for custom error (should return hex)") + } + if result == "execution reverted" { + t.Error("should not return the error message itself") + } + // Should start with "0x" since it's a hex fallback + if len(result) < 2 || result[:2] != "0x" { + t.Errorf("expected hex fallback starting with 0x, got %q", result) + } +} + +func TestDecodeRevertReason_EmptyHexData(t *testing.T) { + err := &mockDataError{ + msg: "execution reverted", + data: "", + } + result := DecodeRevertReason(err) + if result != "" { + t.Errorf("expected empty string for empty hex data, got %q", result) + } +} + +// --- Verify actual contract revert messages decode correctly --- +// These are the specific revert strings from InstructionSender.sol + +func TestDecodeRevertReason_AllContractReverts(t *testing.T) { + reverts := []string{ + "TeeExtensionRegistry cannot be zero address", + "TeeMachineRegistry cannot be zero address", + "TeeExtensionRegistry has no code", + "TeeMachineRegistry has no code", + "Extension ID already set.", + "Extension ID not found.", + "Extension ID is not set.", + } + + for _, msg := range reverts { + t.Run(msg, func(t *testing.T) { + revertHex := "0x" + makeRevertHex(msg) + err := &mockDataError{ + msg: "execution reverted", + data: revertHex, + } + result := DecodeRevertReason(err) + if result != msg { + t.Errorf("expected %q, got %q", msg, result) + } + }) + } +} diff --git a/tools/pkg/fccutils/tee_calls.go b/tools/pkg/fccutils/tee_calls.go new file mode 100644 index 0000000..78fabdb --- /dev/null +++ b/tools/pkg/fccutils/tee_calls.go @@ -0,0 +1,140 @@ +package fccutils + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/flare-foundation/go-flare-common/pkg/logger" + csigning "github.com/flare-foundation/go-flare-common/pkg/signing" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/flare-foundation/go-flare-common/pkg/tee/attestation/googlecloud" + "github.com/flare-foundation/tee-node/pkg/attestation" + "github.com/flare-foundation/tee-node/pkg/types" + "github.com/pkg/errors" +) + +const repeats = 15 + +func TeeInfo(nodeURL string) (*types.SignedTeeInfoResponse, error) { + result, err := http.Get(nodeURL + "/info") + if err != nil { + return nil, errors.Errorf("%s", err) + } + defer result.Body.Close() + + var teeInfo types.SignedTeeInfoResponse + err = json.NewDecoder(result.Body).Decode(&teeInfo) + if err != nil { + return nil, errors.Errorf("%s", err) + } + + return &teeInfo, nil +} + +func CodeHashAndPlatform(attestationString string) (common.Hash, common.Hash, error) { + claims := attestation.NeededClaims{} + _, _, err := googlecloud.ParsePKITokenUnverifiedClaims(attestationString, &claims) + if err != nil { + return common.Hash{}, common.Hash{}, errors.Errorf("%s", err) + } + + codeHash, err := claims.CodeHash() + if err != nil { + return common.Hash{}, common.Hash{}, errors.Errorf("%s", err) + } + platform, err := claims.Platform() + if err != nil { + return common.Hash{}, common.Hash{}, errors.Errorf("%s", err) + } + + return codeHash, platform, nil +} + +func TeeProxyId(teeInfo *types.SignedTeeInfoResponse) (common.Address, common.Address, error) { + pubKey, err := types.ParsePubKey(teeInfo.TeeInfo.PublicKey) + if err != nil { + return common.Address{}, common.Address{}, errors.Errorf("%s", err) + } + + teeID := crypto.PubkeyToAddress(*pubKey) + + hash, err := teeInfo.TeeInfo.Hash() + if err != nil { + return common.Address{}, common.Address{}, errors.Errorf("%s", err) + } + // The proxy signs the TEE info over a domain-separated, chain-ID-bound + // payload (Payload{ProxyTeeInfo, chainID, infoHash}) — see tee-proxy + // external.go. Recover the proxy address over the SAME preimage, or the + // proxyId comes out garbage and the on-chain availability check is rejected + // by the verifier with "proxy signer does not match". + infoSignHash, err := csigning.NewPayload(csigning.ProxyTeeInfo, teeInfo.TeeInfo.ChainID, common.BytesToHash(hash)).Hash() + if err != nil { + return common.Address{}, common.Address{}, errors.Errorf("%s", err) + } + proxyPubKey, err := crypto.SigToPub(accounts.TextHash(infoSignHash[:]), teeInfo.ProxySignature) + if err != nil { + return common.Address{}, common.Address{}, errors.Errorf("%s", err) + } + proxyID := crypto.PubkeyToAddress(*proxyPubKey) + + return teeID, proxyID, nil +} + +func ActionResult(nodeURL string, actionID common.Hash) (*types.ActionResponse, error) { + var result *http.Response + var err error + for range repeats { + result, err = http.Get(nodeURL + "/action/result/" + actionID.Hex()) + if err == nil && result.StatusCode == http.StatusOK { + break + } + time.Sleep(2 * time.Second) + } + if err != nil { + return nil, errors.Errorf("%s", err) + } + if result.StatusCode != http.StatusOK { + logger.Warnf("action result status not ok: got: %d for %s, %s", result.StatusCode, actionID.Hex(), nodeURL) + return nil, errors.Errorf("action result status not ok, got: %d", result.StatusCode) + } + defer result.Body.Close() + + var response types.ActionResponse + err = json.NewDecoder(result.Body).Decode(&response) + if err != nil { + return nil, errors.Errorf("%s", err) + } + + return &response, nil +} + +func SetProxyUrl(configurationPort int, proxyPort int) error { + url := fmt.Sprintf("http://localhost:%d", proxyPort) + request := types.ConfigureProxyURLRequest{ + URL: &url, + } + + body, err := json.Marshal(request) + if err != nil { + return err + } + + url = fmt.Sprintf("http://localhost:%d/proxy", configurationPort) + logger.Infof("Setting proxy url on tee: %s", url) + resp, err := http.Post(url, "application/json", bytes.NewReader(body)) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + return nil +} diff --git a/tools/pkg/support/support.go b/tools/pkg/support/support.go new file mode 100644 index 0000000..c554e0e --- /dev/null +++ b/tools/pkg/support/support.go @@ -0,0 +1,331 @@ +package support + +import ( + "concord/tools/pkg/configs" + "context" + "crypto/ecdsa" + "encoding/hex" + "encoding/json" + stderrors "errors" + "fmt" + "math/big" + "os" + "reflect" + "strings" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/flare-foundation/go-flare-common/pkg/contracts/fdc2/fdc2hub" + "github.com/flare-foundation/go-flare-common/pkg/contracts/system" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/extensiongovernance" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/extensionmanager" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/machinemanager" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/ownerallowlist" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/verification" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/walletkeymanager" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/walletmanager" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/walletprojectmanager" + "github.com/joho/godotenv" + + "github.com/pkg/errors" +) + +type Support struct { + Prv *ecdsa.PrivateKey // funded private key + + FlareSystemManager *system.FlareSystemsManager + Fdc2Hub *fdc2hub.Fdc2Hub + + // Diamond-pattern facet bindings. All are bound to the same FlareTeeManager + // (diamond proxy) address; the diamond routes each call to the correct facet. + TeeMachineRegistry *machinemanager.MachineManager + TeeWalletProjectManager *walletprojectmanager.WalletProjectManager + TeeWalletManager *walletmanager.WalletManager + TeeWalletKeyManager *walletkeymanager.WalletKeyManager + TeeVerification *verification.Verification + TeeExtensionRegistry *extensionmanager.ExtensionManager + TeeExtensionGovernance *extensiongovernance.ExtensionGovernance + TeeOwnerAllowlist *ownerallowlist.OwnerAllowlist + + Addresses *Addresses + + ChainClient *ethclient.Client + ChainID *big.Int +} + +type Addresses struct { + FlareSystemManager common.Address `json:"FlareSystemsManager"` + Fdc2Hub common.Address `json:"Fdc2Hub"` + // FlareTeeManager is the diamond proxy that exposes every TEE facet + // (ExtensionManager, MachineManager, Verification, WalletManager, + // WalletKeyManager, WalletProjectManager, OwnerAllowlist) at a single address. + FlareTeeManager common.Address `json:"FlareTeeManager"` +} + +func DefaultSupport(AddressesFilePath, chainNodeURL string) (*Support, error) { + addr := &Addresses{} + err := configs.ReadAddresses(AddressesFilePath, addr) + if err != nil { + addr, err = ParseAddresses(AddressesFilePath) + if err != nil { + return nil, errors.Errorf("%s", err) + } + } + + cc, err := ethclient.Dial(chainNodeURL) + if err != nil { + return nil, errors.Errorf("%s", err) + } + + privKey, err := DefaultPrivateKey() + if err != nil { + return nil, err + } + + return NewSupport(privKey, cc, addr) +} + +func DefaultPrivateKey() (*ecdsa.PrivateKey, error) { + if err := godotenv.Load(); err != nil { + fmt.Fprintf(os.Stderr, "Warning: Error loading .env file: %v\n", err) + } + privKeyString := os.Getenv("DEPLOYMENT_PRIVATE_KEY") + + if privKeyString == "" { + return nil, errors.New("DEPLOYMENT_PRIVATE_KEY must be set; no private-key fallback is configured") + } + + if strings.HasPrefix(privKeyString, "0x") || strings.HasPrefix(privKeyString, "0X") { + privKeyString = privKeyString[2:] + } + + privKey, err := crypto.HexToECDSA(privKeyString) + if err != nil { + return nil, errors.Errorf("%s", err) + } + return privKey, nil +} + +func NewSupport(prv *ecdsa.PrivateKey, chainClient *ethclient.Client, addresses *Addresses) (*Support, error) { + // Every TEE facet binding is wired to the FlareTeeManager diamond proxy. + diamond := addresses.FlareTeeManager + + tr, err := machinemanager.NewMachineManager(diamond, chainClient) + if err != nil { + return nil, err + } + + twm, err := walletmanager.NewWalletManager(diamond, chainClient) + if err != nil { + return nil, err + } + + twkm, err := walletkeymanager.NewWalletKeyManager(diamond, chainClient) + if err != nil { + return nil, err + } + + twpm, err := walletprojectmanager.NewWalletProjectManager(diamond, chainClient) + if err != nil { + return nil, err + } + + sm, err := system.NewFlareSystemsManager(addresses.FlareSystemManager, chainClient) + if err != nil { + return nil, err + } + + ftdc, err := fdc2hub.NewFdc2Hub(addresses.Fdc2Hub, chainClient) + if err != nil { + return nil, err + } + + tv, err := verification.NewVerification(diamond, chainClient) + if err != nil { + return nil, err + } + + ter, err := extensionmanager.NewExtensionManager(diamond, chainClient) + if err != nil { + return nil, err + } + + toal, err := ownerallowlist.NewOwnerAllowlist(diamond, chainClient) + if err != nil { + return nil, err + } + + teg, err := extensiongovernance.NewExtensionGovernance(diamond, chainClient) + if err != nil { + return nil, err + } + + chainID, err := chainClient.ChainID(context.Background()) + if err != nil { + return nil, err + } + + return &Support{ + Prv: prv, + TeeMachineRegistry: tr, + TeeWalletManager: twm, + TeeWalletKeyManager: twkm, + TeeWalletProjectManager: twpm, + FlareSystemManager: sm, + Fdc2Hub: ftdc, + TeeVerification: tv, + TeeExtensionRegistry: ter, + ChainClient: chainClient, + ChainID: chainID, + TeeOwnerAllowlist: toal, + TeeExtensionGovernance: teg, + Addresses: addresses, + }, nil +} + +func CheckTx(tx *types.Transaction, client *ethclient.Client) (*types.Receipt, error) { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + receipt, err := bind.WaitMined(ctx, client, tx) + if err != nil { + return nil, errors.Errorf("transaction not mined within 2 minutes (tx: %s): %s", tx.Hash().Hex(), err) + } + if receipt.Status == 0 { + reason, err := getFailingMessage(client, tx.Hash()) + if err != nil { + return nil, err + } + return nil, errors.Errorf("error: Transaction fail: %s", reason) + } + + return receipt, nil +} + +func getFailingMessage(client *ethclient.Client, hash common.Hash) (string, error) { + tx, _, err := client.TransactionByHash(context.Background(), hash) + if err != nil { + return "", errors.Errorf("%s", err) + } + + from, err := types.Sender(types.NewEIP155Signer(tx.ChainId()), tx) + if err != nil { + return "", errors.Errorf("%s", err) + } + + msg := ethereum.CallMsg{ + From: from, + To: tx.To(), + Gas: tx.Gas(), + GasPrice: tx.GasPrice(), + Value: tx.Value(), + Data: tx.Data(), + } + + res, err := client.CallContract(context.Background(), msg, nil) + if err != nil { + // Try to decode revert reason from the error itself + if reason := decodeRevertFromError(err); reason != "" { + return reason, nil + } + return err.Error(), nil + } + + // Decode ABI-encoded revert reason from call result + if reason, unpackErr := abi.UnpackRevert(res); unpackErr == nil { + return reason, nil + } + + // Fallback: return hex-encoded bytes instead of raw binary garbage + if len(res) > 0 { + return fmt.Sprintf("0x%x", res), nil + } + + return "unknown revert reason", nil +} + +// decodeRevertFromError extracts a revert reason from an RPC error's data field. +func decodeRevertFromError(err error) string { + type dataError interface { + ErrorData() interface{} + } + var de dataError + if stderrors.As(err, &de) { + if data := de.ErrorData(); data != nil { + if hexStr, ok := data.(string); ok { + hexStr = strings.TrimPrefix(hexStr, "0x") + decoded, decErr := hex.DecodeString(hexStr) + if decErr == nil { + if reason, unpackErr := abi.UnpackRevert(decoded); unpackErr == nil { + return reason + } + } + } + } + } + return "" +} + +// RawContract mirrors the JSON entries +type RawContract struct { + Name string `json:"name"` + ContractName string `json:"contractName"` + Address string `json:"address"` +} + +func ParseAddresses(filePath string) (*Addresses, error) { + file, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + var raw []RawContract + err = json.Unmarshal(file, &raw) + if err != nil { + return nil, err + } + + var addrs Addresses + targets := fieldMap(&addrs) + + for _, c := range raw { + if dest, ok := targets[c.Name]; ok { + *dest = common.HexToAddress(c.Address) + } + } + + return &addrs, err +} + +// fieldMap builds: "" -> pointer to field +func fieldMap(addrStruct *Addresses) map[string]*common.Address { + out := make(map[string]*common.Address) + + v := reflect.ValueOf(addrStruct).Elem() + t := v.Type() + addrType := reflect.TypeOf(common.Address{}) + + for i := 0; i < v.NumField(); i++ { + fv := v.Field(i) + ft := t.Field(i) + + if fv.Type() != addrType { + continue + } + + tag := ft.Tag.Get("json") + key := tag + if key == "" || key == "-" { + key = ft.Name + } + + out[key] = fv.Addr().Interface().(*common.Address) + } + + return out +} diff --git a/tools/pkg/support/support_test.go b/tools/pkg/support/support_test.go new file mode 100644 index 0000000..b855824 --- /dev/null +++ b/tools/pkg/support/support_test.go @@ -0,0 +1,160 @@ +package support + +import ( + "encoding/hex" + stderrors "errors" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/crypto" +) + +// mockDataError implements the ErrorData() interface used by go-ethereum +// JSON-RPC error types. +type mockDataError struct { + msg string + data interface{} +} + +func (e *mockDataError) Error() string { return e.msg } +func (e *mockDataError) ErrorData() interface{} { return e.data } + +// makeRevertHex creates valid ABI-encoded Error(string) revert data as hex. +func makeRevertHex(msg string) string { + selector := crypto.Keccak256([]byte("Error(string)"))[:4] + stringTy, _ := abi.NewType("string", "", nil) + args := abi.Arguments{{Type: stringTy}} + encoded, _ := args.Pack(msg) + return hex.EncodeToString(append(selector, encoded...)) +} + +// --- decodeRevertFromError tests --- + +func TestDecodeRevertFromError_StandardRevert(t *testing.T) { + revertHex := "0x" + makeRevertHex("Extension ID already set.") + err := &mockDataError{ + msg: "execution reverted", + data: revertHex, + } + result := decodeRevertFromError(err) + if result != "Extension ID already set." { + t.Errorf("expected %q, got %q", "Extension ID already set.", result) + } +} + +func TestDecodeRevertFromError_NoPrefix(t *testing.T) { + revertHex := makeRevertHex("Extension ID not found.") + err := &mockDataError{ + msg: "execution reverted", + data: revertHex, + } + result := decodeRevertFromError(err) + if result != "Extension ID not found." { + t.Errorf("expected %q, got %q", "Extension ID not found.", result) + } +} + +func TestDecodeRevertFromError_NoInterface(t *testing.T) { + result := decodeRevertFromError(stderrors.New("plain error without ErrorData")) + if result != "" { + t.Errorf("expected empty string for plain error, got %q", result) + } +} + +func TestDecodeRevertFromError_InvalidHex(t *testing.T) { + err := &mockDataError{ + msg: "execution reverted", + data: "zzzz-not-hex", + } + result := decodeRevertFromError(err) + if result != "" { + t.Errorf("expected empty string for invalid hex, got %q", result) + } +} + +func TestDecodeRevertFromError_ShortData(t *testing.T) { + // Only 2 bytes — too short for ABI selector + err := &mockDataError{ + msg: "execution reverted", + data: "0x1234", + } + result := decodeRevertFromError(err) + if result != "" { + t.Errorf("expected empty string for short data, got %q", result) + } +} + +func TestDecodeRevertFromError_NilData(t *testing.T) { + err := &mockDataError{ + msg: "execution reverted", + data: nil, + } + result := decodeRevertFromError(err) + if result != "" { + t.Errorf("expected empty string for nil data, got %q", result) + } +} + +func TestDecodeRevertFromError_NonStringData(t *testing.T) { + err := &mockDataError{ + msg: "execution reverted", + data: 12345, + } + result := decodeRevertFromError(err) + if result != "" { + t.Errorf("expected empty string for non-string data, got %q", result) + } +} + +func TestDecodeRevertFromError_WrongSelector(t *testing.T) { + // Valid hex, 4+ bytes, but wrong selector (not Error(string)) + err := &mockDataError{ + msg: "execution reverted", + data: "0xdeadbeef00000000000000000000000000000000000000000000000000000000", + } + result := decodeRevertFromError(err) + // decodeRevertFromError only tries abi.UnpackRevert which requires the Error(string) selector. + // Wrong selector → UnpackRevert fails → returns "" + if result != "" { + t.Errorf("expected empty string for wrong selector, got %q", result) + } +} + +func TestDecodeRevertFromError_WrappedDataError(t *testing.T) { + revertHex := "0x" + makeRevertHex("insufficient funds") + innerErr := &mockDataError{ + msg: "execution reverted", + data: revertHex, + } + wrappedErr := stderrors.Join(stderrors.New("outer"), innerErr) + result := decodeRevertFromError(wrappedErr) + if result != "insufficient funds" { + t.Errorf("expected %q, got %q", "insufficient funds", result) + } +} + +func TestDecodeRevertFromError_AllContractReverts(t *testing.T) { + reverts := []string{ + "TeeExtensionRegistry cannot be zero address", + "TeeMachineRegistry cannot be zero address", + "TeeExtensionRegistry has no code", + "TeeMachineRegistry has no code", + "Extension ID already set.", + "Extension ID not found.", + "Extension ID is not set.", + } + + for _, msg := range reverts { + t.Run(msg, func(t *testing.T) { + revertHex := "0x" + makeRevertHex(msg) + err := &mockDataError{ + msg: "execution reverted", + data: revertHex, + } + result := decodeRevertFromError(err) + if result != msg { + t.Errorf("expected %q, got %q", msg, result) + } + }) + } +} diff --git a/tools/pkg/utils/instructions.go b/tools/pkg/utils/instructions.go new file mode 100644 index 0000000..17160d5 --- /dev/null +++ b/tools/pkg/utils/instructions.go @@ -0,0 +1,121 @@ +package utils + +import ( + "context" + "math/big" + "time" + + "concord/tools/pkg/contracts/concordsender" + "concord/tools/pkg/support" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/pkg/errors" +) + +const instructionFeeWei = 1_000_000 + +func DeployInstructionSender(s *support.Support) (common.Address, *concordsender.ConcordInstructionSender, error) { + opts, err := bind.NewKeyedTransactorWithChainID(s.Prv, s.ChainID) + if err != nil { + return common.Address{}, nil, errors.Errorf("failed to create transactor: %s", err) + } + address, tx, contract, err := concordsender.DeployConcordInstructionSender( + opts, s.ChainClient, s.Addresses.FlareTeeManager, s.Addresses.FlareTeeManager, + ) + if err != nil { + return common.Address{}, nil, errors.Errorf("failed to deploy ConcordInstructionSender: %s", err) + } + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + receipt, err := bind.WaitMined(ctx, s.ChainClient, tx) + if err != nil { + return common.Address{}, nil, errors.Errorf("deployment tx not mined within 2 minutes (tx: %s): %s", tx.Hash().Hex(), err) + } + if receipt.Status != types.ReceiptStatusSuccessful { + return common.Address{}, nil, errors.New("ConcordInstructionSender deployment failed") + } + return address, contract, nil +} + +func SetExtensionId(s *support.Support, instructionSenderAddress common.Address) error { + sender, err := concordsender.NewConcordInstructionSender(instructionSenderAddress, s.ChainClient) + if err != nil { + return errors.Errorf("failed to bind ConcordInstructionSender: %s", err) + } + opts, err := bind.NewKeyedTransactorWithChainID(s.Prv, s.ChainID) + if err != nil { + return errors.Errorf("failed to create transactor: %s", err) + } + tx, err := sender.SetExtensionId(opts) + if err != nil { + return errors.Errorf("failed to call setExtensionId: %s", err) + } + receipt, err := bind.WaitMined(context.Background(), s.ChainClient, tx) + if err != nil { + return errors.Errorf("failed waiting for setExtensionId transaction: %s", err) + } + if receipt.Status != types.ReceiptStatusSuccessful { + return errors.New("setExtensionId transaction failed") + } + return nil +} + +func SendSubmitQuote(s *support.Support, instructionSenderAddress common.Address, encryptedPayload []byte) (common.Hash, common.Hash, error) { + sender, err := concordsender.NewConcordInstructionSender(instructionSenderAddress, s.ChainClient) + if err != nil { + return common.Hash{}, common.Hash{}, errors.Errorf("failed to bind ConcordInstructionSender: %s", err) + } + opts, err := bind.NewKeyedTransactorWithChainID(s.Prv, s.ChainID) + if err != nil { + return common.Hash{}, common.Hash{}, errors.Errorf("failed to create transactor: %s", err) + } + opts.Value = big.NewInt(instructionFeeWei) + tx, err := sender.SendSubmitQuote(opts, encryptedPayload) + if err != nil { + return common.Hash{}, common.Hash{}, errors.Errorf("failed to send SUBMIT_QUOTE: %s", err) + } + return parseInstructionReceipt(s, tx) +} + +func SendFinalizeRound(s *support.Support, instructionSenderAddress common.Address, encryptedPayload []byte) (common.Hash, common.Hash, error) { + sender, err := concordsender.NewConcordInstructionSender(instructionSenderAddress, s.ChainClient) + if err != nil { + return common.Hash{}, common.Hash{}, errors.Errorf("failed to bind ConcordInstructionSender: %s", err) + } + opts, err := bind.NewKeyedTransactorWithChainID(s.Prv, s.ChainID) + if err != nil { + return common.Hash{}, common.Hash{}, errors.Errorf("failed to create transactor: %s", err) + } + opts.Value = big.NewInt(instructionFeeWei) + tx, err := sender.SendFinalizeRound(opts, encryptedPayload) + if err != nil { + return common.Hash{}, common.Hash{}, errors.Errorf("failed to send FINALIZE_ROUND: %s", err) + } + return parseInstructionReceipt(s, tx) +} + +func parseInstructionReceipt(s *support.Support, tx *types.Transaction) (common.Hash, common.Hash, error) { + receipt, err := bind.WaitMined(context.Background(), s.ChainClient, tx) + if err != nil { + return common.Hash{}, common.Hash{}, errors.Errorf("failed waiting for transaction: %s", err) + } + if receipt.Status != types.ReceiptStatusSuccessful { + return common.Hash{}, common.Hash{}, errors.Errorf("transaction failed with status: %d", receipt.Status) + } + for _, log := range receipt.Logs { + instructionSent, parseErr := s.TeeVerification.ParseTeeInstructionsSent(*log) + if parseErr == nil { + return instructionSent.InstructionId, receipt.TxHash, nil + } + } + return common.Hash{}, common.Hash{}, errors.New("no TeeInstructionsSent event found in receipt") +} + +// SenderAddress is used by callers that need the configured deployer address +// without reimplementing the key derivation rule. +func SenderAddress(s *support.Support) common.Address { + return crypto.PubkeyToAddress(s.Prv.PublicKey) +} diff --git a/tools/pkg/validate/checks.go b/tools/pkg/validate/checks.go new file mode 100644 index 0000000..b04b2c3 --- /dev/null +++ b/tools/pkg/validate/checks.go @@ -0,0 +1,1051 @@ +package validate + +import ( + "bufio" + "context" + "crypto/ecdsa" + "fmt" + "math/big" + "net" + "net/http" + "os" + "regexp" + "strings" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/extensionmanager" + "github.com/flare-foundation/go-flare-common/pkg/contracts/tee/ownerallowlist" + "github.com/flare-foundation/tee-node/pkg/wallets" +) + +var ( + addressRegex = regexp.MustCompile(`^0x[0-9a-fA-F]{40}$`) + extensionIDRegex = regexp.MustCompile(`^0x[0-9a-fA-F]{64}$`) +) + +// RegisterDeployChecks adds all Step 1 (Contract Deployment) checks to a Report. +func RegisterDeployChecks(r *Report, client *ethclient.Client, key *ecdsa.PrivateKey, addresses map[string]common.Address, extensionEnvPath string) { + // D3: For each address, check that it is not zero. + zeroAddrs := make(map[string]bool) + for label, addr := range addresses { + if err := AddressNotZero(addr, label); err != nil { + r.Add(CheckResult{ + Step: "deploy", + ID: "D3", + Name: label + " address not zero", + Status: FAIL, + Message: err.Error(), + Fix: "Re-run the deploy script or check deployed-addresses.json", + }) + zeroAddrs[label] = true + } else { + r.Add(CheckResult{ + Step: "deploy", + ID: "D3", + Name: label + " address not zero", + Status: PASS, + Message: addr.Hex(), + }) + } + } + + // D1/D2: For each non-zero address, check that code is deployed. + for label, addr := range addresses { + if zeroAddrs[label] { + continue + } + if err := AddressHasCode(client, addr, label); err != nil { + r.Add(CheckResult{ + Step: "deploy", + ID: "D1", + Name: label + " has deployed code", + Status: FAIL, + Message: err.Error(), + Fix: "Deploy contracts or check CHAIN_URL / deployed-addresses.json", + }) + } else { + r.Add(CheckResult{ + Step: "deploy", + ID: "D1", + Name: label + " has deployed code", + Status: PASS, + Message: fmt.Sprintf("code found at %s", addr.Hex()), + }) + } + } + + // D5: Check deployer key source. + r.Add(CheckDeployerKeySource()) + + // D5: Check deployer funds. + if key != nil && client != nil { + if err := KeyHasFunds(client, key, MinDeployBalance); err != nil { + r.Add(CheckResult{ + Step: "deploy", + ID: "D5", + Name: "deployer has sufficient funds", + Status: FAIL, + Message: err.Error(), + Fix: "Fund the deployer account before deploying", + }) + } else { + r.Add(CheckResult{ + Step: "deploy", + ID: "D5", + Name: "deployer has sufficient funds", + Status: PASS, + Message: "balance above minimum threshold", + }) + } + } + + // D6: Check chain ID. + if client != nil { + r.Add(checkChainID(client)) + } + + // D7: Check extension.env format. + if extensionEnvPath != "" { + results := CheckExtensionEnvFormat(extensionEnvPath) + for _, res := range results { + r.Add(res) + } + } +} + +// CheckDeployerKeySource checks whether the deployer private key is configured. +func CheckDeployerKeySource() CheckResult { + privKey := os.Getenv("DEPLOYMENT_PRIVATE_KEY") + if privKey != "" { + return CheckResult{ + Step: "deploy", + ID: "D5", + Name: "deployer key source", + Status: PASS, + Message: "DEPLOYMENT_PRIVATE_KEY is set", + } + } + + localMode := os.Getenv("LOCAL_MODE") + if localMode == "true" || localMode == "" { + return CheckResult{ + Step: "deploy", + ID: "D5", + Name: "deployer key source", + Status: PASS, + Message: "using Hardhat dev key (LOCAL_MODE)", + } + } + + return CheckResult{ + Step: "deploy", + ID: "D5", + Name: "deployer key source", + Status: WARN, + Message: "DEPLOYMENT_PRIVATE_KEY not set — using Hardhat dev key which has no funds on Coston2", + Fix: "Set DEPLOYMENT_PRIVATE_KEY in .env to a funded account on the target network", + } +} + +// CheckExtensionEnvFormat parses an extension.env file and validates its entries. +func CheckExtensionEnvFormat(path string) []CheckResult { + f, err := os.Open(path) + if err != nil { + return []CheckResult{{ + Step: "deploy", + ID: "D7", + Name: "extension.env exists", + Status: SKIP, + Message: "file not found: " + path, + }} + } + defer f.Close() + + // Parse key=value pairs. + vars := make(map[string]string) + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + vars[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + } + } + + var results []CheckResult + fix := "Delete config/extension.env and re-run scripts/pre-build.sh" + + // Check INSTRUCTION_SENDER. + if val, ok := vars["INSTRUCTION_SENDER"]; !ok { + results = append(results, CheckResult{ + Step: "deploy", + ID: "D7", + Name: "INSTRUCTION_SENDER in extension.env", + Status: FAIL, + Message: "INSTRUCTION_SENDER not found in extension.env", + Fix: fix, + }) + } else if !addressRegex.MatchString(val) { + results = append(results, CheckResult{ + Step: "deploy", + ID: "D7", + Name: "INSTRUCTION_SENDER in extension.env", + Status: FAIL, + Message: fmt.Sprintf("INSTRUCTION_SENDER has invalid format: %q", val), + Fix: fix, + }) + } else { + results = append(results, CheckResult{ + Step: "deploy", + ID: "D7", + Name: "INSTRUCTION_SENDER in extension.env", + Status: PASS, + Message: val, + }) + } + + // Check EXTENSION_ID. + if val, ok := vars["EXTENSION_ID"]; !ok { + results = append(results, CheckResult{ + Step: "deploy", + ID: "D7", + Name: "EXTENSION_ID in extension.env", + Status: FAIL, + Message: "EXTENSION_ID not found in extension.env", + Fix: fix, + }) + } else if !extensionIDRegex.MatchString(val) { + results = append(results, CheckResult{ + Step: "deploy", + ID: "D7", + Name: "EXTENSION_ID in extension.env", + Status: FAIL, + Message: fmt.Sprintf("EXTENSION_ID has invalid format: %q", val), + Fix: fix, + }) + } else { + results = append(results, CheckResult{ + Step: "deploy", + ID: "D7", + Name: "EXTENSION_ID in extension.env", + Status: PASS, + Message: val, + }) + } + + return results +} + +// parseExtensionEnv reads EXTENSION_ID and INSTRUCTION_SENDER from an extension.env file. +// Returns empty strings and nil error if file doesn't exist. +func parseExtensionEnv(path string) (extensionID string, instructionSender string, err error) { + f, err := os.Open(path) + if err != nil { + return "", "", nil + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + key := strings.TrimSpace(parts[0]) + val := strings.TrimSpace(parts[1]) + switch key { + case "EXTENSION_ID": + extensionID = val + case "INSTRUCTION_SENDER": + instructionSender = val + } + } + return extensionID, instructionSender, nil +} + +// RegisterRegistrationChecks adds all registration (R1-R7) checks to a Report. +func RegisterRegistrationChecks( + r *Report, + client *ethclient.Client, + key *ecdsa.PrivateKey, + registry *extensionmanager.ExtensionManager, + allowlist *ownerallowlist.OwnerAllowlist, + extensionEnvPath string, +) { + // If no config path provided, skip all registration checks. + if extensionEnvPath == "" { + r.Add(CheckResult{ + Step: "register", + ID: "R1-R7", + Name: "registration checks", + Status: SKIP, + Message: "no --config path provided", + }) + return + } + + // Parse extension.env. + extIDStr, instrSender, err := parseExtensionEnv(extensionEnvPath) + if err != nil { + r.Add(CheckResult{ + Step: "register", + ID: "R1-R7", + Name: "registration checks", + Status: FAIL, + Message: fmt.Sprintf("failed to parse extension.env: %v", err), + }) + return + } + if extIDStr == "" && instrSender == "" { + r.Add(CheckResult{ + Step: "register", + ID: "R1-R7", + Name: "registration checks", + Status: SKIP, + Message: "config/extension.env not found — run pre-build.sh first", + }) + return + } + + callOpts := &bind.CallOpts{Context: context.Background()} + + // R1: Check next public extension id (formerly extensions counter). + var counter *big.Int + if registry != nil && client != nil { + counter, err = registry.NextPublicExtensionId(callOpts) + if err != nil { + r.Add(CheckResult{ + Step: "register", + ID: "R1", + Name: "next public extension id", + Status: FAIL, + Message: fmt.Sprintf("failed to query nextPublicExtensionId: %v", err), + }) + } else if counter.Sign() == 0 { + r.Add(CheckResult{ + Step: "register", + ID: "R1", + Name: "next public extension id", + Status: WARN, + Message: "nextPublicExtensionId is 0 — no public extensions registered yet", + }) + } else { + r.Add(CheckResult{ + Step: "register", + ID: "R1", + Name: "next public extension id", + Status: PASS, + Message: fmt.Sprintf("nextPublicExtensionId = %s", counter.String()), + }) + } + } + + // Validate EXTENSION_ID format. + if !extensionIDRegex.MatchString(extIDStr) { + r.Add(CheckResult{ + Step: "register", + ID: "R2", + Name: "EXTENSION_ID format", + Status: FAIL, + Message: fmt.Sprintf("malformed EXTENSION_ID: %q", extIDStr), + Fix: "Delete config/extension.env and re-run scripts/pre-build.sh", + }) + return + } + + // Parse extension ID from hex. + extensionID := new(big.Int) + extensionID.SetString(extIDStr[2:], 16) + + // R2: Check instruction sender matches on-chain. + if registry != nil && client != nil && addressRegex.MatchString(instrSender) { + onChainSender, err := registry.GetTeeExtensionInstructionsSender(callOpts, extensionID) + if err != nil { + r.Add(CheckResult{ + Step: "register", + ID: "R2", + Name: "instruction sender matches on-chain", + Status: FAIL, + Message: fmt.Sprintf("failed to query instruction sender: %v", err), + }) + } else { + expectedAddr := common.HexToAddress(instrSender) + if onChainSender == expectedAddr { + r.Add(CheckResult{ + Step: "register", + ID: "R2", + Name: "instruction sender matches on-chain", + Status: PASS, + Message: fmt.Sprintf("on-chain sender %s matches extension.env", onChainSender.Hex()), + }) + } else { + r.Add(CheckResult{ + Step: "register", + ID: "R2", + Name: "instruction sender matches on-chain", + Status: FAIL, + Message: fmt.Sprintf("on-chain sender %s != extension.env %s", onChainSender.Hex(), instrSender), + Fix: "Re-register the extension or update extension.env", + }) + } + } + } + + // R3: Check deployer is allowed TEE machine owner. + if key != nil && allowlist != nil && client != nil { + deployerAddr := crypto.PubkeyToAddress(key.PublicKey) + allowed, err := allowlist.IsAllowedTeeMachineOwner(callOpts, extensionID, deployerAddr) + if err != nil { + r.Add(CheckResult{ + Step: "register", + ID: "R3", + Name: "deployer is allowed TEE machine owner", + Status: FAIL, + Message: fmt.Sprintf("failed to query allowlist: %v", err), + }) + } else if allowed { + r.Add(CheckResult{ + Step: "register", + ID: "R3", + Name: "deployer is allowed TEE machine owner", + Status: PASS, + Message: "deployer is on the TEE machine owner allowlist", + }) + } else { + r.Add(CheckResult{ + Step: "register", + ID: "R3", + Name: "deployer is allowed TEE machine owner", + Status: FAIL, + Message: "deployer is NOT on the TEE machine owner allowlist", + Fix: "Run the register step to add the deployer as a TEE machine owner", + }) + } + } + + // R4: Check EVM key type is supported. + if registry != nil && client != nil { + var evmTypeBytes [32]byte + copy(evmTypeBytes[:], wallets.EVMType[:]) + supported, err := registry.IsKeyTypeSupported(callOpts, extensionID, evmTypeBytes) + if err != nil { + r.Add(CheckResult{ + Step: "register", + ID: "R4", + Name: "EVM key type supported", + Status: FAIL, + Message: fmt.Sprintf("failed to query key type support: %v", err), + }) + } else if supported { + r.Add(CheckResult{ + Step: "register", + ID: "R4", + Name: "EVM key type supported", + Status: PASS, + Message: "EVM key type is supported for this extension", + }) + } else { + r.Add(CheckResult{ + Step: "register", + ID: "R4", + Name: "EVM key type supported", + Status: FAIL, + Message: "EVM key type is NOT supported for this extension", + Fix: "Run the register step to add EVM key type support", + }) + } + } + + // R7: Check for duplicate instruction sender across extensions. + if counter != nil && counter.Sign() > 0 && addressRegex.MatchString(instrSender) && registry != nil && client != nil { + expectedAddr := common.HexToAddress(instrSender) + var matchingIDs []string + for i := int64(0); i < counter.Int64(); i++ { + id := big.NewInt(i) + sender, err := registry.GetTeeExtensionInstructionsSender(callOpts, id) + if err != nil { + continue + } + if sender == expectedAddr { + matchingIDs = append(matchingIDs, id.String()) + } + } + if len(matchingIDs) > 1 { + r.Add(CheckResult{ + Step: "register", + ID: "R7", + Name: "unique instruction sender", + Status: WARN, + Message: fmt.Sprintf("INSTRUCTION_SENDER %s is used by multiple extensions: %s", instrSender, strings.Join(matchingIDs, ", ")), + }) + } else { + r.Add(CheckResult{ + Step: "register", + ID: "R7", + Name: "unique instruction sender", + Status: PASS, + Message: "instruction sender is unique across registered extensions", + }) + } + } +} + +// checkChainID queries the connected chain for its ID and reports the network name. +func checkChainID(client *ethclient.Client) CheckResult { + ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout) + defer cancel() + + chainID, err := client.ChainID(ctx) + if err != nil { + return CheckResult{ + Step: "deploy", + ID: "D6", + Name: "chain ID", + Status: FAIL, + Message: fmt.Sprintf("failed to get chain ID: %v", err), + Fix: "Check CHAIN_URL", + } + } + + name := chainName(chainID) + return CheckResult{ + Step: "deploy", + ID: "D6", + Name: "chain ID", + Status: PASS, + Message: fmt.Sprintf("chain ID %s (%s)", chainID.String(), name), + } +} + +// chainName returns a human-readable name for known chain IDs. +func chainName(id interface{ String() string }) string { + switch id.String() { + case "31337": + return "Hardhat/Anvil (local devnet)" + case "114": + return "Coston2 (testnet)" + case "14": + return "Flare (mainnet)" + default: + return "unknown network" + } +} + +// RegisterServicesChecks adds service-readiness checks (S2, S5, S7, S9, S10) to a Report. +func RegisterServicesChecks(r *Report, extensionEnvPath string) { + // S2: EXTENSION_ID is set and well-formed. + if extensionEnvPath == "" { + r.Add(CheckResult{ + Step: "services", + ID: "S2", + Name: "EXTENSION_ID is set and well-formed", + Status: SKIP, + Message: "run pre-build.sh first", + }) + } else { + extID, _, _ := parseExtensionEnv(extensionEnvPath) + if extID == "" { + r.Add(CheckResult{ + Step: "services", + ID: "S2", + Name: "EXTENSION_ID is set and well-formed", + Status: FAIL, + Message: "EXTENSION_ID is empty or not found in extension.env", + Fix: "Delete config/extension.env and re-run scripts/pre-build.sh", + }) + } else if !extensionIDRegex.MatchString(extID) { + r.Add(CheckResult{ + Step: "services", + ID: "S2", + Name: "EXTENSION_ID is set and well-formed", + Status: FAIL, + Message: fmt.Sprintf("EXTENSION_ID has invalid format: %q", extID), + Fix: "Delete config/extension.env and re-run scripts/pre-build.sh", + }) + } else { + r.Add(CheckResult{ + Step: "services", + ID: "S2", + Name: "EXTENSION_ID is set and well-formed", + Status: PASS, + Message: extID, + }) + } + } + + // S5: Extension port not in use. + port := os.Getenv("EXTENSION_PORT") + if port == "" { + port = "7702" + } + conn, err := net.DialTimeout("tcp", "localhost:"+port, 1*time.Second) + if err == nil { + conn.Close() + r.Add(CheckResult{ + Step: "services", + ID: "S5", + Name: "extension port not in use", + Status: WARN, + Message: fmt.Sprintf("port %s is already in use — extension server will fail to bind", port), + Fix: "Stop the process using that port or change EXTENSION_PORT", + }) + } else { + r.Add(CheckResult{ + Step: "services", + ID: "S5", + Name: "extension port not in use", + Status: PASS, + Message: "port available", + }) + } + + // S7: Port configuration. + extPort := os.Getenv("EXTENSION_PORT") + signPort := os.Getenv("SIGN_PORT") + if extPort != "" || signPort != "" { + msg := fmt.Sprintf("EXTENSION_PORT=%s, SIGN_PORT=%s", extPort, signPort) + r.Add(CheckResult{ + Step: "services", + ID: "S7", + Name: "port configuration", + Status: PASS, + Message: msg, + }) + } else { + r.Add(CheckResult{ + Step: "services", + ID: "S7", + Name: "port configuration", + Status: PASS, + Message: "using defaults (standalone: 8080/9090, Docker: 7702/7701)", + }) + } + + // S9: EXT_PROXY_URL reachable. + proxyURL := os.Getenv("EXT_PROXY_URL") + if proxyURL == "" { + r.Add(CheckResult{ + Step: "services", + ID: "S9", + Name: "EXT_PROXY_URL reachable", + Status: SKIP, + Message: "EXT_PROXY_URL not set", + }) + } else { + httpClient := &http.Client{Timeout: 5 * time.Second} + resp, err := httpClient.Get(proxyURL + "/info") + if err == nil { + resp.Body.Close() + } + if err == nil && resp.StatusCode == http.StatusOK { + r.Add(CheckResult{ + Step: "services", + ID: "S9", + Name: "EXT_PROXY_URL reachable", + Status: PASS, + Message: fmt.Sprintf("proxy reachable at %s", proxyURL), + }) + } else { + r.Add(CheckResult{ + Step: "services", + ID: "S9", + Name: "EXT_PROXY_URL reachable", + Status: WARN, + Message: fmt.Sprintf("proxy not reachable at %s — is the proxy running?", proxyURL), + Fix: "Start the proxy or check EXT_PROXY_URL", + }) + } + } + + // S10: PROXY_PRIVATE_KEY set on non-local. + localMode := os.Getenv("LOCAL_MODE") + privateKey := os.Getenv("PROXY_PRIVATE_KEY") + if localMode == "true" || localMode == "" { + r.Add(CheckResult{ + Step: "services", + ID: "S10", + Name: "PROXY_PRIVATE_KEY set on non-local", + Status: PASS, + Message: "local mode, dev key OK", + }) + } else if localMode == "false" && privateKey == "" { + r.Add(CheckResult{ + Step: "services", + ID: "S10", + Name: "PROXY_PRIVATE_KEY set on non-local", + Status: WARN, + Message: "PROXY_PRIVATE_KEY not set in non-local mode — proxy will use default key", + Fix: "Set PROXY_PRIVATE_KEY in .env for non-local deployments", + }) + } else { + r.Add(CheckResult{ + Step: "services", + ID: "S10", + Name: "PROXY_PRIVATE_KEY set on non-local", + Status: PASS, + Message: "PROXY_PRIVATE_KEY is configured", + }) + } +} + +// RegisterTeeVersionChecks adds TEE version registration checks (V2, V4, V6) to a Report. +func RegisterTeeVersionChecks(r *Report, extensionEnvPath string) { + // V2: Extension owner key. + extOwnerKey := os.Getenv("EXTENSION_OWNER_KEY") + privKey := os.Getenv("DEPLOYMENT_PRIVATE_KEY") + localMode := os.Getenv("LOCAL_MODE") + + if extOwnerKey != "" { + r.Add(CheckResult{ + Step: "tee-version", + ID: "V2", + Name: "extension owner key", + Status: PASS, + Message: "EXTENSION_OWNER_KEY is configured", + }) + } else if privKey != "" { + r.Add(CheckResult{ + Step: "tee-version", + ID: "V2", + Name: "extension owner key", + Status: PASS, + Message: "using DEPLOYMENT_PRIVATE_KEY (EXTENSION_OWNER_KEY not set)", + }) + } else if localMode != "true" && localMode != "" { + r.Add(CheckResult{ + Step: "tee-version", + ID: "V2", + Name: "extension owner key", + Status: WARN, + Message: "neither EXTENSION_OWNER_KEY nor DEPLOYMENT_PRIVATE_KEY set — AddTeeVersion will use dev key which isn't the extension owner on Coston2", + Fix: "Set EXTENSION_OWNER_KEY or DEPLOYMENT_PRIVATE_KEY in .env", + }) + } else { + r.Add(CheckResult{ + Step: "tee-version", + ID: "V2", + Name: "extension owner key", + Status: PASS, + Message: "using dev key (local mode)", + }) + } + + // V4: Extension ID valid. + if extensionEnvPath == "" { + r.Add(CheckResult{ + Step: "tee-version", + ID: "V4", + Name: "EXTENSION_ID valid", + Status: SKIP, + Message: "no --config provided", + }) + } else { + extID, _, err := parseExtensionEnv(extensionEnvPath) + if err != nil { + r.Add(CheckResult{ + Step: "tee-version", + ID: "V4", + Name: "EXTENSION_ID valid", + Status: FAIL, + Message: fmt.Sprintf("failed to parse extension.env: %v", err), + }) + } else if extID == "" { + // parseExtensionEnv returns empty strings and nil error if file doesn't exist + if _, statErr := os.Stat(extensionEnvPath); os.IsNotExist(statErr) { + r.Add(CheckResult{ + Step: "tee-version", + ID: "V4", + Name: "EXTENSION_ID valid", + Status: SKIP, + Message: "extension.env not found — run pre-build.sh first", + }) + } else { + r.Add(CheckResult{ + Step: "tee-version", + ID: "V4", + Name: "EXTENSION_ID valid", + Status: FAIL, + Message: "EXTENSION_ID not set", + Fix: "Delete config/extension.env and re-run scripts/pre-build.sh", + }) + } + } else if !extensionIDRegex.MatchString(extID) { + r.Add(CheckResult{ + Step: "tee-version", + ID: "V4", + Name: "EXTENSION_ID valid", + Status: FAIL, + Message: "EXTENSION_ID malformed", + Fix: "Delete config/extension.env and re-run scripts/pre-build.sh", + }) + } else { + r.Add(CheckResult{ + Step: "tee-version", + ID: "V4", + Name: "EXTENSION_ID valid", + Status: PASS, + Message: extID, + }) + } + } + + // V6: Proxy reachable. + proxyURL := os.Getenv("EXT_PROXY_URL") + if proxyURL == "" { + r.Add(CheckResult{ + Step: "tee-version", + ID: "V6", + Name: "proxy reachable", + Status: SKIP, + Message: "EXT_PROXY_URL not set — allow-tee-version uses -p flag", + }) + } else { + httpClient := &http.Client{Timeout: 5 * time.Second} + resp, err := httpClient.Get(proxyURL + "/info") + if err == nil { + resp.Body.Close() + } + if err == nil && resp.StatusCode == http.StatusOK { + r.Add(CheckResult{ + Step: "tee-version", + ID: "V6", + Name: "proxy reachable", + Status: PASS, + Message: fmt.Sprintf("proxy reachable at %s", proxyURL), + }) + } else { + r.Add(CheckResult{ + Step: "tee-version", + ID: "V6", + Name: "proxy reachable", + Status: WARN, + Message: fmt.Sprintf("proxy not reachable at %s — start services first", proxyURL), + Fix: "Start the proxy or check EXT_PROXY_URL", + }) + } + } +} + +// RegisterTeeMachineChecks adds TEE machine registration checks (T1, T5, T10) to a Report. +func RegisterTeeMachineChecks(r *Report, extensionEnvPath string) { + // T1: SIMULATED_TEE mode. + simulatedTee := os.Getenv("SIMULATED_TEE") + localMode := os.Getenv("LOCAL_MODE") + + if simulatedTee == "true" && localMode == "false" { + r.Add(CheckResult{ + Step: "tee-machine", + ID: "T1", + Name: "SIMULATED_TEE mode", + Status: WARN, + Message: "SIMULATED_TEE=true on non-local deployment — machine will be registered with test attestation values. Set SIMULATED_TEE=false for real TEE hardware", + Fix: "Set SIMULATED_TEE=false in .env when deploying to a real GCP TEE", + }) + } else if simulatedTee == "true" { + r.Add(CheckResult{ + Step: "tee-machine", + ID: "T1", + Name: "SIMULATED_TEE mode", + Status: PASS, + Message: "simulated TEE mode (local)", + }) + } else { + r.Add(CheckResult{ + Step: "tee-machine", + ID: "T1", + Name: "SIMULATED_TEE mode", + Status: PASS, + Message: "real attestation mode", + }) + } + + // T5: NORMAL_PROXY_URL reachable. + normalProxyURL := os.Getenv("NORMAL_PROXY_URL") + if normalProxyURL == "" { + r.Add(CheckResult{ + Step: "tee-machine", + ID: "T5", + Name: "NORMAL_PROXY_URL reachable", + Status: SKIP, + Message: "NORMAL_PROXY_URL not set", + }) + } else { + httpClient := &http.Client{Timeout: 5 * time.Second} + resp, err := httpClient.Get(normalProxyURL + "/info") + if err == nil { + resp.Body.Close() + } + if err == nil && resp.StatusCode == http.StatusOK { + r.Add(CheckResult{ + Step: "tee-machine", + ID: "T5", + Name: "NORMAL_PROXY_URL reachable", + Status: PASS, + Message: fmt.Sprintf("normal proxy reachable at %s", normalProxyURL), + }) + } else { + r.Add(CheckResult{ + Step: "tee-machine", + ID: "T5", + Name: "NORMAL_PROXY_URL reachable", + Status: WARN, + Message: fmt.Sprintf("normal proxy not reachable at %s — required for FTDC availability check", normalProxyURL), + Fix: "Ensure the normal/FTDC proxy is running and NORMAL_PROXY_URL is correct in .env", + }) + } + } + + // T10: Host URL reachable. + extProxyURL := os.Getenv("EXT_PROXY_URL") + if extProxyURL == "" { + r.Add(CheckResult{ + Step: "tee-machine", + ID: "T10", + Name: "host URL reachable", + Status: SKIP, + Message: "EXT_PROXY_URL not set", + }) + } else { + httpClient := &http.Client{Timeout: 5 * time.Second} + resp, err := httpClient.Get(extProxyURL + "/info") + if err == nil { + resp.Body.Close() + } + if err == nil && resp.StatusCode == http.StatusOK { + r.Add(CheckResult{ + Step: "tee-machine", + ID: "T10", + Name: "host URL reachable", + Status: PASS, + Message: fmt.Sprintf("host URL reachable at %s", extProxyURL), + }) + } else { + r.Add(CheckResult{ + Step: "tee-machine", + ID: "T10", + Name: "host URL reachable", + Status: WARN, + Message: fmt.Sprintf("host URL not reachable at %s — machine will be registered with unreachable host. Data providers can't relay instructions", extProxyURL), + Fix: "Ensure the host is accessible and EXT_PROXY_URL or -h flag points to the right URL", + }) + } + } +} + +// RegisterTestChecks adds testing-readiness checks (E1, E3, E6) to a Report. +func RegisterTestChecks(r *Report, extensionEnvPath string) { + // E1: INSTRUCTION_SENDER valid. + if extensionEnvPath == "" { + r.Add(CheckResult{ + Step: "test", + ID: "E1", + Name: "INSTRUCTION_SENDER valid", + Status: SKIP, + Message: "no --config provided", + }) + } else { + _, instrSender, _ := parseExtensionEnv(extensionEnvPath) + if _, statErr := os.Stat(extensionEnvPath); os.IsNotExist(statErr) { + r.Add(CheckResult{ + Step: "test", + ID: "E1", + Name: "INSTRUCTION_SENDER valid", + Status: SKIP, + Message: "extension.env not found — run pre-build.sh first", + }) + } else if instrSender == "" { + r.Add(CheckResult{ + Step: "test", + ID: "E1", + Name: "INSTRUCTION_SENDER valid", + Status: FAIL, + Message: "INSTRUCTION_SENDER not set — run pre-build.sh", + Fix: "Run scripts/pre-build.sh to deploy and register the extension", + }) + } else if !addressRegex.MatchString(instrSender) { + r.Add(CheckResult{ + Step: "test", + ID: "E1", + Name: "INSTRUCTION_SENDER valid", + Status: FAIL, + Message: "INSTRUCTION_SENDER malformed", + Fix: "Delete config/extension.env and re-run scripts/pre-build.sh", + }) + } else { + r.Add(CheckResult{ + Step: "test", + ID: "E1", + Name: "INSTRUCTION_SENDER valid", + Status: PASS, + Message: instrSender, + }) + } + } + + // E3: Proxy reachable for test results. + proxyURL := os.Getenv("EXT_PROXY_URL") + if proxyURL == "" { + r.Add(CheckResult{ + Step: "test", + ID: "E3", + Name: "proxy reachable for test results", + Status: SKIP, + Message: "EXT_PROXY_URL not set", + }) + } else { + httpClient := &http.Client{Timeout: 5 * time.Second} + resp, err := httpClient.Get(proxyURL + "/info") + if err == nil { + resp.Body.Close() + } + if err == nil && resp.StatusCode == http.StatusOK { + r.Add(CheckResult{ + Step: "test", + ID: "E3", + Name: "proxy reachable for test results", + Status: PASS, + Message: "proxy reachable for test result polling", + }) + } else { + r.Add(CheckResult{ + Step: "test", + ID: "E3", + Name: "proxy reachable for test results", + Status: WARN, + Message: "proxy not reachable — test results cannot be polled", + Fix: "Start the proxy or check EXT_PROXY_URL", + }) + } + } + + // E6: OPType/OPCommand alignment info. + r.Add(CheckResult{ + Step: "test", + ID: "E6", + Name: "OPType/OPCommand alignment info", + Status: PASS, + Message: "OPType/OPCommand hashes are validated at runtime — if a 501 error occurs, check the hash values in the error message", + }) +} + +// RegisterStubChecks adds one SKIP result as a placeholder for unimplemented checks. +func RegisterStubChecks(r *Report, step, idRange, name string) { + r.Add(CheckResult{ + Step: step, + ID: idRange, + Name: name, + Status: SKIP, + Message: "not yet implemented", + }) +} diff --git a/tools/pkg/validate/checks_test.go b/tools/pkg/validate/checks_test.go new file mode 100644 index 0000000..e4e3a4a --- /dev/null +++ b/tools/pkg/validate/checks_test.go @@ -0,0 +1,488 @@ +package validate + +import ( + "os" + "path/filepath" + "testing" +) + +func TestCheckExtensionEnvFormat_Valid(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "extension.env") + content := `# This is a comment +INSTRUCTION_SENDER=0x1234567890abcdef1234567890abcdef12345678 +EXTENSION_ID=0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef +` + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatal(err) + } + + results := CheckExtensionEnvFormat(path) + + for _, res := range results { + if res.Status == FAIL { + t.Errorf("unexpected FAIL: %s — %s", res.Name, res.Message) + } + } + + if len(results) != 2 { + t.Fatalf("expected 2 results, got %d", len(results)) + } + + // Both should be PASS. + for _, res := range results { + if res.Status != PASS { + t.Errorf("expected PASS for %s, got %s: %s", res.Name, res.Status, res.Message) + } + if res.Step != "deploy" { + t.Errorf("expected step=deploy, got %s", res.Step) + } + if res.ID != "D7" { + t.Errorf("expected ID=D7, got %s", res.ID) + } + } +} + +func TestCheckExtensionEnvFormat_Malformed(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "extension.env") + content := `INSTRUCTION_SENDER=not-an-address +EXTENSION_ID=0xTOOSHORT +` + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatal(err) + } + + results := CheckExtensionEnvFormat(path) + + if len(results) != 2 { + t.Fatalf("expected 2 results, got %d", len(results)) + } + + failCount := 0 + for _, res := range results { + if res.Status == FAIL { + failCount++ + } + } + + if failCount != 2 { + t.Errorf("expected 2 FAILs, got %d", failCount) + } +} + +func TestCheckExtensionEnvFormat_Missing(t *testing.T) { + results := CheckExtensionEnvFormat("/nonexistent/path/extension.env") + + if len(results) != 1 { + t.Fatalf("expected 1 result, got %d", len(results)) + } + + if results[0].Status != SKIP { + t.Errorf("expected SKIP, got %s", results[0].Status) + } + + if results[0].Step != "deploy" { + t.Errorf("expected step=deploy, got %s", results[0].Step) + } + + if results[0].ID != "D7" { + t.Errorf("expected ID=D7, got %s", results[0].ID) + } +} + +func TestCheckDeployerKeySource_DevKey(t *testing.T) { + t.Setenv("DEPLOYMENT_PRIVATE_KEY", "") + t.Setenv("LOCAL_MODE", "false") + + result := CheckDeployerKeySource() + + if result.Status != WARN { + t.Errorf("expected WARN, got %s: %s", result.Status, result.Message) + } + + if result.Step != "deploy" { + t.Errorf("expected step=deploy, got %s", result.Step) + } + + if result.ID != "D5" { + t.Errorf("expected ID=D5, got %s", result.ID) + } + + if result.Fix == "" { + t.Error("expected a Fix message, got empty") + } +} + +func TestCheckDeployerKeySource_RealKey(t *testing.T) { + t.Setenv("DEPLOYMENT_PRIVATE_KEY", "abc") + + result := CheckDeployerKeySource() + + if result.Status != PASS { + t.Errorf("expected PASS, got %s: %s", result.Status, result.Message) + } + + if result.Step != "deploy" { + t.Errorf("expected step=deploy, got %s", result.Step) + } + + if result.ID != "D5" { + t.Errorf("expected ID=D5, got %s", result.ID) + } +} + +func TestCheckDeployerKeySource_DevKeyLocal(t *testing.T) { + t.Setenv("DEPLOYMENT_PRIVATE_KEY", "") + t.Setenv("LOCAL_MODE", "true") + + result := CheckDeployerKeySource() + + if result.Status != PASS { + t.Errorf("expected PASS, got %s: %s", result.Status, result.Message) + } + + if result.Step != "deploy" { + t.Errorf("expected step=deploy, got %s", result.Step) + } + + if result.ID != "D5" { + t.Errorf("expected ID=D5, got %s", result.ID) + } +} + +func TestParseExtensionEnv_Valid(t *testing.T) { + dir := t.TempDir() + envFile := filepath.Join(dir, "extension.env") + os.WriteFile(envFile, []byte( + "# Auto-generated\n"+ + "EXTENSION_ID=0x00000000000000000000000000000000000000000000000000000000000000db\n"+ + "INSTRUCTION_SENDER=0x32F967bE8F35F73274Bd3d4130073547361A0d75\n", + ), 0644) + + extID, instrSender, err := parseExtensionEnv(envFile) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if extID != "0x00000000000000000000000000000000000000000000000000000000000000db" { + t.Fatalf("unexpected EXTENSION_ID: %s", extID) + } + if instrSender != "0x32F967bE8F35F73274Bd3d4130073547361A0d75" { + t.Fatalf("unexpected INSTRUCTION_SENDER: %s", instrSender) + } +} + +func TestParseExtensionEnv_Missing(t *testing.T) { + extID, instrSender, err := parseExtensionEnv("/nonexistent/extension.env") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if extID != "" || instrSender != "" { + t.Fatalf("expected empty strings for missing file, got %q and %q", extID, instrSender) + } +} + +func TestRegisterServicesChecks_NoConfig(t *testing.T) { + r := &Report{} + RegisterServicesChecks(r, "") + // Should have at least one SKIP + hasSkip := false + for _, res := range r.Results { + if res.Status == SKIP { + hasSkip = true + } + } + if !hasSkip { + t.Fatal("expected at least one SKIP result for empty config path") + } +} + +func TestRegisterServicesChecks_WithValidConfig(t *testing.T) { + dir := t.TempDir() + envFile := filepath.Join(dir, "extension.env") + os.WriteFile(envFile, []byte( + "EXTENSION_ID=0x00000000000000000000000000000000000000000000000000000000000000db\n"+ + "INSTRUCTION_SENDER=0x32F967bE8F35F73274Bd3d4130073547361A0d75\n", + ), 0644) + + r := &Report{} + RegisterServicesChecks(r, envFile) + // Should have S2 PASS for extension ID format + found := false + for _, res := range r.Results { + if res.ID == "S2" && res.Status == PASS { + found = true + } + } + if !found { + t.Fatal("expected S2 PASS for valid extension ID") + } +} + +func TestRegisterRegistrationChecks_NoConfig(t *testing.T) { + r := &Report{} + RegisterRegistrationChecks(r, nil, nil, nil, nil, "") + if len(r.Results) != 1 { + t.Fatalf("expected 1 result, got %d", len(r.Results)) + } + if r.Results[0].Status != SKIP { + t.Fatalf("expected SKIP, got %s", r.Results[0].Status) + } +} + +func TestRegisterRegistrationChecks_MissingFile(t *testing.T) { + r := &Report{} + RegisterRegistrationChecks(r, nil, nil, nil, nil, "/nonexistent/extension.env") + if len(r.Results) != 1 { + t.Fatalf("expected 1 result, got %d", len(r.Results)) + } + if r.Results[0].Status != SKIP { + t.Fatalf("expected SKIP, got %s", r.Results[0].Status) + } +} + +func TestRegisterTeeMachineChecks_NoConfig(t *testing.T) { + r := &Report{} + RegisterTeeMachineChecks(r, "") + if len(r.Results) < 1 { + t.Fatalf("expected at least 1 result, got %d", len(r.Results)) + } +} + +func TestRegisterTeeMachineChecks_SimulatedTeeWarning(t *testing.T) { + t.Setenv("SIMULATED_TEE", "true") + t.Setenv("LOCAL_MODE", "false") + r := &Report{} + RegisterTeeMachineChecks(r, "") + foundWarn := false + for _, res := range r.Results { + if res.ID == "T1" && res.Status == WARN { + foundWarn = true + } + } + if !foundWarn { + t.Fatal("expected T1 WARN when SIMULATED_TEE=true and LOCAL_MODE=false") + } +} + +func TestRegisterTeeVersionChecks_NoConfig(t *testing.T) { + r := &Report{} + RegisterTeeVersionChecks(r, "") + // Should have results for V2, and SKIP for V4 + if len(r.Results) < 2 { + t.Fatalf("expected at least 2 results, got %d", len(r.Results)) + } +} + +func TestRegisterTestChecks_NoConfig(t *testing.T) { + r := &Report{} + RegisterTestChecks(r, "") + hasSkip := false + for _, res := range r.Results { + if res.Status == SKIP { + hasSkip = true + } + } + if !hasSkip { + t.Fatal("expected at least one SKIP for empty config") + } +} + +func TestRegisterTestChecks_WithValidConfig(t *testing.T) { + dir := t.TempDir() + envFile := filepath.Join(dir, "extension.env") + os.WriteFile(envFile, []byte( + "EXTENSION_ID=0x00000000000000000000000000000000000000000000000000000000000000db\n"+ + "INSTRUCTION_SENDER=0x32F967bE8F35F73274Bd3d4130073547361A0d75\n", + ), 0644) + + r := &Report{} + RegisterTestChecks(r, envFile) + foundE1 := false + for _, res := range r.Results { + if res.ID == "E1" && res.Status == PASS { + foundE1 = true + } + } + if !foundE1 { + t.Fatal("expected E1 PASS for valid INSTRUCTION_SENDER") + } +} + +func TestRegisterTeeVersionChecks_WithConfig(t *testing.T) { + dir := t.TempDir() + envFile := filepath.Join(dir, "extension.env") + os.WriteFile(envFile, []byte( + "EXTENSION_ID=0x00000000000000000000000000000000000000000000000000000000000000db\n"+ + "INSTRUCTION_SENDER=0x32F967bE8F35F73274Bd3d4130073547361A0d75\n", + ), 0644) + + t.Setenv("DEPLOYMENT_PRIVATE_KEY", "abc123") + r := &Report{} + RegisterTeeVersionChecks(r, envFile) + // Should have V2 PASS and V4 PASS at minimum + foundV2 := false + foundV4 := false + for _, res := range r.Results { + if res.ID == "V2" && res.Status == PASS { + foundV2 = true + } + if res.ID == "V4" && res.Status == PASS { + foundV4 = true + } + } + if !foundV2 { + t.Fatal("expected V2 PASS") + } + if !foundV4 { + t.Fatal("expected V4 PASS") + } +} + +// --- Additional env parsing edge case tests --- + +func TestParseExtensionEnv_CommentsOnly(t *testing.T) { + dir := t.TempDir() + envFile := filepath.Join(dir, "extension.env") + os.WriteFile(envFile, []byte( + "# This is a comment\n"+ + "# Another comment\n"+ + "\n"+ + " \n", + ), 0644) + + extID, instrSender, err := parseExtensionEnv(envFile) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if extID != "" { + t.Errorf("expected empty EXTENSION_ID, got %q", extID) + } + if instrSender != "" { + t.Errorf("expected empty INSTRUCTION_SENDER, got %q", instrSender) + } +} + +func TestParseExtensionEnv_ExtraKeys(t *testing.T) { + dir := t.TempDir() + envFile := filepath.Join(dir, "extension.env") + os.WriteFile(envFile, []byte( + "EXTENSION_ID=0x00000000000000000000000000000000000000000000000000000000000000db\n"+ + "INSTRUCTION_SENDER=0x32F967bE8F35F73274Bd3d4130073547361A0d75\n"+ + "EXTRA_KEY=some_value\n"+ + "ANOTHER_KEY=another_value\n", + ), 0644) + + extID, instrSender, err := parseExtensionEnv(envFile) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if extID != "0x00000000000000000000000000000000000000000000000000000000000000db" { + t.Fatalf("unexpected EXTENSION_ID: %s", extID) + } + if instrSender != "0x32F967bE8F35F73274Bd3d4130073547361A0d75" { + t.Fatalf("unexpected INSTRUCTION_SENDER: %s", instrSender) + } +} + +func TestParseExtensionEnv_NoEquals(t *testing.T) { + dir := t.TempDir() + envFile := filepath.Join(dir, "extension.env") + os.WriteFile(envFile, []byte( + "EXTENSION_ID_NO_EQUALS\n"+ + "INSTRUCTION_SENDER=0x32F967bE8F35F73274Bd3d4130073547361A0d75\n", + ), 0644) + + extID, instrSender, err := parseExtensionEnv(envFile) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if extID != "" { + t.Errorf("expected empty EXTENSION_ID for line without equals, got %q", extID) + } + if instrSender != "0x32F967bE8F35F73274Bd3d4130073547361A0d75" { + t.Errorf("unexpected INSTRUCTION_SENDER: %s", instrSender) + } +} + +func TestCheckExtensionEnvFormat_LowercaseHex(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "extension.env") + content := `INSTRUCTION_SENDER=0xabcdef1234567890abcdef1234567890abcdef12 +EXTENSION_ID=0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 +` + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatal(err) + } + + results := CheckExtensionEnvFormat(path) + for _, res := range results { + if res.Status == FAIL { + t.Errorf("unexpected FAIL for lowercase hex: %s — %s", res.Name, res.Message) + } + } +} + +func TestCheckExtensionEnvFormat_MixedCaseChecksum(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "extension.env") + // EIP-55 checksum address format + content := `INSTRUCTION_SENDER=0x32F967bE8F35F73274Bd3d4130073547361A0d75 +EXTENSION_ID=0x00000000000000000000000000000000000000000000000000000000000000Db +` + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatal(err) + } + + results := CheckExtensionEnvFormat(path) + for _, res := range results { + if res.Status == FAIL { + t.Errorf("unexpected FAIL for mixed-case (EIP-55) address: %s — %s", res.Name, res.Message) + } + } +} + +func TestCheckExtensionEnvFormat_EmptyValues(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "extension.env") + content := `INSTRUCTION_SENDER= +EXTENSION_ID= +` + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatal(err) + } + + results := CheckExtensionEnvFormat(path) + failCount := 0 + for _, res := range results { + if res.Status == FAIL { + failCount++ + } + } + if failCount != 2 { + t.Errorf("expected 2 FAILs for empty values, got %d", failCount) + } +} + +func TestCheckExtensionEnvFormat_NoPrefix(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "extension.env") + // Missing 0x prefix + content := `INSTRUCTION_SENDER=1234567890abcdef1234567890abcdef12345678 +EXTENSION_ID=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef +` + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatal(err) + } + + results := CheckExtensionEnvFormat(path) + failCount := 0 + for _, res := range results { + if res.Status == FAIL { + failCount++ + } + } + if failCount != 2 { + t.Errorf("expected 2 FAILs for missing 0x prefix, got %d", failCount) + } +} diff --git a/tools/pkg/validate/report.go b/tools/pkg/validate/report.go new file mode 100644 index 0000000..9f16d94 --- /dev/null +++ b/tools/pkg/validate/report.go @@ -0,0 +1,211 @@ +package validate + +import ( + "encoding/json" + "fmt" + "io" + "os" + "strings" +) + +// Status represents the outcome of a single verification check. +type Status int + +const ( + PASS Status = iota + WARN + FAIL + SKIP +) + +// String returns the human-readable label for a Status. +func (s Status) String() string { + switch s { + case PASS: + return "PASS" + case WARN: + return "WARN" + case FAIL: + return "FAIL" + case SKIP: + return "SKIP" + default: + return "UNKNOWN" + } +} + +// CheckResult captures the outcome of a single verification check. +type CheckResult struct { + Step string // "deploy", "register", "services", "tee-version", "tee-machine", "test" + ID string // "D1", "D2", etc. + Name string // human-readable check name + Status Status + Message string // what was found + Fix string // how to fix (only on FAIL/WARN) +} + +// Report collects the results of all verification checks. +type Report struct { + Results []CheckResult +} + +// Add appends a CheckResult to the report. +func (r *Report) Add(result CheckResult) { + r.Results = append(r.Results, result) +} + +// HasFailures returns true if any result has FAIL status. +func (r *Report) HasFailures() bool { + for _, res := range r.Results { + if res.Status == FAIL { + return true + } + } + return false +} + +// Summary returns a string like "N passed, N warning, N failed, N skipped". +func (r *Report) Summary() string { + var passed, warning, failed, skipped int + for _, res := range r.Results { + switch res.Status { + case PASS: + passed++ + case WARN: + warning++ + case FAIL: + failed++ + case SKIP: + skipped++ + } + } + return fmt.Sprintf("%d passed, %d warning, %d failed, %d skipped", + passed, warning, failed, skipped) +} + +// ANSI color codes for terminal output. +const ( + colorReset = "\033[0m" + colorRed = "\033[31m" + colorGreen = "\033[32m" + colorYellow = "\033[33m" + colorCyan = "\033[36m" + colorGray = "\033[90m" +) + +// stepLabel maps step identifiers to human-readable labels. +var stepLabel = map[string]string{ + "deploy": "Contract Deployment", + "register": "Extension Registration", + "services": "Service Startup", + "tee-version": "TEE Version Registration", + "tee-machine": "TEE Machine Registration", + "test": "Testing", +} + +// statusColor returns the ANSI color code for a given status. +func statusColor(s Status) string { + switch s { + case PASS: + return colorGreen + case WARN: + return colorYellow + case FAIL: + return colorRed + case SKIP: + return colorGray + default: + return colorReset + } +} + +// Fprint writes a colored terminal report to w, grouped by Step. +func (r *Report) Fprint(w io.Writer) { + fmt.Fprintf(w, "\n%s=== Deployment Verification Report ===%s\n\n", colorCyan, colorReset) + + // Collect unique steps in order of first appearance. + var steps []string + seen := make(map[string]bool) + for _, res := range r.Results { + if !seen[res.Step] { + seen[res.Step] = true + steps = append(steps, res.Step) + } + } + + for _, step := range steps { + label := stepLabel[step] + if label == "" { + label = step + } + fmt.Fprintf(w, "%s:\n", label) + + for _, res := range r.Results { + if res.Step != step { + continue + } + color := statusColor(res.Status) + fmt.Fprintf(w, " %s[%s]%s %s %s\n", color, res.Status, colorReset, res.ID, res.Name) + + if res.Status == FAIL || res.Status == WARN { + if res.Message != "" { + fmt.Fprintf(w, " %s\n", res.Message) + } + if res.Fix != "" { + fmt.Fprintf(w, " Fix: %s\n", res.Fix) + } + } + } + fmt.Fprintln(w) + } + + fmt.Fprintf(w, "%s\n", r.Summary()) +} + +// Print writes the colored report to stdout. +func (r *Report) Print() { + r.Fprint(os.Stdout) +} + +// jsonResult is the JSON-serializable form of a CheckResult. +type jsonResult struct { + Step string `json:"step"` + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Message string `json:"message,omitempty"` + Fix string `json:"fix,omitempty"` +} + +// jsonReport is the top-level JSON output structure. +type jsonReport struct { + Results []jsonResult `json:"results"` + Summary string `json:"summary"` +} + +// FprintJSON writes the report as pretty-printed JSON to w. +func (r *Report) FprintJSON(w io.Writer) { + jr := jsonReport{ + Results: make([]jsonResult, len(r.Results)), + Summary: r.Summary(), + } + for i, res := range r.Results { + jr.Results[i] = jsonResult{ + Step: res.Step, + ID: res.ID, + Name: res.Name, + Status: res.Status.String(), + Message: strings.TrimSpace(res.Message), + Fix: strings.TrimSpace(res.Fix), + } + } + + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + enc.Encode(jr) +} + +// PrintJSON writes the JSON report to stdout. +func (r *Report) PrintJSON() { + r.FprintJSON(os.Stdout) +} diff --git a/tools/pkg/validate/report_test.go b/tools/pkg/validate/report_test.go new file mode 100644 index 0000000..06805be --- /dev/null +++ b/tools/pkg/validate/report_test.go @@ -0,0 +1,237 @@ +package validate + +import ( + "bytes" + "encoding/json" + "strings" + "testing" +) + +func TestReport_Summary(t *testing.T) { + var r Report + r.Add(CheckResult{Step: "deploy", ID: "D1", Name: "Contract deployed", Status: PASS}) + r.Add(CheckResult{Step: "deploy", ID: "D2", Name: "Balance check", Status: WARN, Message: "Low balance", Fix: "Add funds"}) + r.Add(CheckResult{Step: "services", ID: "S1", Name: "Service skipped", Status: SKIP}) + + want := "1 passed, 1 warning, 0 failed, 1 skipped" + got := r.Summary() + if got != want { + t.Errorf("Summary() = %q, want %q", got, want) + } +} + +func TestReport_HasFailures(t *testing.T) { + var r Report + r.Add(CheckResult{Step: "deploy", ID: "D1", Name: "OK check", Status: PASS}) + r.Add(CheckResult{Step: "deploy", ID: "D2", Name: "Warn check", Status: WARN}) + + if r.HasFailures() { + t.Error("HasFailures() = true, want false (no FAIL results)") + } + + r.Add(CheckResult{Step: "register", ID: "R1", Name: "Bad check", Status: FAIL, Message: "broken", Fix: "fix it"}) + + if !r.HasFailures() { + t.Error("HasFailures() = false, want true (FAIL result added)") + } +} + +func TestReport_PrintJSON(t *testing.T) { + var r Report + r.Add(CheckResult{Step: "deploy", ID: "D1", Name: "Contract deployed", Status: PASS, Message: "found code"}) + r.Add(CheckResult{Step: "register", ID: "R1", Name: "Extension registered", Status: FAIL, Message: "not registered", Fix: "run register cmd"}) + + var buf bytes.Buffer + r.FprintJSON(&buf) + + var parsed struct { + Results []struct { + Step string `json:"step"` + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Message string `json:"message"` + Fix string `json:"fix"` + } `json:"results"` + Summary string `json:"summary"` + } + + if err := json.Unmarshal(buf.Bytes(), &parsed); err != nil { + t.Fatalf("failed to unmarshal JSON output: %v\nraw: %s", err, buf.String()) + } + + if len(parsed.Results) != 2 { + t.Fatalf("expected 2 results, got %d", len(parsed.Results)) + } + + // Status must be the string "PASS", not a number. + if parsed.Results[0].Status != "PASS" { + t.Errorf("results[0].status = %q, want \"PASS\"", parsed.Results[0].Status) + } + if parsed.Results[1].Status != "FAIL" { + t.Errorf("results[1].status = %q, want \"FAIL\"", parsed.Results[1].Status) + } + + // Verify fields. + if parsed.Results[0].Step != "deploy" { + t.Errorf("results[0].step = %q, want \"deploy\"", parsed.Results[0].Step) + } + if parsed.Results[1].Fix != "run register cmd" { + t.Errorf("results[1].fix = %q, want \"run register cmd\"", parsed.Results[1].Fix) + } + + // Summary should be present. + if !strings.Contains(parsed.Summary, "passed") { + t.Errorf("summary missing 'passed': %q", parsed.Summary) + } +} + +func TestStatus_String(t *testing.T) { + tests := []struct { + s Status + want string + }{ + {PASS, "PASS"}, + {WARN, "WARN"}, + {FAIL, "FAIL"}, + {SKIP, "SKIP"}, + } + for _, tt := range tests { + if got := tt.s.String(); got != tt.want { + t.Errorf("Status(%d).String() = %q, want %q", int(tt.s), got, tt.want) + } + } +} + +func TestReport_EmptyReport(t *testing.T) { + var r Report + + want := "0 passed, 0 warning, 0 failed, 0 skipped" + got := r.Summary() + if got != want { + t.Errorf("Summary() = %q, want %q", got, want) + } + + if r.HasFailures() { + t.Error("HasFailures() = true for empty report, want false") + } + + // JSON output should be valid with empty results array + var buf bytes.Buffer + r.FprintJSON(&buf) + var parsed struct { + Results []interface{} `json:"results"` + Summary string `json:"summary"` + } + if err := json.Unmarshal(buf.Bytes(), &parsed); err != nil { + t.Fatalf("failed to parse empty report JSON: %v", err) + } + if len(parsed.Results) != 0 { + t.Errorf("expected 0 results in JSON, got %d", len(parsed.Results)) + } + if parsed.Summary != want { + t.Errorf("JSON summary = %q, want %q", parsed.Summary, want) + } +} + +func TestReport_AllStatuses(t *testing.T) { + var r Report + r.Add(CheckResult{Step: "deploy", ID: "D1", Name: "Pass check", Status: PASS}) + r.Add(CheckResult{Step: "deploy", ID: "D2", Name: "Warn check", Status: WARN, Message: "warning"}) + r.Add(CheckResult{Step: "register", ID: "R1", Name: "Fail check", Status: FAIL, Message: "failed", Fix: "fix it"}) + r.Add(CheckResult{Step: "services", ID: "S1", Name: "Skip check", Status: SKIP}) + + if !r.HasFailures() { + t.Error("HasFailures() = false, want true (has FAIL)") + } + + want := "1 passed, 1 warning, 1 failed, 1 skipped" + got := r.Summary() + if got != want { + t.Errorf("Summary() = %q, want %q", got, want) + } +} + +func TestStatus_String_Unknown(t *testing.T) { + unknown := Status(99) + if got := unknown.String(); got != "UNKNOWN" { + t.Errorf("Status(99).String() = %q, want %q", got, "UNKNOWN") + } +} + +func TestReport_FprintJSON_OmitsEmptyFields(t *testing.T) { + var r Report + r.Add(CheckResult{Step: "deploy", ID: "D1", Name: "OK", Status: PASS}) + + var buf bytes.Buffer + r.FprintJSON(&buf) + + // The JSON output should NOT contain "fix" or "message" keys for PASS results + // because they use omitempty + raw := buf.String() + if strings.Contains(raw, `"fix"`) { + t.Error("JSON contains 'fix' field for PASS result — should be omitted") + } + if strings.Contains(raw, `"message"`) { + t.Error("JSON contains 'message' field for PASS result — should be omitted") + } +} + +func TestReport_Fprint_UnknownStep(t *testing.T) { + var r Report + r.Add(CheckResult{Step: "unknown-step", ID: "X1", Name: "Custom check", Status: PASS}) + + var buf bytes.Buffer + r.Fprint(&buf) + out := buf.String() + + // For unknown steps, the step key itself should be used as the label + if !strings.Contains(out, "unknown-step") { + t.Error("output should use step key as label for unknown steps") + } +} + +func TestReport_Fprint(t *testing.T) { + var r Report + r.Add(CheckResult{Step: "deploy", ID: "D1", Name: "Contract deployed", Status: PASS}) + r.Add(CheckResult{Step: "deploy", ID: "D2", Name: "Balance check", Status: FAIL, Message: "No funds", Fix: "Add ETH"}) + r.Add(CheckResult{Step: "services", ID: "S1", Name: "API running", Status: WARN, Message: "Slow start", Fix: "Check logs"}) + + var buf bytes.Buffer + r.Fprint(&buf) + out := buf.String() + + // Header present. + if !strings.Contains(out, "Deployment Verification Report") { + t.Error("output missing header") + } + + // Step grouping. + if !strings.Contains(out, "Contract Deployment") { + t.Error("output missing 'Contract Deployment' group header") + } + if !strings.Contains(out, "Service Startup") { + t.Error("output missing 'Service Startup' group header") + } + + // Status labels present. + if !strings.Contains(out, "[PASS]") { + t.Error("output missing [PASS]") + } + if !strings.Contains(out, "[FAIL]") { + t.Error("output missing [FAIL]") + } + if !strings.Contains(out, "[WARN]") { + t.Error("output missing [WARN]") + } + + // Fix line present for FAIL. + if !strings.Contains(out, "Fix: Add ETH") { + t.Error("output missing fix line for FAIL result") + } + + // Summary at the end. + if !strings.Contains(out, "1 passed, 1 warning, 1 failed, 0 skipped") { + t.Errorf("output missing correct summary line, got:\n%s", out) + } +} diff --git a/tools/pkg/validate/validate.go b/tools/pkg/validate/validate.go new file mode 100644 index 0000000..3e989c2 --- /dev/null +++ b/tools/pkg/validate/validate.go @@ -0,0 +1,93 @@ +package validate + +import ( + "context" + "crypto/ecdsa" + "fmt" + "math/big" + "os" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" +) + +// MinDeployBalance is the minimum balance required for deployment (0.01 ETH). +var MinDeployBalance = big.NewInt(10_000_000_000_000_000) + +// rpcTimeout is the timeout for all RPC calls. +const rpcTimeout = 10 * time.Second + +// AddressNotZero checks that addr is not the zero address. +// Returns an error with the label identifying which address was zero. +func AddressNotZero(addr common.Address, label string) error { + if addr == (common.Address{}) { + return fmt.Errorf( + "%s is the zero address (%s) in the addresses file", + label, addr.Hex(), + ) + } + return nil +} + +// AddressHasCode calls eth_getCode to verify that a contract is deployed at addr. +// Returns a descriptive error if the client is nil, the RPC call fails, or no code exists. +func AddressHasCode(client *ethclient.Client, addr common.Address, label string) error { + if client == nil { + return fmt.Errorf("cannot check %s: no chain client connected", label) + } + + ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout) + defer cancel() + + code, err := client.CodeAt(ctx, addr, nil) + if err != nil { + return fmt.Errorf("cannot check %s: %w", label, err) + } + + if len(code) == 0 { + return fmt.Errorf( + "%s at %s has no deployed code. "+ + "This address will be set as immutable in the contract constructor and cannot be changed. "+ + "Check your deployed-addresses.json file — are you on the right network?", + label, addr.Hex(), + ) + } + + return nil +} + +// KeyHasFunds checks that the account derived from key has at least minWei balance. +// Returns a descriptive error if the client is nil, the RPC call fails, or funds are insufficient. +func KeyHasFunds(client *ethclient.Client, key *ecdsa.PrivateKey, minWei *big.Int) error { + if client == nil { + return fmt.Errorf("cannot check deployer balance: no chain client connected") + } + + addr := crypto.PubkeyToAddress(key.PublicKey) + + ctx, cancel := context.WithTimeout(context.Background(), rpcTimeout) + defer cancel() + + balance, err := client.BalanceAt(ctx, addr, nil) + if err != nil { + return fmt.Errorf("cannot check deployer balance: %w", err) + } + + if balance.Cmp(minWei) < 0 { + return fmt.Errorf( + "deployer %s has insufficient funds (balance: %s wei, minimum required: %s wei). "+ + "Fund this account before deploying", + addr.Hex(), balance.String(), minWei.String(), + ) + } + + return nil +} + +// IsUsingDevKey returns true if the DEPLOYMENT_PRIVATE_KEY environment variable is empty, +// indicating the deployer is falling back to the hardcoded dev key. +func IsUsingDevKey() bool { + return os.Getenv("DEPLOYMENT_PRIVATE_KEY") == "" +} diff --git a/tools/pkg/validate/validate_test.go b/tools/pkg/validate/validate_test.go new file mode 100644 index 0000000..34a85e4 --- /dev/null +++ b/tools/pkg/validate/validate_test.go @@ -0,0 +1,134 @@ +package validate + +import ( + "math/big" + "os" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +func TestAddressNotZero_ZeroAddress(t *testing.T) { + err := AddressNotZero(common.Address{}, "TeeExtensionRegistry") + if err == nil { + t.Fatal("expected error for zero address, got nil") + } + + msg := err.Error() + if !strings.Contains(msg, "TeeExtensionRegistry") { + t.Errorf("error should mention the label, got: %s", msg) + } + if !strings.Contains(msg, "zero address") { + t.Errorf("error should mention 'zero address', got: %s", msg) + } + if !strings.Contains(msg, "0x0000000000000000000000000000000000000000") { + t.Errorf("error should contain the zero address hex, got: %s", msg) + } +} + +func TestAddressNotZero_NonZeroAddress(t *testing.T) { + addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + err := AddressNotZero(addr, "TeeExtensionRegistry") + if err != nil { + t.Fatalf("expected nil error for non-zero address, got: %v", err) + } +} + +func TestAddressHasCode_NilClient(t *testing.T) { + addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + err := AddressHasCode(nil, addr, "TeeExtensionRegistry") + if err == nil { + t.Fatal("expected error for nil client, got nil") + } + + msg := err.Error() + if !strings.Contains(msg, "no chain client connected") { + t.Errorf("error should mention 'no chain client connected', got: %s", msg) + } + if !strings.Contains(msg, "TeeExtensionRegistry") { + t.Errorf("error should mention the label, got: %s", msg) + } +} + +func TestKeyHasFunds_NilClient(t *testing.T) { + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("failed to generate key: %v", err) + } + + err = KeyHasFunds(nil, key, big.NewInt(1)) + if err == nil { + t.Fatal("expected error for nil client, got nil") + } + + msg := err.Error() + if !strings.Contains(msg, "no chain client connected") { + t.Errorf("error should mention 'no chain client connected', got: %s", msg) + } +} + +func TestIsUsingDevKey_WhenSet(t *testing.T) { + original := os.Getenv("DEPLOYMENT_PRIVATE_KEY") + defer os.Setenv("DEPLOYMENT_PRIVATE_KEY", original) + + os.Setenv("DEPLOYMENT_PRIVATE_KEY", "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890") + if IsUsingDevKey() { + t.Error("expected IsUsingDevKey() to return false when DEPLOYMENT_PRIVATE_KEY is set") + } +} + +func TestIsUsingDevKey_WhenUnset(t *testing.T) { + original := os.Getenv("DEPLOYMENT_PRIVATE_KEY") + defer os.Setenv("DEPLOYMENT_PRIVATE_KEY", original) + + os.Unsetenv("DEPLOYMENT_PRIVATE_KEY") + if !IsUsingDevKey() { + t.Error("expected IsUsingDevKey() to return true when DEPLOYMENT_PRIVATE_KEY is unset") + } +} + +func TestAddressNotZero_PartialZero(t *testing.T) { + // Address with only the last byte non-zero — NOT a zero address + addr := common.HexToAddress("0x0000000000000000000000000000000000000001") + err := AddressNotZero(addr, "TestRegistry") + if err != nil { + t.Fatalf("expected nil error for partial-zero address, got: %v", err) + } +} + +func TestAddressNotZero_ErrorContainsLabel(t *testing.T) { + err := AddressNotZero(common.Address{}, "MyCustomLabel") + if err == nil { + t.Fatal("expected error for zero address") + } + if !strings.Contains(err.Error(), "MyCustomLabel") { + t.Errorf("error should contain label 'MyCustomLabel', got: %s", err.Error()) + } +} + +func TestAddressNotZero_ErrorContainsHexAddress(t *testing.T) { + err := AddressNotZero(common.Address{}, "Reg") + if err == nil { + t.Fatal("expected error for zero address") + } + if !strings.Contains(err.Error(), "0x0000000000000000000000000000000000000000") { + t.Errorf("error should contain the zero address hex, got: %s", err.Error()) + } +} + +func TestIsUsingDevKey_WhenEmpty(t *testing.T) { + t.Setenv("DEPLOYMENT_PRIVATE_KEY", "") + if !IsUsingDevKey() { + t.Error("expected IsUsingDevKey() to return true when DEPLOYMENT_PRIVATE_KEY is empty string") + } +} + +func TestMinDeployBalance_Value(t *testing.T) { + // Verify the constant is 0.01 ETH = 10^16 wei + expected := big.NewInt(10_000_000_000_000_000) + if MinDeployBalance.Cmp(expected) != 0 { + t.Errorf("MinDeployBalance = %s, want %s", MinDeployBalance.String(), expected.String()) + } +} From a43c6c648f2f720541e71f8d5336bad09836bc85 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 11:59:12 +0100 Subject: [PATCH 002/107] fix: harden Concord verification and deployment flow --- contracts/CapitalFacility.sol | 4 + contracts/test/ConcordFlow.t.sol | 84 ++++ docs/getting-started.md | 52 ++- go/internal/extension/extension.go | 108 ++++- go/internal/extension/extension_test.go | 85 ++++ script/DeployCoston2.s.sol | 53 ++- scripts/deploy-coston2.sh | 35 +- scripts/extension-post-setup.sh | 0 scripts/extension-setup.sh | 0 tools/cmd/verify-allocation/main.go | 536 +++++++++++++++++++++++ tools/cmd/verify-allocation/main_test.go | 54 +++ 11 files changed, 966 insertions(+), 45 deletions(-) mode change 100755 => 100644 scripts/deploy-coston2.sh mode change 100755 => 100644 scripts/extension-post-setup.sh mode change 100755 => 100644 scripts/extension-setup.sh create mode 100644 tools/cmd/verify-allocation/main.go create mode 100644 tools/cmd/verify-allocation/main_test.go diff --git a/contracts/CapitalFacility.sol b/contracts/CapitalFacility.sol index decdb4b..2c5741e 100644 --- a/contracts/CapitalFacility.sol +++ b/contracts/CapitalFacility.sol @@ -245,6 +245,10 @@ contract CapitalFacility { ConcordTypes.RootAccord storage root = _root(result.rootAccordId); MakkariRound storage round = rounds[result.roundId]; + // The verifier authorizes the digest; the treasury still controls + // when its root relationship materializes those selected children. + if (msg.sender != root.borrower) revert Unauthorized(); + if (result.extensionId != extensionId || result.rootAccordId != round.rootAccordId) { revert InvalidAllocation(); } diff --git a/contracts/test/ConcordFlow.t.sol b/contracts/test/ConcordFlow.t.sol index 978016e..c0e890d 100644 --- a/contracts/test/ConcordFlow.t.sol +++ b/contracts/test/ConcordFlow.t.sol @@ -146,6 +146,10 @@ contract ConcordFlowTest { // Only the configured verifier may authorize materialization. providerC.mark(facility, result.resultDigest); + (bool unauthorizedMaterializeOk,) = address(providerA).call( + abi.encodeWithSelector(ConcordActor.materialize.selector, facility, result) + ); + require(!unauthorizedMaterializeOk, "provider materialized allocation"); treasury.materialize(facility, result); bytes32 childA = keccak256(abi.encode(ROOT_ID, result.resultDigest, address(providerA))); @@ -164,6 +168,15 @@ contract ConcordFlowTest { require(facility.getChild(childB).drawnPrincipal == 150, "B exposure mismatch"); require(facility.getDrawLegIds(DRAW_ID).length == 2, "draw was not split"); + (bool overdrawOk,) = address(treasury).call( + abi.encodeWithSelector(ConcordActor.draw.selector, facility, keccak256("DRAW-02"), ROOT_ID, 251) + ); + require(!overdrawOk, "draw exceeded available capacity"); + (bool exposedCloseOk,) = address(providerA).call( + abi.encodeWithSelector(ConcordActor.closeChild.selector, facility, childA) + ); + require(!exposedCloseOk, "provider closed a child with exposure"); + usdt0.mint(address(treasury), 750); treasury.approve(usdt0, address(facility), type(uint256).max); treasury.repay(facility, DRAW_ID, 750); @@ -177,6 +190,77 @@ contract ConcordFlowTest { require(fxrp.balanceOf(address(treasury)) == 100, "collateral not returned"); } + function testPartialFundingLineageAndAllocationReplay() public { + uint64 expiry = uint64(block.timestamp + 30 days); + treasury.createRoot(facility, ROOT_ID, 1_000, expiry, keccak256("POLICY")); + treasury.lock(facility, ROOT_ID, 100); + + address[] memory providers = new address[](2); + providers[0] = address(providerA); + providers[1] = address(providerB); + treasury.open(facility, ROOT_ID, ROUND_ID, 700, expiry - 1 days, providers); + + address[] memory selected = new address[](2); + selected[0] = address(providerA); + selected[1] = address(providerB); + uint256[] memory allocations = new uint256[](2); + allocations[0] = 600; + allocations[1] = 400; + uint32[] memory fees = new uint32[](2); + fees[0] = 610; + fees[1] = 640; + bytes32[] memory terms = new bytes32[](2); + terms[0] = keccak256("A-TERMS"); + terms[1] = keccak256("B-TERMS"); + + ConcordTypes.AllocationResult memory result = ConcordTypes.AllocationResult({ + extensionId: EXTENSION_ID, + roundId: ROUND_ID, + rootAccordId: ROOT_ID, + success: true, + selectedProviders: selected, + allocatedCapacity: allocations, + acceptedFeeBps: fees, + termsCommitments: terms, + roundExpiry: expiry - 1 days, + resultDigest: bytes32(0) + }); + result.resultDigest = allocationDigest(result); + providerC.mark(facility, result.resultDigest); + treasury.materialize(facility, result); + + bytes32 childA = keccak256(abi.encode(ROOT_ID, result.resultDigest, address(providerA))); + bytes32 childB = keccak256(abi.encode(ROOT_ID, result.resultDigest, address(providerB))); + + providerA.fund(facility, childA, 300); + require(facility.getChild(childA).state == ConcordTypes.ChildState.FUNDED, "partial funding not recorded"); + require(facility.getChild(childA).committedCapacity == 300, "partial commitment mismatch"); + require(facility.getRoot(ROOT_ID).state == ConcordTypes.RootState.FUNDING, "root activated too early"); + require(facility.getRoot(ROOT_ID).committedCapacity == 300, "root partial commitment mismatch"); + + providerA.fund(facility, childA, 300); + providerB.fund(facility, childB, 400); + require(facility.getRoot(ROOT_ID).state == ConcordTypes.RootState.ACTIVE, "root did not activate"); + require(facility.getRoot(ROOT_ID).committedCapacity == 1_000, "root commitment sum mismatch"); + + bytes32[] memory rootChildren = registry.getChildren(ROOT_ID); + require(rootChildren.length == 1 && rootChildren[0] == ROUND_ID, "root lineage mismatch"); + bytes32[] memory roundChildren = registry.getChildren(ROUND_ID); + require(roundChildren.length == 1 && roundChildren[0] == result.resultDigest, "round lineage mismatch"); + bytes32[] memory allocationChildren = registry.getChildren(result.resultDigest); + require(allocationChildren.length == 2, "child Accord lineage missing"); + require( + facility.getChild(childA).validUntil <= facility.getRoot(ROOT_ID).validUntil && + facility.getChild(childB).validUntil <= facility.getRoot(ROOT_ID).validUntil, + "child validity exceeded root" + ); + + (bool replayOk,) = address(treasury).call( + abi.encodeWithSelector(ConcordActor.materialize.selector, facility, result) + ); + require(!replayOk, "allocation replay succeeded"); + } + function testUnauthorizedResultAndOverfundingFail() public { uint64 expiry = uint64(block.timestamp + 30 days); treasury.createRoot(facility, ROOT_ID, 1_000, expiry, keccak256("POLICY")); diff --git a/docs/getting-started.md b/docs/getting-started.md index 54a222a..222639c 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -45,16 +45,43 @@ named entry in the checked-in Flare ContractRegistry package. ## Deployment inputs -The deployment script requires a funded Coston2 broadcaster and an explicit -allocation verifier: +Concord deployment is intentionally two-phase: the FCC instruction sender is +deployed and registered first, then the economic facility is deployed with +the resulting extension id. Do not deploy the facility with a guessed id. + +The official scaffold lifecycle remains available through pre-build.sh. +It deploys the Concord instruction sender with the configured scaffold +registries, registers the extension, and writes the live values to +config/extension.env: + +~~~bash +export CHAIN=coston2 +export DEPLOYMENT_PRIVATE_KEY=0x... +./scripts/pre-build.sh +source ./config/extension.env +~~~ + +For a standalone Foundry sender deployment, use the explicit sender phase +and complete FCC registration through the official scaffold tooling before +running the facility phase: ~~~bash export DEPLOYMENT_OWNER=0x... -export ALLOCATION_VERIFIER=0x... -export CONCORD_EXTENSION_ID=0x... +export DEPLOYMENT_PRIVATE_KEY=0x... export TEE_EXTENSION_REGISTRY=0x... export TEE_MACHINE_REGISTRY=0x... -./scripts/deploy-coston2.sh +DEPLOY_PHASE=sender ./scripts/deploy-coston2.sh +~~~ + +After registration, set CONCORD_EXTENSION_ID to the registered extension +id and deploy the Accord/facility contracts: + +~~~bash +export DEPLOYMENT_OWNER=0x... +export DEPLOYMENT_PRIVATE_KEY=0x... +export ALLOCATION_VERIFIER=0x... +export CONCORD_EXTENSION_ID=0x... +DEPLOY_PHASE=facility ./scripts/deploy-coston2.sh ~~~ The TEE registry values are normally the Coston2 FlareTeeManager diamond @@ -71,5 +98,20 @@ register extension → start official proxy/TEE scaffold → submit encrypted qu → finalize encrypted round → verify result offchain → mark digest → materialize children ~~~ +The runner writes the raw successful FCC allocation response with `-out`. +The verifier then checks the response against the deployed facility allocation +verifier boundary: extension id, open round, root binding, target capacity, +fee limits, eligible providers, and the canonical result digest. It is +read-only unless `-mark` is supplied: + +~~~bash +cd tools +go run ./cmd/verify-allocation -c https://coston2-api.flare.network/ext/C/rpc -facility 0x... -result ../evidence/allocation.json -extensionId 0x... -roundId 0x... -rootAccordId 0x... -out ../evidence/allocation-verification.json + +# Only after the local checks pass, with DEPLOYMENT_PRIVATE_KEY set to +# the facility allocationVerifier address: +go run ./cmd/verify-allocation -c https://coston2-api.flare.network/ext/C/rpc -facility 0x... -result ../evidence/allocation.json -extensionId 0x... -roundId 0x... -rootAccordId 0x... -mark -out ../evidence/allocation-verification.json +~~~ + No live FCC or facility deployment is claimed by local tests alone. See CONCORD_STATUS.md for the current evidence boundary. diff --git a/go/internal/extension/extension.go b/go/internal/extension/extension.go index c421a7a..ff34ed1 100644 --- a/go/internal/extension/extension.go +++ b/go/internal/extension/extension.go @@ -32,6 +32,8 @@ type Extension struct { Server *http.Server signPort int quotes map[string]map[string]types.QuoteRequest + finalized map[string]bool + finalizing map[string]bool quoteCount int finalizedRounds int decrypt func([]byte) ([]byte, error) @@ -39,8 +41,10 @@ type Extension struct { func New(extensionPort, signPort int) *Extension { e := &Extension{ - signPort: signPort, - quotes: make(map[string]map[string]types.QuoteRequest), + signPort: signPort, + quotes: make(map[string]map[string]types.QuoteRequest), + finalized: make(map[string]bool), + finalizing: make(map[string]bool), } e.decrypt = e.decryptPayload @@ -124,10 +128,11 @@ func (e *Extension) processSubmitQuote(action teetypes.Action, df *instruction.D } provider := strings.ToLower(common.HexToAddress(req.Provider).Hex()) e.mu.Lock() - if e.quotes[req.RoundID] == nil { - e.quotes[req.RoundID] = make(map[string]types.QuoteRequest) + key := roundKey(req.RoundID) + if e.quotes[key] == nil { + e.quotes[key] = make(map[string]types.QuoteRequest) } - e.quotes[req.RoundID][provider] = req + e.quotes[key][provider] = req e.quoteCount++ e.mu.Unlock() @@ -140,24 +145,65 @@ func (e *Extension) processFinalizeRound(action teetypes.Action, df *instruction if err := decodeStrict(payload, &req); err != nil { return marshalActionResult(action, df, nil, 0, fmt.Errorf("decoding finalization: %w", err)) } - if len(req.Quotes) == 0 { - e.mu.RLock() - for _, quote := range e.quotes[req.RoundID] { - req.Quotes = append(req.Quotes, quote) - } - e.mu.RUnlock() - } if req.EvaluationTime == 0 { req.EvaluationTime = uint64(time.Now().Unix()) } + + round := roundKey(req.RoundID) + e.mu.Lock() + if e.finalized[round] { + e.mu.Unlock() + return marshalActionResult(action, df, nil, 0, fmt.Errorf("round has already been finalized")) + } + if e.finalizing[round] { + e.mu.Unlock() + return marshalActionResult(action, df, nil, 0, fmt.Errorf("round finalization is already in progress")) + } + + storedQuotes := make([]types.QuoteRequest, 0, len(e.quotes[round])) + for _, quote := range e.quotes[round] { + storedQuotes = append(storedQuotes, quote) + } + if len(storedQuotes) == 0 { + e.mu.Unlock() + return marshalActionResult(action, df, nil, 0, fmt.Errorf("round has no submitted quotes")) + } + + // FINALIZE_ROUND may repeat the submitted set for auditability, but it may + // not introduce a new quote. Compare provider -> quote digest against the + // stored SUBMIT_QUOTE set, then always run CoFill over the stored snapshot. + if len(req.Quotes) != 0 { + storedDigests, err := quoteDigestSet(storedQuotes) + if err != nil { + e.mu.Unlock() + return marshalActionResult(action, df, nil, 0, err) + } + requestDigests, err := quoteDigestSet(req.Quotes) + if err != nil { + e.mu.Unlock() + return marshalActionResult(action, df, nil, 0, err) + } + if !sameQuoteSet(storedDigests, requestDigests) { + e.mu.Unlock() + return marshalActionResult(action, df, nil, 0, fmt.Errorf("finalization quotes do not match submitted quotes")) + } + } + req.Quotes = storedQuotes + e.finalizing[round] = true + e.mu.Unlock() + result, err := CoFill(req) + e.mu.Lock() + delete(e.finalizing, round) + if err == nil { + e.finalized[round] = true + e.finalizedRounds++ + } + e.mu.Unlock() if err != nil { return marshalActionResult(action, df, nil, 0, err) } data, _ := json.Marshal(result) - e.mu.Lock() - e.finalizedRounds++ - e.mu.Unlock() return marshalActionResult(action, df, data, 1, nil) } @@ -239,6 +285,38 @@ func validBytes32(value string) bool { return strings.HasPrefix(value, "0x") && len(value) == 66 && common.HexToHash(value) != (common.Hash{}) } +func roundKey(value string) string { + return strings.ToLower(value) +} + +func quoteDigestSet(quotes []types.QuoteRequest) (map[string]string, error) { + digests := make(map[string]string, len(quotes)) + for _, quote := range quotes { + provider := strings.ToLower(common.HexToAddress(quote.Provider).Hex()) + if _, exists := digests[provider]; exists { + return nil, fmt.Errorf("duplicate provider quote") + } + digest, err := quoteDigest(quote) + if err != nil { + return nil, err + } + digests[provider] = digest.Hex() + } + return digests, nil +} + +func sameQuoteSet(left, right map[string]string) bool { + if len(left) != len(right) { + return false + } + for provider, digest := range left { + if right[provider] != digest { + return false + } + } + return true +} + func validateQuote(q types.QuoteRequest, now uint64, maxFee uint32, roundExpiry uint64, eligible map[string]bool) error { if !validBytes32(q.RoundID) || !validBytes32(q.RootAccordID) { return fmt.Errorf("roundId and rootAccordId must be non-zero bytes32 values") diff --git a/go/internal/extension/extension_test.go b/go/internal/extension/extension_test.go index 7a8b9e9..5583bfc 100644 --- a/go/internal/extension/extension_test.go +++ b/go/internal/extension/extension_test.go @@ -13,6 +13,7 @@ import ( "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" "github.com/flare-foundation/go-flare-common/pkg/tee/instruction" teetypes "github.com/flare-foundation/tee-node/pkg/types" @@ -57,6 +58,19 @@ func buildAction(opCommand string, payload []byte) teetypes.Action { return teetypes.Action{Data: teetypes.ActionData{ID: common.HexToHash("0x1234"), Message: b}} } +func actionResult(t *testing.T, body []byte) (status uint8, data []byte, log string) { + t.Helper() + var result struct { + Status uint8 `json:"status"` + Data hexutil.Bytes `json:"data"` + Log string `json:"log"` + } + if err := json.Unmarshal(body, &result); err != nil { + t.Fatal(err) + } + return result.Status, []byte(result.Data), result.Log +} + func TestCoFillDeterministicPartialAllocation(t *testing.T) { keys := make([]*ecdsa.PrivateKey, 3) quotes := make([]types.QuoteRequest, 3) @@ -125,3 +139,74 @@ func TestActionRequiresTEEDecryption(t *testing.T) { t.Fatalf("expected encrypted payload boundary, got status=%d body=%s", status, body) } } + +func TestFinalizationUsesOnlySubmittedQuotesAndCannotReplay(t *testing.T) { + e := New(0, 1) + e.decrypt = func(payload []byte) ([]byte, error) { return payload, nil } + + keyA, _ := crypto.GenerateKey() + providerA := crypto.PubkeyToAddress(keyA.PublicKey).Hex() + submitted := quoteFixture(t, keyA, providerA, 610, "100") + + payload, _ := json.Marshal(submitted) + status, body := e.processAction(buildAction(config.OPCommandSubmitQuote, payload)) + if status != 200 { + t.Fatalf("submit returned HTTP %d", status) + } + submitStatus, _, submitLog := actionResult(t, body) + if submitStatus != 1 { + t.Fatalf("submit failed: status=%d log=%s", submitStatus, submitLog) + } + + keyB, _ := crypto.GenerateKey() + providerB := crypto.PubkeyToAddress(keyB.PublicKey).Hex() + unsubmitted := quoteFixture(t, keyB, providerB, 620, "100") + finalize := types.FinalizeRoundRequest{ + ExtensionID: "0x0300000000000000000000000000000000000000000000000000000000000000", + RoundID: submitted.RoundID, + RootAccordID: submitted.RootAccordID, + TargetCapacity: "100", + MaxFeeBps: 700, + RoundExpiry: uint64(time.Now().Unix()) + 7200, + EvaluationTime: uint64(time.Now().Unix()), + EligibleProviders: []string{providerA, providerB}, + Quotes: []types.QuoteRequest{unsubmitted}, + } + payload, _ = json.Marshal(finalize) + status, body = e.processAction(buildAction(config.OPCommandFinalizeRound, payload)) + if status != 200 { + t.Fatalf("mismatched finalization returned HTTP %d", status) + } + finalizeStatus, _, finalizeLog := actionResult(t, body) + if finalizeStatus != 0 || !strings.Contains(finalizeLog, "do not match submitted") { + t.Fatalf("unsubmitted quote was accepted: status=%d log=%s", finalizeStatus, finalizeLog) + } + + finalize.Quotes = nil + finalize.EligibleProviders = []string{providerA} + payload, _ = json.Marshal(finalize) + status, body = e.processAction(buildAction(config.OPCommandFinalizeRound, payload)) + if status != 200 { + t.Fatalf("valid finalization returned HTTP %d", status) + } + finalizeStatus, data, finalizeLog := actionResult(t, body) + if finalizeStatus != 1 { + t.Fatalf("valid finalization failed: status=%d log=%s", finalizeStatus, finalizeLog) + } + var result types.FinalizeRoundResponse + if err := json.Unmarshal(data, &result); err != nil { + t.Fatal(err) + } + if len(result.SelectedProviders) != 1 || !strings.EqualFold(result.SelectedProviders[0], providerA) { + t.Fatalf("finalization did not use submitted quote: %+v", result) + } + + status, body = e.processAction(buildAction(config.OPCommandFinalizeRound, payload)) + if status != 200 { + t.Fatalf("replayed finalization returned HTTP %d", status) + } + finalizeStatus, _, finalizeLog = actionResult(t, body) + if finalizeStatus != 0 || !strings.Contains(finalizeLog, "already been finalized") { + t.Fatalf("finalization replay was accepted: status=%d log=%s", finalizeStatus, finalizeLog) + } +} diff --git a/script/DeployCoston2.s.sol b/script/DeployCoston2.s.sol index 2b3da68..a6f0850 100644 --- a/script/DeployCoston2.s.sol +++ b/script/DeployCoston2.s.sol @@ -28,13 +28,49 @@ contract DeployCoston2 { bytes32 extensionId ); + event InstructionSenderDeployed( + address indexed instructionSender, + address indexed teeExtensionRegistry, + address indexed teeMachineRegistry + ); + + /// @notice Standard Foundry entry point for the facility phase. function run() external { + _deployFacility(); + } + + /// @notice Low-level sender deployment for a manual registration flow. + /// Register the emitted sender through the official FCC tooling before + /// deploying the facility with runFacility(). + function runSender() external { + _deploySender(); + } + + /// @notice Deploy the economic contracts after the FCC extension id exists. + function runFacility() external { + _deployFacility(); + } + + function _deploySender() internal { + address owner = vm.envAddress("DEPLOYMENT_OWNER"); + address teeExtensionRegistry = vm.envAddress("TEE_EXTENSION_REGISTRY"); + address teeMachineRegistry = vm.envAddress("TEE_MACHINE_REGISTRY"); + + vm.startBroadcast(owner); + ConcordInstructionSender sender = new ConcordInstructionSender( + ITeeExtensionRegistry(teeExtensionRegistry), + ITeeMachineRegistry(teeMachineRegistry) + ); + vm.stopBroadcast(); + + emit InstructionSenderDeployed(address(sender), teeExtensionRegistry, teeMachineRegistry); + } + + function _deployFacility() internal { address owner = vm.envAddress("DEPLOYMENT_OWNER"); address fxrp = vm.envAddress("FXRP_TOKEN"); address usdt0 = vm.envAddress("USDT0_TOKEN"); address verifier = vm.envAddress("ALLOCATION_VERIFIER"); - address teeExtensionRegistry = vm.envAddress("TEE_EXTENSION_REGISTRY"); - address teeMachineRegistry = vm.envAddress("TEE_MACHINE_REGISTRY"); bytes32 extensionId = vm.envBytes32("CONCORD_EXTENSION_ID"); vm.startBroadcast(owner); @@ -47,19 +83,8 @@ contract DeployCoston2 { extensionId ); registry.setFacility(address(facility)); - ConcordInstructionSender sender = new ConcordInstructionSender( - ITeeExtensionRegistry(teeExtensionRegistry), - ITeeMachineRegistry(teeMachineRegistry) - ); vm.stopBroadcast(); - emit ConcordDeployed( - address(registry), - address(facility), - address(sender), - fxrp, - usdt0, - extensionId - ); + emit ConcordDeployed(address(registry), address(facility), address(0), fxrp, usdt0, extensionId); } } diff --git a/scripts/deploy-coston2.sh b/scripts/deploy-coston2.sh old mode 100755 new mode 100644 index 0141079..7f24420 --- a/scripts/deploy-coston2.sh +++ b/scripts/deploy-coston2.sh @@ -7,23 +7,36 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" CONFIG_FILE="${CONCORD_NETWORK_CONFIG:-$PROJECT_DIR/config/networks/coston2.json}" +DEPLOY_PHASE="${DEPLOY_PHASE:-facility}" die() { echo "deploy-coston2: $*" >&2; exit 1; } command -v forge >/dev/null 2>&1 || die "forge (Foundry) is required" command -v jq >/dev/null 2>&1 || die "jq is required" -ASSETS_JSON="$($SCRIPT_DIR/resolve-coston2-assets.sh)" export CHAIN_URL="${CHAIN_URL:-$(jq -r '.rpcUrl' "$CONFIG_FILE")}" -export FXRP_TOKEN="$(jq -r '.fxrp.token' <<<"$ASSETS_JSON")" -export USDT0_TOKEN="$(jq -r '.usdt0.token' <<<"$ASSETS_JSON")" - : "${DEPLOYMENT_OWNER:?DEPLOYMENT_OWNER must be set to the broadcasting address}" -: "${ALLOCATION_VERIFIER:?ALLOCATION_VERIFIER must be set to the verifier address}" -: "${CONCORD_EXTENSION_ID:?CONCORD_EXTENSION_ID must be set to the registered FCC extension id}" -: "${TEE_EXTENSION_REGISTRY:?TEE_EXTENSION_REGISTRY must be set; on Coston2 this is normally the FlareTeeManager diamond}" -: "${TEE_MACHINE_REGISTRY:?TEE_MACHINE_REGISTRY must be set; on Coston2 this is normally the FlareTeeManager diamond}" +: "${DEPLOYMENT_PRIVATE_KEY:?DEPLOYMENT_PRIVATE_KEY must be set in the ignored environment}" cd "$PROJECT_DIR" -forge script script/DeployCoston2.s.sol:DeployCoston2 \ - --rpc-url "$CHAIN_URL" \ - --broadcast +FORGE_ARGS=(--rpc-url "$CHAIN_URL" --broadcast --private-key "$DEPLOYMENT_PRIVATE_KEY") + +case "$DEPLOY_PHASE" in + sender) + : "${TEE_EXTENSION_REGISTRY:?TEE_EXTENSION_REGISTRY must be set for sender deployment}" + : "${TEE_MACHINE_REGISTRY:?TEE_MACHINE_REGISTRY must be set for sender deployment}" + forge script script/DeployCoston2.s.sol:DeployCoston2 \ + --sig "runSender()" "${FORGE_ARGS[@]}" + ;; + facility) + : "${ALLOCATION_VERIFIER:?ALLOCATION_VERIFIER must be set to the verifier address}" + : "${CONCORD_EXTENSION_ID:?CONCORD_EXTENSION_ID must be set to the registered FCC extension id}" + ASSETS_JSON="$($SCRIPT_DIR/resolve-coston2-assets.sh)" + export FXRP_TOKEN="$(jq -r '.fxrp.token' <<<"$ASSETS_JSON")" + export USDT0_TOKEN="$(jq -r '.usdt0.token' <<<"$ASSETS_JSON")" + forge script script/DeployCoston2.s.sol:DeployCoston2 \ + --sig "runFacility()" "${FORGE_ARGS[@]}" + ;; + *) + die "DEPLOY_PHASE must be sender or facility" + ;; +esac diff --git a/scripts/extension-post-setup.sh b/scripts/extension-post-setup.sh old mode 100755 new mode 100644 diff --git a/scripts/extension-setup.sh b/scripts/extension-setup.sh old mode 100755 new mode 100644 diff --git a/tools/cmd/verify-allocation/main.go b/tools/cmd/verify-allocation/main.go new file mode 100644 index 0000000..2cab0d9 --- /dev/null +++ b/tools/cmd/verify-allocation/main.go @@ -0,0 +1,536 @@ +package main + +import ( + "bytes" + "context" + "encoding/binary" + "encoding/json" + "flag" + "fmt" + "math/big" + "os" + "strings" + "time" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + coretypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + + "concord/tools/pkg/configs" + "concord/tools/pkg/support" +) + +type allocationResult struct { + Success bool `json:"success"` + ExtensionID string `json:"extensionId"` + RoundID string `json:"roundId"` + RootAccordID string `json:"rootAccordId"` + SelectedProviders []string `json:"selectedProviders"` + AllocatedCapacity []string `json:"allocatedCapacity"` + AcceptedFeeBps []uint32 `json:"acceptedFeeBps"` + TermsCommitments []string `json:"termsCommitments"` + RoundExpiry uint64 `json:"roundExpiry"` + ResultDigest string `json:"resultDigest"` +} + +const facilityABIJSON = `[ + { + "inputs": [], + "name": "extensionId", + "outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allocationVerifier", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "name": "rounds", + "outputs": [ + {"internalType": "bytes32", "name": "id", "type": "bytes32"}, + {"internalType": "bytes32", "name": "rootAccordId", "type": "bytes32"}, + {"internalType": "uint256", "name": "targetCapacity", "type": "uint256"}, + {"internalType": "uint32", "name": "maxFeeBps", "type": "uint32"}, + {"internalType": "uint64", "name": "roundExpiry", "type": "uint64"}, + {"internalType": "uint8", "name": "status", "type": "uint8"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "bytes32", "name": "", "type": "bytes32"}, + {"internalType": "address", "name": "", "type": "address"} + ], + "name": "eligibleProvider", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "bytes32", "name": "resultDigest", "type": "bytes32"}], + "name": "markAllocationVerified", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +]` + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "verify-allocation: %v\n", err) + os.Exit(1) + } +} + +func run() error { + chainURL := flag.String("c", configs.ChainNodeURL, "chain RPC URL") + facilityFlag := flag.String("facility", "", "CapitalFacility address") + resultFile := flag.String("result", "", "JSON file produced by run-test") + extensionFlag := flag.String("extensionId", "", "expected FCC extension id") + roundFlag := flag.String("roundId", "", "expected Makkari round id") + rootFlag := flag.String("rootAccordId", "", "expected root Accord id") + mark := flag.Bool("mark", false, "broadcast markAllocationVerified after local verification") + evidenceFile := flag.String("out", "", "optional JSON evidence output path") + flag.Parse() + + if *facilityFlag == "" || *resultFile == "" || *extensionFlag == "" || *roundFlag == "" || *rootFlag == "" { + return fmt.Errorf("--facility, --result, --extensionId, --roundId, and --rootAccordId are required") + } + if !common.IsHexAddress(*facilityFlag) || common.HexToAddress(*facilityFlag) == (common.Address{}) { + return fmt.Errorf("facility must be a non-zero EVM address") + } + facility := common.HexToAddress(*facilityFlag) + expectedExtension, err := parseBytes32(*extensionFlag, "extensionId", false) + if err != nil { + return err + } + expectedRound, err := parseBytes32(*roundFlag, "roundId", false) + if err != nil { + return err + } + expectedRoot, err := parseBytes32(*rootFlag, "rootAccordId", false) + if err != nil { + return err + } + + payload, err := os.ReadFile(*resultFile) + if err != nil { + return fmt.Errorf("read result: %w", err) + } + var result allocationResult + if err := json.Unmarshal(payload, &result); err != nil { + return fmt.Errorf("decode allocation result: %w", err) + } + + facilityABI, err := abi.JSON(strings.NewReader(facilityABIJSON)) + if err != nil { + return fmt.Errorf("parse CapitalFacility ABI: %w", err) + } + client, err := ethclient.Dial(*chainURL) + if err != nil { + return fmt.Errorf("connect to chain: %w", err) + } + defer client.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + digest, verifier, err := verifyResult( + ctx, + client, + facilityABI, + facility, + result, + expectedExtension, + expectedRound, + expectedRoot, + ) + if err != nil { + return err + } + fmt.Printf("local allocation verification passed: digest=%s facility=%s\n", digest.Hex(), facility.Hex()) + + evidence := map[string]any{ + "broadcast": false, + "facility": facility.Hex(), + "extensionId": expectedExtension.Hex(), + "roundId": expectedRound.Hex(), + "rootAccordId": expectedRoot.Hex(), + "resultDigest": digest.Hex(), + "verifier": verifier.Hex(), + "verifiedAt": time.Now().UTC().Format(time.RFC3339), + } + if !*mark { + if *evidenceFile != "" { + if err := writeEvidence(*evidenceFile, evidence); err != nil { + return err + } + } + fmt.Println("not broadcast: rerun with -mark to call markAllocationVerified") + return nil + } + + privateKey, err := support.DefaultPrivateKey() + if err != nil { + return err + } + signer := crypto.PubkeyToAddress(privateKey.PublicKey) + if signer != verifier { + return fmt.Errorf("DEPLOYMENT_PRIVATE_KEY resolves to %s, but facility allocationVerifier is %s", signer.Hex(), verifier.Hex()) + } + chainID, err := client.ChainID(ctx) + if err != nil { + return fmt.Errorf("read chain id: %w", err) + } + opts, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID) + if err != nil { + return fmt.Errorf("create transactor: %w", err) + } + opts.Context = ctx + bound := bind.NewBoundContract(facility, facilityABI, client, client, client) + tx, err := bound.Transact(opts, "markAllocationVerified", digest) + if err != nil { + return fmt.Errorf("send markAllocationVerified: %w", err) + } + receipt, err := bind.WaitMined(ctx, client, tx) + if err != nil { + return fmt.Errorf("wait for markAllocationVerified %s: %w", tx.Hash().Hex(), err) + } + if receipt.Status != coretypes.ReceiptStatusSuccessful { + return fmt.Errorf("markAllocationVerified reverted in transaction %s", tx.Hash().Hex()) + } + evidence["broadcast"] = true + evidence["txHash"] = tx.Hash().Hex() + if receipt.BlockNumber != nil { + evidence["blockNumber"] = receipt.BlockNumber.String() + } + if *evidenceFile != "" { + if err := writeEvidence(*evidenceFile, evidence); err != nil { + return err + } + } + fmt.Printf("allocation digest marked verified: tx=%s\n", tx.Hash().Hex()) + return nil +} + +func verifyResult( + ctx context.Context, + client *ethclient.Client, + facilityABI abi.ABI, + facility common.Address, + result allocationResult, + expectedExtension common.Hash, + expectedRound common.Hash, + expectedRoot common.Hash, +) (common.Hash, common.Address, error) { + if !result.Success { + return common.Hash{}, common.Address{}, fmt.Errorf("FCC allocation result is not successful") + } + extensionID, err := parseBytes32(result.ExtensionID, "result extensionId", false) + if err != nil { + return common.Hash{}, common.Address{}, err + } + roundID, err := parseBytes32(result.RoundID, "result roundId", false) + if err != nil { + return common.Hash{}, common.Address{}, err + } + rootID, err := parseBytes32(result.RootAccordID, "result rootAccordId", false) + if err != nil { + return common.Hash{}, common.Address{}, err + } + resultDigest, err := parseBytes32(result.ResultDigest, "resultDigest", false) + if err != nil { + return common.Hash{}, common.Address{}, err + } + if extensionID != expectedExtension || roundID != expectedRound || rootID != expectedRoot { + return common.Hash{}, common.Address{}, fmt.Errorf("result is not bound to the requested extension, round, and root") + } + if len(result.SelectedProviders) == 0 || + len(result.SelectedProviders) != len(result.AllocatedCapacity) || + len(result.SelectedProviders) != len(result.AcceptedFeeBps) || + len(result.SelectedProviders) != len(result.TermsCommitments) { + return common.Hash{}, common.Address{}, fmt.Errorf("allocation arrays are empty or have different lengths") + } + + extensionValues, err := callMethod(ctx, client, facility, facilityABI, "extensionId") + if err != nil { + return common.Hash{}, common.Address{}, err + } + chainExtension, err := hashValue(extensionValues, "facility extensionId") + if err != nil { + return common.Hash{}, common.Address{}, err + } + if chainExtension != extensionID { + return common.Hash{}, common.Address{}, fmt.Errorf("facility extensionId %s does not match result %s", chainExtension.Hex(), extensionID.Hex()) + } + + verifierValues, err := callMethod(ctx, client, facility, facilityABI, "allocationVerifier") + if err != nil { + return common.Hash{}, common.Address{}, err + } + verifier, err := addressValue(verifierValues, "facility allocationVerifier") + if err != nil { + return common.Hash{}, common.Address{}, err + } + if verifier == (common.Address{}) { + return common.Hash{}, common.Address{}, fmt.Errorf("facility allocationVerifier is zero") + } + + roundValues, err := callMethod(ctx, client, facility, facilityABI, "rounds", roundID) + if err != nil { + return common.Hash{}, common.Address{}, err + } + if len(roundValues) != 6 { + return common.Hash{}, common.Address{}, fmt.Errorf("facility rounds getter returned %d values, expected 6", len(roundValues)) + } + chainRoundRoot, err := hashValue(roundValues[1:2], "round rootAccordId") + if err != nil { + return common.Hash{}, common.Address{}, err + } + target, ok := roundValues[2].(*big.Int) + if !ok || target.Sign() <= 0 { + return common.Hash{}, common.Address{}, fmt.Errorf("facility round targetCapacity is invalid") + } + maxFee, err := uint64Value(roundValues[3], "round maxFeeBps") + if err != nil { + return common.Hash{}, common.Address{}, err + } + roundExpiry, err := uint64Value(roundValues[4], "round roundExpiry") + if err != nil { + return common.Hash{}, common.Address{}, err + } + status, err := uint64Value(roundValues[5], "round status") + if err != nil { + return common.Hash{}, common.Address{}, err + } + if chainRoundRoot != rootID { + return common.Hash{}, common.Address{}, fmt.Errorf("round is bound to root %s, not %s", chainRoundRoot.Hex(), rootID.Hex()) + } + if status != 1 { + return common.Hash{}, common.Address{}, fmt.Errorf("round is not open; status=%d", status) + } + if result.RoundExpiry != roundExpiry { + return common.Hash{}, common.Address{}, fmt.Errorf("result expiry %d does not match round expiry %d", result.RoundExpiry, roundExpiry) + } + if maxFee > 10000 { + return common.Hash{}, common.Address{}, fmt.Errorf("round maxFeeBps exceeds the protocol bound") + } + + sum := new(big.Int) + seen := make(map[common.Address]bool, len(result.SelectedProviders)) + for i, providerText := range result.SelectedProviders { + provider, err := parseAddress(providerText, fmt.Sprintf("selectedProviders[%d]", i)) + if err != nil { + return common.Hash{}, common.Address{}, err + } + if seen[provider] { + return common.Hash{}, common.Address{}, fmt.Errorf("provider %s appears more than once", provider.Hex()) + } + seen[provider] = true + if uint64(result.AcceptedFeeBps[i]) > maxFee { + return common.Hash{}, common.Address{}, fmt.Errorf("provider %s exceeds the round fee bound", provider.Hex()) + } + amount, err := parsePositiveUint256(result.AllocatedCapacity[i], fmt.Sprintf("allocatedCapacity[%d]", i)) + if err != nil { + return common.Hash{}, common.Address{}, err + } + if _, err := parseBytes32(result.TermsCommitments[i], fmt.Sprintf("termsCommitments[%d]", i), true); err != nil { + return common.Hash{}, common.Address{}, err + } + eligibleValues, err := callMethod(ctx, client, facility, facilityABI, "eligibleProvider", roundID, provider) + if err != nil { + return common.Hash{}, common.Address{}, err + } + if len(eligibleValues) != 1 { + return common.Hash{}, common.Address{}, fmt.Errorf("eligibleProvider returned an unexpected result") + } + eligible, ok := eligibleValues[0].(bool) + if !ok || !eligible { + return common.Hash{}, common.Address{}, fmt.Errorf("provider %s is not eligible for the round", provider.Hex()) + } + sum.Add(sum, amount) + } + if sum.Cmp(target) != 0 { + return common.Hash{}, common.Address{}, fmt.Errorf("allocation total %s does not equal round target %s", sum, target) + } + + computed, err := allocationResultDigest(result, extensionID, roundID, rootID) + if err != nil { + return common.Hash{}, common.Address{}, err + } + if computed != resultDigest { + return common.Hash{}, common.Address{}, fmt.Errorf("resultDigest %s does not match recomputed digest %s", resultDigest.Hex(), computed.Hex()) + } + return computed, verifier, nil +} + +func callMethod( + ctx context.Context, + client *ethclient.Client, + contract common.Address, + contractABI abi.ABI, + method string, + args ...interface{}, +) ([]interface{}, error) { + input, err := contractABI.Pack(method, args...) + if err != nil { + return nil, fmt.Errorf("pack %s: %w", method, err) + } + output, err := client.CallContract(ctx, ethereum.CallMsg{To: &contract, Data: input}, nil) + if err != nil { + return nil, fmt.Errorf("call %s: %w", method, err) + } + values, err := contractABI.Unpack(method, output) + if err != nil { + return nil, fmt.Errorf("decode %s: %w", method, err) + } + return values, nil +} + +func allocationResultDigest(result allocationResult, extensionID, roundID, rootID common.Hash) (common.Hash, error) { + var encoded bytes.Buffer + encoded.Write(extensionID.Bytes()) + encoded.Write(roundID.Bytes()) + encoded.Write(rootID.Bytes()) + if result.Success { + encoded.WriteByte(1) + } else { + encoded.WriteByte(0) + } + var expiry [8]byte + binary.BigEndian.PutUint64(expiry[:], result.RoundExpiry) + encoded.Write(expiry[:]) + for i := range result.SelectedProviders { + provider, err := parseAddress(result.SelectedProviders[i], fmt.Sprintf("selectedProviders[%d]", i)) + if err != nil { + return common.Hash{}, err + } + amount, err := parsePositiveUint256(result.AllocatedCapacity[i], fmt.Sprintf("allocatedCapacity[%d]", i)) + if err != nil { + return common.Hash{}, err + } + terms, err := parseBytes32(result.TermsCommitments[i], fmt.Sprintf("termsCommitments[%d]", i), true) + if err != nil { + return common.Hash{}, err + } + encoded.Write(provider.Bytes()) + amountBytes := make([]byte, 32) + amount.FillBytes(amountBytes) + encoded.Write(amountBytes) + var fee [4]byte + binary.BigEndian.PutUint32(fee[:], result.AcceptedFeeBps[i]) + encoded.Write(fee[:]) + encoded.Write(terms.Bytes()) + } + return crypto.Keccak256Hash(encoded.Bytes()), nil +} + +func parseBytes32(value, name string, allowZero bool) (common.Hash, error) { + raw, err := hexutil.Decode(value) + if err != nil || len(raw) != 32 { + return common.Hash{}, fmt.Errorf("%s must be a 32-byte 0x-prefixed value", name) + } + hash := common.BytesToHash(raw) + if !allowZero && hash == (common.Hash{}) { + return common.Hash{}, fmt.Errorf("%s must be non-zero", name) + } + return hash, nil +} + +func parseAddress(value, name string) (common.Address, error) { + if !common.IsHexAddress(value) { + return common.Address{}, fmt.Errorf("%s must be a valid EVM address", name) + } + address := common.HexToAddress(value) + if address == (common.Address{}) { + return common.Address{}, fmt.Errorf("%s must be non-zero", name) + } + return address, nil +} + +func parsePositiveUint256(value, name string) (*big.Int, error) { + if value == "" || strings.HasPrefix(value, "-") { + return nil, fmt.Errorf("%s must be a positive uint256", name) + } + number, ok := new(big.Int).SetString(value, 10) + if !ok || number.Sign() <= 0 || number.BitLen() > 256 { + return nil, fmt.Errorf("%s must be a positive uint256", name) + } + return number, nil +} + +func hashValue(values []interface{}, name string) (common.Hash, error) { + if len(values) != 1 { + return common.Hash{}, fmt.Errorf("%s returned an unexpected value count", name) + } + switch value := values[0].(type) { + case [32]byte: + return common.BytesToHash(value[:]), nil + case common.Hash: + return value, nil + case []byte: + if len(value) != 32 { + return common.Hash{}, fmt.Errorf("%s is not bytes32", name) + } + return common.BytesToHash(value), nil + default: + return common.Hash{}, fmt.Errorf("%s has unexpected type %T", name, values[0]) + } +} + +func addressValue(values []interface{}, name string) (common.Address, error) { + if len(values) != 1 { + return common.Address{}, fmt.Errorf("%s returned an unexpected value count", name) + } + switch value := values[0].(type) { + case common.Address: + return value, nil + case [20]byte: + return common.BytesToAddress(value[:]), nil + default: + return common.Address{}, fmt.Errorf("%s has unexpected type %T", name, values[0]) + } +} + +func uint64Value(value interface{}, name string) (uint64, error) { + switch typed := value.(type) { + case uint8: + return uint64(typed), nil + case uint16: + return uint64(typed), nil + case uint32: + return uint64(typed), nil + case uint64: + return typed, nil + case *big.Int: + if typed.Sign() < 0 || !typed.IsUint64() { + return 0, fmt.Errorf("%s is outside uint64", name) + } + return typed.Uint64(), nil + default: + return 0, fmt.Errorf("%s has unexpected type %T", name, value) + } +} + +func writeEvidence(path string, evidence map[string]any) error { + data, err := json.MarshalIndent(evidence, "", " ") + if err != nil { + return fmt.Errorf("encode evidence: %w", err) + } + data = append(data, '\n') + if err := os.WriteFile(path, data, 0600); err != nil { + return fmt.Errorf("write evidence: %w", err) + } + fmt.Printf("verification evidence written to %s\n", path) + return nil +} diff --git a/tools/cmd/verify-allocation/main_test.go b/tools/cmd/verify-allocation/main_test.go new file mode 100644 index 0000000..236fb0c --- /dev/null +++ b/tools/cmd/verify-allocation/main_test.go @@ -0,0 +1,54 @@ +package main + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +func TestAllocationResultDigestIsStableAndBoundToTerms(t *testing.T) { + result := allocationResult{ + Success: true, + SelectedProviders: []string{"0x0000000000000000000000000000000000000001"}, + AllocatedCapacity: []string{"250"}, + AcceptedFeeBps: []uint32{610}, + TermsCommitments: []string{"0x0100000000000000000000000000000000000000000000000000000000000000"}, + RoundExpiry: 12345, + } + extensionID := common.HexToHash("0x01") + roundID := common.HexToHash("0x02") + rootID := common.HexToHash("0x03") + + first, err := allocationResultDigest(result, extensionID, roundID, rootID) + if err != nil { + t.Fatal(err) + } + second, err := allocationResultDigest(result, extensionID, roundID, rootID) + if err != nil { + t.Fatal(err) + } + if first != second { + t.Fatalf("digest is not stable: %s != %s", first.Hex(), second.Hex()) + } + + result.TermsCommitments[0] = "0x0200000000000000000000000000000000000000000000000000000000000000" + changed, err := allocationResultDigest(result, extensionID, roundID, rootID) + if err != nil { + t.Fatal(err) + } + if first == changed { + t.Fatal("digest did not bind terms commitment") + } +} + +func TestParseBytes32RejectsMalformedValues(t *testing.T) { + if _, err := parseBytes32("0x01", "digest", false); err == nil { + t.Fatal("short bytes32 value accepted") + } + if _, err := parseBytes32("0x0000000000000000000000000000000000000000000000000000000000000000", "digest", false); err == nil { + t.Fatal("zero digest accepted") + } + if _, err := parseBytes32("0x0000000000000000000000000000000000000000000000000000000000000000", "terms", true); err != nil { + t.Fatalf("zero terms commitment rejected: %v", err) + } +} From a1449886e792595874e96bba24f5437470365ca6 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 12:09:22 +0100 Subject: [PATCH 003/107] fix: preserve and verify FCC action evidence --- docs/getting-started.md | 14 +-- tools/cmd/run-test/main.go | 51 ++++++++--- tools/cmd/verify-allocation/main.go | 129 +++++++++++++++++++++++++-- tools/pkg/fccutils/tee_calls.go | 40 ++++++++- tools/pkg/fccutils/tee_calls_test.go | 83 +++++++++++++++++ 5 files changed, 289 insertions(+), 28 deletions(-) create mode 100644 tools/pkg/fccutils/tee_calls_test.go diff --git a/docs/getting-started.md b/docs/getting-started.md index 222639c..01d50d4 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -98,19 +98,19 @@ register extension → start official proxy/TEE scaffold → submit encrypted qu → finalize encrypted round → verify result offchain → mark digest → materialize children ~~~ -The runner writes the raw successful FCC allocation response with `-out`. -The verifier then checks the response against the deployed facility allocation -verifier boundary: extension id, open round, root binding, target capacity, -fee limits, eligible providers, and the canonical result digest. It is -read-only unless `-mark` is supplied: +The runner writes a signed FCC action-evidence envelope with `-out`. +The verifier rechecks the TEE action signature, proxy-signed TEE identity, +active machine binding, facility extension id, open round, root binding, +target capacity, fee limits, eligible providers, and canonical result digest. +It is read-only unless `-mark` is supplied: ~~~bash cd tools -go run ./cmd/verify-allocation -c https://coston2-api.flare.network/ext/C/rpc -facility 0x... -result ../evidence/allocation.json -extensionId 0x... -roundId 0x... -rootAccordId 0x... -out ../evidence/allocation-verification.json +go run ./cmd/verify-allocation -c https://coston2-api.flare.network/ext/C/rpc -facility 0x... -teeRegistry 0x... -result ../evidence/allocation.json -extensionId 0x... -roundId 0x... -rootAccordId 0x... -out ../evidence/allocation-verification.json # Only after the local checks pass, with DEPLOYMENT_PRIVATE_KEY set to # the facility allocationVerifier address: -go run ./cmd/verify-allocation -c https://coston2-api.flare.network/ext/C/rpc -facility 0x... -result ../evidence/allocation.json -extensionId 0x... -roundId 0x... -rootAccordId 0x... -mark -out ../evidence/allocation-verification.json +go run ./cmd/verify-allocation -c https://coston2-api.flare.network/ext/C/rpc -facility 0x... -teeRegistry 0x... -result ../evidence/allocation.json -extensionId 0x... -roundId 0x... -rootAccordId 0x... -mark -out ../evidence/allocation-verification.json ~~~ No live FCC or facility deployment is claimed by local tests alone. See diff --git a/tools/cmd/run-test/main.go b/tools/cmd/run-test/main.go index 844c1d1..c8ef144 100644 --- a/tools/cmd/run-test/main.go +++ b/tools/cmd/run-test/main.go @@ -22,6 +22,15 @@ import ( "github.com/pkg/errors" ) +type actionEvidence struct { + InstructionID string `json:"instructionId"` + Response *teeTypes.ActionResponse `json:"response"` + TeeInfo *teeTypes.SignedTeeInfoResponse `json:"teeInfo"` + TeeID string `json:"teeId"` + ProxyID string `json:"proxyId"` + VerifiedAt string `json:"verifiedAt"` +} + func main() { addressesFile := flag.String("a", configs.AddressesFile, "file with deployed addresses") chainURL := flag.String("c", configs.ChainNodeURL, "chain RPC URL") @@ -60,7 +69,7 @@ func main() { fccutils.FatalWithCause(err) } logger.Infof("SUBMIT_QUOTE sent: instruction=%s tx=%s", instructionID.Hex(), txHash.Hex()) - if _, err := verifyAction(*proxyURL, instructionID); err != nil { + if _, _, _, _, err := verifyAction(*proxyURL, instructionID, s.ChainID.Uint64()); err != nil { fccutils.FatalWithCause(err) } } @@ -78,36 +87,56 @@ func main() { fccutils.FatalWithCause(err) } logger.Infof("FINALIZE_ROUND sent: instruction=%s tx=%s", instructionID.Hex(), txHash.Hex()) - response, err := verifyAction(*proxyURL, instructionID) + response, teeInfo, teeID, proxyID, err := verifyAction(*proxyURL, instructionID, s.ChainID.Uint64()) if err != nil { fccutils.FatalWithCause(err) } if *outFile != "" { - if err := os.WriteFile(*outFile, response.Result.Data, 0600); err != nil { + evidence, err := json.MarshalIndent(actionEvidence{ + InstructionID: instructionID.Hex(), + Response: response, + TeeInfo: teeInfo, + TeeID: teeID.Hex(), + ProxyID: proxyID.Hex(), + VerifiedAt: time.Now().UTC().Format(time.RFC3339), + }, "", " ") + if err != nil { + fccutils.FatalWithCause(errors.Errorf("encode action evidence: %s", err)) + } + evidence = append(evidence, '\n') + if err := os.WriteFile(*outFile, evidence, 0600); err != nil { fccutils.FatalWithCause(err) } - logger.Infof("verified FCC allocation response written to %s", *outFile) + logger.Infof("signed FCC allocation evidence written to %s", *outFile) } } -func verifyAction(proxyURL string, instructionID common.Hash) (*teeTypes.ActionResponse, error) { +func verifyAction(proxyURL string, instructionID common.Hash, expectedChainID uint64) (*teeTypes.ActionResponse, *teeTypes.SignedTeeInfoResponse, common.Address, common.Address, error) { response, err := fccutils.ActionResult(proxyURL, instructionID) if err != nil { - return nil, err + return nil, nil, common.Address{}, common.Address{}, err } if response.Result.Status == 2 { - return nil, errors.New("FCC action remained pending after polling") + return nil, nil, common.Address{}, common.Address{}, errors.New("FCC action remained pending after polling") } if response.Result.Status == 0 { - return nil, errors.Errorf("FCC action failed: %s", response.Result.Log) + return nil, nil, common.Address{}, common.Address{}, errors.Errorf("FCC action failed: %s", response.Result.Log) } if len(response.Result.Data) == 0 { - return nil, errors.New("FCC action returned no data") + return nil, nil, common.Address{}, common.Address{}, errors.New("FCC action returned no data") + } + teeInfo, err := fccutils.TeeInfo(proxyURL) + if err != nil { + return nil, nil, common.Address{}, common.Address{}, errors.Errorf("fetch signed TEE info: %s", err) + } + teeID, proxyID, err := fccutils.VerifyActionResponse(response, teeInfo, expectedChainID) + if err != nil { + return nil, nil, common.Address{}, common.Address{}, err } var probe map[string]any if err := json.Unmarshal(response.Result.Data, &probe); err != nil { - return nil, errors.Errorf("FCC result data is not JSON: %s", err) + return nil, nil, common.Address{}, common.Address{}, errors.Errorf("FCC result data is not JSON: %s", err) } logger.Infof("FCC action verified at %s: %d fields", time.Now().UTC().Format(time.RFC3339), len(probe)) - return response, nil + return response, teeInfo, teeID, proxyID, nil } diff --git a/tools/cmd/verify-allocation/main.go b/tools/cmd/verify-allocation/main.go index 2cab0d9..7376e0d 100644 --- a/tools/cmd/verify-allocation/main.go +++ b/tools/cmd/verify-allocation/main.go @@ -20,8 +20,10 @@ import ( coretypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" + teeTypes "github.com/flare-foundation/tee-node/pkg/types" "concord/tools/pkg/configs" + "concord/tools/pkg/fccutils" "concord/tools/pkg/support" ) @@ -38,6 +40,15 @@ type allocationResult struct { ResultDigest string `json:"resultDigest"` } +type actionEvidence struct { + InstructionID string `json:"instructionId"` + Response *teeTypes.ActionResponse `json:"response"` + TeeInfo *teeTypes.SignedTeeInfoResponse `json:"teeInfo"` + TeeID string `json:"teeId"` + ProxyID string `json:"proxyId"` + VerifiedAt string `json:"verifiedAt"` +} + const facilityABIJSON = `[ { "inputs": [], @@ -86,6 +97,19 @@ const facilityABIJSON = `[ } ]` +const teeRegistryABIJSON = `[ + { + "inputs": [{"internalType": "uint256", "name": "_extensionId", "type": "uint256"}], + "name": "getActiveTeeMachines", + "outputs": [ + {"internalType": "address[]", "name": "_teeIds", "type": "address[]"}, + {"internalType": "string[]", "name": "_urls", "type": "string[]"} + ], + "stateMutability": "view", + "type": "function" + } +]` + func main() { if err := run(); err != nil { fmt.Fprintf(os.Stderr, "verify-allocation: %v\n", err) @@ -100,6 +124,7 @@ func run() error { extensionFlag := flag.String("extensionId", "", "expected FCC extension id") roundFlag := flag.String("roundId", "", "expected Makkari round id") rootFlag := flag.String("rootAccordId", "", "expected root Accord id") + teeRegistryFlag := flag.String("teeRegistry", "", "FlareTeeManager diamond address for active-machine verification") mark := flag.Bool("mark", false, "broadcast markAllocationVerified after local verification") evidenceFile := flag.String("out", "", "optional JSON evidence output path") flag.Parse() @@ -128,9 +153,9 @@ func run() error { if err != nil { return fmt.Errorf("read result: %w", err) } - var result allocationResult - if err := json.Unmarshal(payload, &result); err != nil { - return fmt.Errorf("decode allocation result: %w", err) + result, signedEvidence, err := decodeEvidence(payload) + if err != nil { + return err } facilityABI, err := abi.JSON(strings.NewReader(facilityABIJSON)) @@ -145,6 +170,24 @@ func run() error { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() + chainID, err := client.ChainID(ctx) + if err != nil { + return fmt.Errorf("read chain id: %w", err) + } + var evidenceTeeID, evidenceProxyID common.Address + if signedEvidence != nil { + evidenceTeeID, evidenceProxyID, err = fccutils.VerifyActionResponse( + signedEvidence.Response, signedEvidence.TeeInfo, chainID.Uint64(), + ) + if err != nil { + return err + } + if signedEvidence.TeeInfo.MachineData.ExtensionID != expectedExtension { + return fmt.Errorf("TEE machine evidence is bound to extension %s, not %s", signedEvidence.TeeInfo.MachineData.ExtensionID.Hex(), expectedExtension.Hex()) + } + } else if *mark { + return fmt.Errorf("-mark requires signed action evidence from run-test; raw allocation JSON cannot authorize materialization") + } digest, verifier, err := verifyResult( ctx, client, @@ -170,6 +213,13 @@ func run() error { "verifier": verifier.Hex(), "verifiedAt": time.Now().UTC().Format(time.RFC3339), } + if signedEvidence != nil { + evidence["signedAction"] = true + evidence["teeId"] = evidenceTeeID.Hex() + evidence["proxyId"] = evidenceProxyID.Hex() + } else { + evidence["signedAction"] = false + } if !*mark { if *evidenceFile != "" { if err := writeEvidence(*evidenceFile, evidence); err != nil { @@ -179,6 +229,15 @@ func run() error { fmt.Println("not broadcast: rerun with -mark to call markAllocationVerified") return nil } + if *teeRegistryFlag == "" || !common.IsHexAddress(*teeRegistryFlag) || common.HexToAddress(*teeRegistryFlag) == (common.Address{}) { + return fmt.Errorf("-mark requires a non-zero -teeRegistry address for active-machine verification") + } + if signedEvidence == nil { + return fmt.Errorf("-mark requires signed action evidence from run-test") + } + if err := verifyActiveMachine(ctx, client, common.HexToAddress(*teeRegistryFlag), expectedExtension, evidenceTeeID); err != nil { + return err + } privateKey, err := support.DefaultPrivateKey() if err != nil { @@ -188,10 +247,6 @@ func run() error { if signer != verifier { return fmt.Errorf("DEPLOYMENT_PRIVATE_KEY resolves to %s, but facility allocationVerifier is %s", signer.Hex(), verifier.Hex()) } - chainID, err := client.ChainID(ctx) - if err != nil { - return fmt.Errorf("read chain id: %w", err) - } opts, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID) if err != nil { return fmt.Errorf("create transactor: %w", err) @@ -223,6 +278,66 @@ func run() error { return nil } +func decodeEvidence(payload []byte) (allocationResult, *actionEvidence, error) { + var envelope actionEvidence + if err := json.Unmarshal(payload, &envelope); err == nil && envelope.Response != nil { + if envelope.TeeInfo == nil { + return allocationResult{}, nil, fmt.Errorf("signed action evidence is missing teeInfo") + } + if envelope.Response.Result.Status != 1 { + return allocationResult{}, nil, fmt.Errorf("signed FCC action status is %d", envelope.Response.Result.Status) + } + var result allocationResult + if err := json.Unmarshal(envelope.Response.Result.Data, &result); err != nil { + return allocationResult{}, nil, fmt.Errorf("decode allocation data from signed action evidence: %w", err) + } + return result, &envelope, nil + } + + var result allocationResult + if err := json.Unmarshal(payload, &result); err != nil { + return allocationResult{}, nil, fmt.Errorf("decode allocation result: %w", err) + } + return result, nil, nil +} + +func verifyActiveMachine( + ctx context.Context, + client *ethclient.Client, + registry common.Address, + extensionID common.Hash, + teeID common.Address, +) error { + registryABI, err := abi.JSON(strings.NewReader(teeRegistryABIJSON)) + if err != nil { + return fmt.Errorf("parse TEE registry ABI: %w", err) + } + values, err := callMethod( + ctx, + client, + registry, + registryABI, + "getActiveTeeMachines", + new(big.Int).SetBytes(extensionID.Bytes()), + ) + if err != nil { + return err + } + if len(values) != 2 { + return fmt.Errorf("getActiveTeeMachines returned %d values, expected 2", len(values)) + } + teeIDs, ok := values[0].([]common.Address) + if !ok { + return fmt.Errorf("getActiveTeeMachines returned unexpected tee id type %T", values[0]) + } + for _, activeID := range teeIDs { + if activeID == teeID { + return nil + } + } + return fmt.Errorf("TEE %s is not active for extension %s", teeID.Hex(), extensionID.Hex()) +} + func verifyResult( ctx context.Context, client *ethclient.Client, diff --git a/tools/pkg/fccutils/tee_calls.go b/tools/pkg/fccutils/tee_calls.go index 78fabdb..38eb6d2 100644 --- a/tools/pkg/fccutils/tee_calls.go +++ b/tools/pkg/fccutils/tee_calls.go @@ -10,12 +10,12 @@ import ( "github.com/flare-foundation/go-flare-common/pkg/logger" csigning "github.com/flare-foundation/go-flare-common/pkg/signing" - "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/flare-foundation/go-flare-common/pkg/tee/attestation/googlecloud" "github.com/flare-foundation/tee-node/pkg/attestation" "github.com/flare-foundation/tee-node/pkg/types" + teeutils "github.com/flare-foundation/tee-node/pkg/utils" "github.com/pkg/errors" ) @@ -77,11 +77,10 @@ func TeeProxyId(teeInfo *types.SignedTeeInfoResponse) (common.Address, common.Ad if err != nil { return common.Address{}, common.Address{}, errors.Errorf("%s", err) } - proxyPubKey, err := crypto.SigToPub(accounts.TextHash(infoSignHash[:]), teeInfo.ProxySignature) + proxyID, err := teeutils.SignatureToSignersAddress(infoSignHash[:], teeInfo.ProxySignature) if err != nil { return common.Address{}, common.Address{}, errors.Errorf("%s", err) } - proxyID := crypto.PubkeyToAddress(*proxyPubKey) return teeID, proxyID, nil } @@ -114,6 +113,41 @@ func ActionResult(nodeURL string, actionID common.Hash) (*types.ActionResponse, return &response, nil } +// VerifyActionResponse checks the official TEE action-result signature and +// the proxy-signed TEE identity against the expected chain id. The returned +// tee and proxy ids are evidence that can be checked against Flare registries. +func VerifyActionResponse( + response *types.ActionResponse, + teeInfo *types.SignedTeeInfoResponse, + expectedChainID uint64, +) (common.Address, common.Address, error) { + if response == nil || teeInfo == nil { + return common.Address{}, common.Address{}, errors.New("action response and TEE info are required") + } + if teeInfo.TeeInfo.ChainID != expectedChainID { + return common.Address{}, common.Address{}, errors.Errorf( + "TEE info chain id %d does not match expected chain id %d", + teeInfo.TeeInfo.ChainID, expectedChainID, + ) + } + teeID, proxyID, err := TeeProxyId(teeInfo) + if err != nil { + return common.Address{}, common.Address{}, errors.Errorf("TEE identity verification failed: %s", err) + } + signHash, err := csigning.NewPayload( + csigning.TEEActionResult, + expectedChainID, + common.BytesToHash(response.Result.Hash()), + ).Hash() + if err != nil { + return common.Address{}, common.Address{}, errors.Errorf("action-result signing hash failed: %s", err) + } + if err := teeutils.VerifySignature(signHash[:], response.Signature, teeID); err != nil { + return common.Address{}, common.Address{}, errors.Errorf("TEE action-result signature verification failed: %s", err) + } + return teeID, proxyID, nil +} + func SetProxyUrl(configurationPort int, proxyPort int) error { url := fmt.Sprintf("http://localhost:%d", proxyPort) request := types.ConfigureProxyURLRequest{ diff --git a/tools/pkg/fccutils/tee_calls_test.go b/tools/pkg/fccutils/tee_calls_test.go new file mode 100644 index 0000000..8506fd6 --- /dev/null +++ b/tools/pkg/fccutils/tee_calls_test.go @@ -0,0 +1,83 @@ +package fccutils + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + csigning "github.com/flare-foundation/go-flare-common/pkg/signing" + teeTypes "github.com/flare-foundation/tee-node/pkg/types" + teeutils "github.com/flare-foundation/tee-node/pkg/utils" +) + +func TestVerifyActionResponseChecksTEEAndProxySignatures(t *testing.T) { + teeKey, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + proxyKey, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + teeInfo := &teeTypes.SignedTeeInfoResponse{ + TeeInfoResponse: teeTypes.TeeInfoResponse{ + TeeInfo: teeTypes.TeeInfo{ + PublicKey: teeTypes.PublicKey{ + X: common.BytesToHash(teeKey.PublicKey.X.Bytes()), + Y: common.BytesToHash(teeKey.PublicKey.Y.Bytes()), + }, + ChainID: 114, + }, + }, + } + infoHash, err := teeInfo.TeeInfo.Hash() + if err != nil { + t.Fatal(err) + } + proxySignHash, err := csigning.NewPayload( + csigning.ProxyTeeInfo, + teeInfo.TeeInfo.ChainID, + common.BytesToHash(infoHash), + ).Hash() + if err != nil { + t.Fatal(err) + } + teeInfo.ProxySignature, err = teeutils.Sign(proxySignHash[:], proxyKey) + if err != nil { + t.Fatal(err) + } + + result := teeTypes.ActionResult{ + ID: common.HexToHash("0x01"), + Data: []byte("{\"success\":true}"), + } + actionSignHash, err := csigning.NewPayload( + csigning.TEEActionResult, + teeInfo.TeeInfo.ChainID, + common.BytesToHash(result.Hash()), + ).Hash() + if err != nil { + t.Fatal(err) + } + signature, err := teeutils.Sign(actionSignHash[:], teeKey) + if err != nil { + t.Fatal(err) + } + response := &teeTypes.ActionResponse{Result: result, Signature: signature} + + teeID, proxyID, err := VerifyActionResponse(response, teeInfo, 114) + if err != nil { + t.Fatal(err) + } + if teeID != crypto.PubkeyToAddress(teeKey.PublicKey) { + t.Fatalf("TEE id mismatch: got %s", teeID.Hex()) + } + if proxyID != crypto.PubkeyToAddress(proxyKey.PublicKey) { + t.Fatalf("proxy id mismatch: got %s", proxyID.Hex()) + } + + response.Signature[0] ^= 1 + if _, _, err := VerifyActionResponse(response, teeInfo, 114); err == nil { + t.Fatal("tampered action signature accepted") + } +} From ae206a3dc9fe46989179f28ba554fe3d985208e4 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 12:12:21 +0100 Subject: [PATCH 004/107] fix: enforce zero-fee round bounds --- go/internal/extension/extension.go | 18 +++++++++++++----- go/internal/extension/extension_test.go | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/go/internal/extension/extension.go b/go/internal/extension/extension.go index ff34ed1..da77db3 100644 --- a/go/internal/extension/extension.go +++ b/go/internal/extension/extension.go @@ -118,7 +118,7 @@ func (e *Extension) processSubmitQuote(action teetypes.Action, df *instruction.D if err := decodeStrict(payload, &req); err != nil { return marshalActionResult(action, df, nil, 0, fmt.Errorf("decoding quote: %w", err)) } - if err := validateQuote(req, uint64(time.Now().Unix()), 0, 0, nil); err != nil { + if err := validateQuote(req, uint64(time.Now().Unix()), 0, false, 0, false, nil); err != nil { return marshalActionResult(action, df, nil, 0, err) } @@ -317,7 +317,15 @@ func sameQuoteSet(left, right map[string]string) bool { return true } -func validateQuote(q types.QuoteRequest, now uint64, maxFee uint32, roundExpiry uint64, eligible map[string]bool) error { +func validateQuote( + q types.QuoteRequest, + now uint64, + maxFee uint32, + enforceMaxFee bool, + roundExpiry uint64, + enforceRoundExpiry bool, + eligible map[string]bool, +) error { if !validBytes32(q.RoundID) || !validBytes32(q.RootAccordID) { return fmt.Errorf("roundId and rootAccordId must be non-zero bytes32 values") } @@ -335,10 +343,10 @@ func validateQuote(q types.QuoteRequest, now uint64, maxFee uint32, roundExpiry if q.ValidUntil <= now { return fmt.Errorf("quote is expired") } - if maxFee != 0 && q.FeeBps > maxFee { + if enforceMaxFee && q.FeeBps > maxFee { return fmt.Errorf("quote fee exceeds maxFeeBps") } - if roundExpiry != 0 && q.ValidUntil > roundExpiry { + if enforceRoundExpiry && q.ValidUntil > roundExpiry { return fmt.Errorf("quote validity exceeds round expiry") } if eligible != nil && !eligible[strings.ToLower(common.HexToAddress(q.Provider).Hex())] { @@ -453,7 +461,7 @@ func CoFill(req types.FinalizeRoundRequest) (types.FinalizeRoundResponse, error) } quotes := append([]types.QuoteRequest(nil), req.Quotes...) for i := range quotes { - if err := validateQuote(quotes[i], req.EvaluationTime, req.MaxFeeBps, req.RoundExpiry, eligible); err != nil { + if err := validateQuote(quotes[i], req.EvaluationTime, req.MaxFeeBps, true, req.RoundExpiry, true, eligible); err != nil { return types.FinalizeRoundResponse{}, fmt.Errorf("quote %d rejected: %w", i, err) } if !strings.EqualFold(quotes[i].RoundID, req.RoundID) || !strings.EqualFold(quotes[i].RootAccordID, req.RootAccordID) { diff --git a/go/internal/extension/extension_test.go b/go/internal/extension/extension_test.go index 5583bfc..dd684cb 100644 --- a/go/internal/extension/extension_test.go +++ b/go/internal/extension/extension_test.go @@ -132,6 +132,26 @@ func TestCoFillRejectsInvalidSignatureAndInsufficientCapacity(t *testing.T) { } } +func TestCoFillEnforcesZeroFeeCap(t *testing.T) { + key, _ := crypto.GenerateKey() + provider := crypto.PubkeyToAddress(key.PublicKey).Hex() + quote := quoteFixture(t, key, provider, 1, "100") + req := types.FinalizeRoundRequest{ + ExtensionID: "0x0300000000000000000000000000000000000000000000000000000000000000", + RoundID: quote.RoundID, + RootAccordID: quote.RootAccordID, + TargetCapacity: "100", + MaxFeeBps: 0, + RoundExpiry: uint64(time.Now().Unix()) + 7200, + EvaluationTime: uint64(time.Now().Unix()), + EligibleProviders: []string{provider}, + Quotes: []types.QuoteRequest{quote}, + } + if _, err := CoFill(req); err == nil || !strings.Contains(err.Error(), "exceeds maxFeeBps") { + t.Fatalf("non-zero fee passed a zero-fee round: %v", err) + } +} + func TestActionRequiresTEEDecryption(t *testing.T) { e := New(0, 1) status, body := e.processAction(buildAction(config.OPCommandSubmitQuote, []byte("not-plaintext"))) From 0ae7025c8a760b9a3060af3b8303e70509083f05 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 12:14:23 +0100 Subject: [PATCH 005/107] docs: align Concord status with evidence boundary --- docs/CONCORD_STATUS.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index fa9736d..a8d6d8f 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -9,7 +9,13 @@ Updated 2026-08-12. - Foundry flow tests pass locally, including the two-child draw and repayment lifecycle. - The Go Concord extension compiles and tests signed quotes and deterministic - CoFill inside the official FCE scaffold structure. + CoFill inside the official FCE scaffold structure, including exact zero-fee + round bounds. +- `tools/cmd/run-test` preserves the complete signed FCC action response and + signed TEE-info envelope instead of reducing the result to unverified JSON. +- `tools/cmd/verify-allocation` independently checks the action signature, + proxy-signed TEE identity, chain/extension binding, facility round/digest, + and—before `-mark`—the active TEE machine for the extension. - Deployment tooling generates a binding for ConcordInstructionSender and has no embedded private-key fallback. - Coston2 asset resolution is centralized in config/networks/coston2.json and @@ -21,7 +27,10 @@ Updated 2026-08-12. - No funded Coston2 treasury/provider accounts, FCC registration, instruction IDs, settlement receipts or end-to-end explorer evidence are recorded yet. - The configured allocationVerifier is an explicit offchain integration - boundary. A local test actor is not a live FCC proof. + boundary. A local test actor is not a live FCC proof. The `-mark` path is + deliberately restricted to signed action evidence plus an active machine + lookup; it has not been run against a deployed Concord facility in this + checkpoint. ## Privacy truth @@ -46,7 +55,8 @@ affected configuration and record the contradiction before continuing. ## Deployment evidence template -When the live slice is run, append: +When the live slice is run, append the evidence below. Until then, leave the +deployment fields empty rather than inferring them from local tests: ~~~text Network: From 57ad3c1ed61d115d06c0215dcfadd18486595262 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 12:28:58 +0100 Subject: [PATCH 006/107] fix: make Coston2 runner Concord-aware --- .gitignore | 1 + docs/testing-against-coston2.md | 103 ++++++++++++++++++++ scripts/test.sh | 161 ++++++++++++++++++++++++++++++++ 3 files changed, 265 insertions(+) create mode 100644 docs/testing-against-coston2.md create mode 100644 scripts/test.sh diff --git a/.gitignore b/.gitignore index d4a689b..cc512d9 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ cache/ # Uploaded artifacts and local evidence must never enter the source tree. upload/ +evidence/ *.log # go.work is generated on demand by start-services.sh for USE_LOCAL_SIBLINGS=1 diff --git a/docs/testing-against-coston2.md b/docs/testing-against-coston2.md new file mode 100644 index 0000000..6ce34dc --- /dev/null +++ b/docs/testing-against-coston2.md @@ -0,0 +1,103 @@ +# Testing against a Coston2 deployment + +For exercising an extension that is already deployed and registered on Coston2 — +either on a Confidential Space VM, or locally with a public tunnel. To deploy one +first, see [deployment-steps.md](deployment-steps.md). + +## What you need + +| Need | Note | +|---|---| +| `.env.coston2` | filled in; `use-chain.sh coston2` activates it | +| A funded Coston2 key | sends the test instructions; get C2FLR from the [faucet](https://faucet.flare.network/coston2) | +| `config/coston2/deployed-addresses.json` | the `FlareTeeManager` diamond and friends | +| `config/proxy/extension_proxy.coston2.docker.toml` | gitignored — copy the `.example` and fill in `[db]` | +| A reachable `EXT_PROXY_URL` | the VM's URL, or a tunnel if the proxy runs locally | + +## Confirm the deployment is live + +```bash +curl -s "$EXT_PROXY_URL/info" | jq '{extensionId, codeHash, platform}' +``` + +| Field | Expect | +|---|---| +| `extensionId` | matches `EXTENSION_ID` in `config/extension.env` | +| `codeHash` | the hash registered on-chain — `0x194844cf…` means simulated, not real hardware | +| `platform` | `GCP_AMD_SEV` on real hardware, `TEST_PLATFORM` when simulated | + +Then check the on-chain side: + +```bash +cd tools +go run ./cmd/query-tee -ext -rpc "$CHAIN_URL" +go run ./cmd/verify-deploy -a ../config/coston2/deployed-addresses.json -c "$CHAIN_URL" +``` + +`query-tee` lists the TEE machines registered for the extension. **More than one +active machine is a problem** — instructions are load-balanced across them, so a +stale one swallows roughly half your requests. See +[deployment-steps.md](deployment-steps.md). + +## Run the Concord flow + +```bash +./scripts/use-chain.sh coston2 +CONCORD_QUOTES=fixtures/quote-a.json,fixtures/quote-b.json,fixtures/quote-c.json \ +CONCORD_FINALIZE=fixtures/finalize-round.json \ +CONCORD_EVIDENCE_OUT=evidence/fcc-action.json \ +./scripts/test.sh +``` + +The quote files must contain signed Concord `QuoteRequest` payloads. The +finalization file must contain a `FinalizeRoundRequest` bound to the live +extension, root Accord, Makkari round, target, expiry, and eligible providers. +The runner submits each quote through encrypted `SUBMIT_QUOTE` instructions, +then submits `FINALIZE_ROUND` and writes the complete signed FCC evidence +envelope to `CONCORD_EVIDENCE_OUT`. + +Verify that envelope against the deployed CapitalFacility before materializing +children: + +```bash +cd tools +go run ./cmd/verify-allocation \ + -c "$CHAIN_URL" \ + -facility "$CAPITAL_FACILITY" \ + -teeRegistry "$TEE_REGISTRY" \ + -result ../evidence/fcc-action.json \ + -extensionId "$EXTENSION_ID" \ + -roundId "$ROUND_ID" \ + -rootAccordId "$ROOT_ACCORD_ID" \ + -out ../evidence/allocation-verification.json +``` + +The verifier is read-only by default. Use `-mark` only after the signed +action, active-machine, facility-binding, and digest checks pass; the caller +must be the facility's configured `allocationVerifier`. + +Counters are in memory, so the numbers restart at 1 after any TEE relaunch. + +## Testing a local proxy against Coston2 + +The proxy must be publicly reachable for FTDC data providers to answer it. Start +the tunnel and let the scripts wire the URL in: + +```bash +./scripts/full-setup.sh --chain coston2 --tunnel --test +``` + +That writes the tunnel URL into `.env` as `EXT_PROXY_URL`, so `post-build.sh` and +`test.sh` pick it up. Details in [cloudflared.md](cloudflared.md). + +## If something's blocked + +| Symptom | Cause | +|---|---| +| `pollAction` timeout, `/action/result` 404 | multiple active TEE machines; pause the stale ones | +| `Verification.ChallengeExpired` | re-registration without `-command rRap` | +| `no round` / 404 from the FTDC proxy | proxy signing policy out of sync with the on-chain reward epoch; `register-tee` pre-flights this | +| `signature must be 65 bytes, got 0` | `CHAIN_ID` unset on the node | +| `InvalidTeePublicKeyOrSignature` | node `CHAIN_ID`, proxy `chain_id` and the registry disagree — all three must say 114 | +| Instructions never arrive | `EXT_PROXY_URL` not reachable from outside, or a rotated tunnel URL left stale in `.env` | +| `Extension ID already set.` | `setExtensionId()` is one-shot; a redeploy is the only reset | diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100644 index 0000000..198c8f6 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# test.sh — Send Concord instructions on-chain, poll for results, and validate correctness. +# +# Run this AFTER post-build.sh (TEE machine must be registered and running). +# +# Inputs (env vars): +# EXT_PROXY_URL — extension proxy URL (auto-detected: :6674 for Docker, :6664 for local) +# CHAIN_URL — chain RPC URL (default: http://127.0.0.1:8545) +# ADDRESSES_FILE — path to deployed-addresses.json (auto-detected if unset) +# INSTRUCTION_SENDER — deployed InstructionSender address (from config/extension.env) +# CONCORD_QUOTES — comma-separated signed QuoteRequest JSON files +# CONCORD_FINALIZE — FinalizeRoundRequest JSON file +# CONCORD_EVIDENCE_OUT — signed FCC evidence output path (default: evidence/fcc-action.json) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'; NC='\033[0m' +log() { echo -e "${GREEN}[test]${NC} $*"; } +step() { echo -e "\n${CYAN}=== Step $1: $2 ===${NC}"; } +die() { echo -e "${RED}[test] ERROR:${NC} $*" >&2; exit 1; } + +# --- Load .env from project root (if present) --- +if [[ -f "$PROJECT_DIR/.env" ]]; then + set -a + source "$PROJECT_DIR/.env" + set +a +fi + +# --- Source extension config (generated by pre-build.sh) --- +CONFIG_FILE="$PROJECT_DIR/config/extension.env" +if [[ -f "$CONFIG_FILE" ]]; then + # shellcheck disable=SC1090 + source "$CONFIG_FILE" + log "Loaded config from $CONFIG_FILE" +fi + +# Auto-detect proxy port: use Docker port (6674) if ext-proxy container is running, +# otherwise fall back to local Go process port (6664). +if [[ -z "${EXT_PROXY_URL:-}" ]]; then + if docker compose -f "$PROJECT_DIR/docker-compose.yaml" ps ext-proxy --status running 2>/dev/null | grep -q ext-proxy; then + EXT_PROXY_URL="http://localhost:6674" + else + EXT_PROXY_URL="http://localhost:6664" + fi +fi +CHAIN_URL="${CHAIN_URL:-http://127.0.0.1:8545}" +ADDRESSES_FILE="${ADDRESSES_FILE:-}" +# Resolve relative paths against PROJECT_DIR (not caller's cwd) +if [[ -n "$ADDRESSES_FILE" && "$ADDRESSES_FILE" != /* ]]; then + ADDRESSES_FILE="$PROJECT_DIR/$ADDRESSES_FILE" +fi +INSTRUCTION_SENDER="${INSTRUCTION_SENDER:-}" +CONCORD_QUOTES="${CONCORD_QUOTES:-}" +CONCORD_FINALIZE="${CONCORD_FINALIZE:-}" +CONCORD_EVIDENCE_OUT="${CONCORD_EVIDENCE_OUT:-$PROJECT_DIR/evidence/fcc-action.json}" + +[[ -n "$INSTRUCTION_SENDER" ]] || die "INSTRUCTION_SENDER not set. Run pre-build.sh first or set it manually." +[[ -n "$CONCORD_QUOTES" ]] || die "CONCORD_QUOTES must contain comma-separated signed quote JSON files" +[[ -n "$CONCORD_FINALIZE" ]] || die "CONCORD_FINALIZE must point to a FinalizeRoundRequest JSON file" + +resolve_file() { + local value="$1" + local label="$2" + if [[ "$value" != /* ]]; then + value="$PROJECT_DIR/$value" + fi + [[ -f "$value" ]] || die "$label file not found: $value" + printf '%s/%s\n' "$(cd "$(dirname "$value")" && pwd)" "$(basename "$value")" +} + +resolve_quote_files() { + local raw="$1" + local item resolved joined="" + local -a items=() + IFS=',' read -r -a items <<< "$raw" + for item in "${items[@]}"; do + item="${item#"${item%%[![:space:]]*}"}" + item="${item%"${item##*[![:space:]]}"}" + [[ -n "$item" ]] || die "CONCORD_QUOTES contains an empty path" + resolved="$(resolve_file "$item" "quote")" + if [[ -n "$joined" ]]; then + joined+="," + fi + joined+="$resolved" + done + printf '%s' "$joined" +} + +QUOTE_FILES="$(resolve_quote_files "$CONCORD_QUOTES")" +FINALIZE_FILE="$(resolve_file "$CONCORD_FINALIZE" "finalize")" +if [[ "$CONCORD_EVIDENCE_OUT" != /* ]]; then + CONCORD_EVIDENCE_OUT="$PROJECT_DIR/$CONCORD_EVIDENCE_OUT" +fi +mkdir -p "$(dirname "$CONCORD_EVIDENCE_OUT")" + +# --- Auto-detect addresses file --- +LOCAL_MODE="${LOCAL_MODE:-true}" +if [[ -z "$ADDRESSES_FILE" ]]; then + if [[ "$LOCAL_MODE" != "true" ]]; then + # Non-local mode: use coston2 deployed addresses + candidate="$PROJECT_DIR/config/coston2/deployed-addresses.json" + if [[ -f "$candidate" ]]; then + ADDRESSES_FILE="$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate")" + fi + fi + + # Fall back to sim_dump candidates (local devnet) + if [[ -z "$ADDRESSES_FILE" ]]; then + for candidate in \ + "$PROJECT_DIR/../../e2e/docker/sim_dump/deployed-addresses.json" \ + "$PROJECT_DIR/../docker/sim_dump/deployed-addresses.json" \ + "$PROJECT_DIR/../../docker/sim_dump/deployed-addresses.json" \ + "$PROJECT_DIR/../../../docker/sim_dump/deployed-addresses.json"; do + if [[ -f "$candidate" ]]; then + ADDRESSES_FILE="$(cd "$(dirname "$candidate")" && pwd)/$(basename "$candidate")" + break + fi + done + fi + + [[ -n "$ADDRESSES_FILE" ]] || die "Cannot find deployed-addresses.json. Set ADDRESSES_FILE." +fi + +[[ -f "$ADDRESSES_FILE" ]] || die "Addresses file not found: $ADDRESSES_FILE" + +# Resolve to absolute path so it works after cd into tools/ +ADDRESSES_FILE="$(cd "$(dirname "$ADDRESSES_FILE")" && pwd)/$(basename "$ADDRESSES_FILE")" + +log "Extension proxy: $EXT_PROXY_URL" +log "Chain URL: $CHAIN_URL" +log "Addresses file: $ADDRESSES_FILE" +log "InstructionSender: $INSTRUCTION_SENDER" +log "Quote files: $QUOTE_FILES" +log "Finalize payload: $FINALIZE_FILE" +log "FCC evidence: $CONCORD_EVIDENCE_OUT" + +# --- Pre-flight: verify extension proxy is reachable --- +if ! curl -sf -o /dev/null "$EXT_PROXY_URL/info" 2>/dev/null; then + die "Extension proxy not reachable at $EXT_PROXY_URL. Is Docker Compose running? (docker compose up -d)" +fi +log "Extension proxy is reachable" + +# --- Run test --- +step 1 "Run extension tests" +cd "$PROJECT_DIR/tools" +go run ./cmd/run-test \ + -a "$ADDRESSES_FILE" \ + -c "$CHAIN_URL" \ + -p "$EXT_PROXY_URL" \ + -instructionSender "$INSTRUCTION_SENDER" \ + -quotes "$QUOTE_FILES" \ + -finalize "$FINALIZE_FILE" \ + -out "$CONCORD_EVIDENCE_OUT" \ + || die "Test failed" + +echo "" +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN} Tests passed${NC}" +echo -e "${GREEN}========================================${NC}" From 00cf0c51578aa209c953f4c0fe8738486235cd76 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 13:22:01 +0100 Subject: [PATCH 007/107] feat: add shared Concord integration surfaces --- README.md | 22 ++ api/README.md | 20 + api/openapi.yaml | 476 ++++++++++++++++++++++++ docs/cli.md | 33 ++ docs/frontend-map.md | 156 ++++++++ docs/shared-product-contract.md | 189 ++++++++++ frontend/README.md | 13 + scripts/concord | 15 + sdk/typescript/package.json | 21 ++ sdk/typescript/src/index.ts | 272 ++++++++++++++ sdk/typescript/tsconfig.json | 14 + tools/cmd/concord/main.go | 282 ++++++++++++++ tools/pkg/api/server.go | 396 ++++++++++++++++++++ tools/pkg/api/server_test.go | 123 ++++++ tools/pkg/mcpserver/server.go | 259 +++++++++++++ tools/pkg/mcpserver/server_test.go | 38 ++ tools/pkg/readmodel/chain.go | 439 ++++++++++++++++++++++ tools/pkg/readmodel/chain_codec_test.go | 43 +++ tools/pkg/readmodel/model.go | 348 +++++++++++++++++ 19 files changed, 3159 insertions(+) create mode 100644 api/README.md create mode 100644 api/openapi.yaml create mode 100644 docs/cli.md create mode 100644 docs/frontend-map.md create mode 100644 docs/shared-product-contract.md create mode 100644 frontend/README.md create mode 100755 scripts/concord create mode 100644 sdk/typescript/package.json create mode 100644 sdk/typescript/src/index.ts create mode 100644 sdk/typescript/tsconfig.json create mode 100644 tools/cmd/concord/main.go create mode 100644 tools/pkg/api/server.go create mode 100644 tools/pkg/api/server_test.go create mode 100644 tools/pkg/mcpserver/server.go create mode 100644 tools/pkg/mcpserver/server_test.go create mode 100644 tools/pkg/readmodel/chain.go create mode 100644 tools/pkg/readmodel/chain_codec_test.go create mode 100644 tools/pkg/readmodel/model.go diff --git a/README.md b/README.md index bee4552..0cec4cd 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,30 @@ tools/ deployment, FCC verification, and test commands config/ network and proxy configuration templates docs/ product, architecture, truth, and runbooks scripts/ scaffold lifecycle and binding commands +api/ REST/OpenAPI integration contract +sdk/typescript/ typed read and unsigned-intent client +frontend/ frontend boundary and implementation map ``` +## Shared surfaces + +The shared product contract is documented in +[docs/shared-product-contract.md](docs/shared-product-contract.md). The +frontend map is in [docs/frontend-map.md](docs/frontend-map.md), and the +machine-readable REST contract is [api/openapi.yaml](api/openapi.yaml). + +The unified CLI can check the environment and run the read-only API/MCP +surfaces: + +```bash +./scripts/concord doctor --offline +./scripts/concord api -facility 0x... -registry 0x... +./scripts/concord mcp -api-url http://127.0.0.1:8080 +``` + +These surfaces prepare unsigned transaction intents only. They do not custody, +sign, broadcast, or grant FCC verifier authority. + ## Official Flare sources - [Build Your First FCC Extension](https://dev.flare.network/fcc/guides/getting-started) diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..da94ea3 --- /dev/null +++ b/api/README.md @@ -0,0 +1,20 @@ +# Concord REST surface + +`openapi.yaml` is the v1 integration contract. The runnable implementation is +the read-only chain projection and unsigned-intent server exposed by: + +```bash +./scripts/concord api \ + -facility 0x... \ + -registry 0x... +``` + +The server reads `CapitalFacility` and `AccordRegistry` through the configured +RPC. It does not use an indexer yet, and it does not sign, broadcast, custody, +or authorize transactions. A successful `POST /v1/transactions/prepare` +response means only that calldata was constructed and current read +preconditions were checked. + +The API deliberately returns amounts as base-unit decimal strings and marks +private quote fields as `withheld`. The frontend and SDK must consume these +same fields rather than reconstructing protocol state independently. diff --git a/api/openapi.yaml b/api/openapi.yaml new file mode 100644 index 0000000..914d266 --- /dev/null +++ b/api/openapi.yaml @@ -0,0 +1,476 @@ +openapi: 3.1.0 +info: + title: Concord API + version: 0.1.0 + license: + name: MIT + identifier: MIT + description: >- + Read and prepare-only access to Concord's FXRP-backed syndicated capital + facility. The API projects authoritative onchain state and verified FCC + evidence; it does not custody assets or replace Accord authority. +servers: + - url: https://api.concord.invalid + description: Placeholder; use the local CLI or an explicitly configured deployment URL. +tags: + - name: Read model + description: Authoritative onchain relationship projections. + - name: Transaction intents + description: Unsigned calldata preparation with explicit approval boundaries. + - name: Evidence + description: FCC verification metadata and disclosure status. +security: [] +x-security-status: >- + The local scaffold is intentionally unauthenticated. A production deployment + must add caller authentication and role-scoped disclosure before exposing + private quote or institution data. +paths: + /v1/health: + get: + tags: [Read model] + summary: Check API and configured network + operationId: getHealth + responses: + '200': + description: API process and configured network status + content: + application/json: + schema: + $ref: '#/components/schemas/HealthResponse' + '500': + $ref: '#/components/responses/InternalError' + '400': + $ref: '#/components/responses/InvalidRequest' + /v1/facilities/{rootAccordId}: + get: + tags: [Read model] + summary: Read one Root Accord facility + operationId: getFacility + parameters: + - $ref: '#/components/parameters/RootAccordId' + responses: + '200': + description: Root Accord with observed child relationships and round summary + content: + application/json: + schema: + $ref: '#/components/schemas/FacilityResponse' + '404': + $ref: '#/components/responses/NotObserved' + /v1/facilities/{rootAccordId}/lineage: + get: + tags: [Read model] + summary: Read Root Accord causal lineage + operationId: getFacilityLineage + parameters: + - $ref: '#/components/parameters/RootAccordId' + responses: + '200': + description: Causal relationship graph rooted at the Accord + content: + application/json: + schema: + $ref: '#/components/schemas/LineageResponse' + '404': + $ref: '#/components/responses/NotObserved' + /v1/rounds/{roundId}: + get: + tags: [Read model] + summary: Read one Makkari round + operationId: getRound + parameters: + - $ref: '#/components/parameters/RoundId' + responses: + '200': + description: Public round metadata and allocation status + content: + application/json: + schema: + $ref: '#/components/schemas/RoundResponse' + '404': + $ref: '#/components/responses/NotObserved' + /v1/draws/{drawId}: + get: + tags: [Read model] + summary: Read one draw and its DrawLegs + operationId: getDraw + parameters: + - $ref: '#/components/parameters/DrawId' + responses: + '200': + description: Root draw and explicit child DrawLeg objects + content: + application/json: + schema: + $ref: '#/components/schemas/DrawResponse' + '404': + $ref: '#/components/responses/NotObserved' + /v1/evidence/{resultDigest}: + get: + tags: [Evidence] + summary: Read allocation evidence metadata + operationId: getAllocationEvidence + parameters: + - $ref: '#/components/parameters/ResultDigest' + responses: + '200': + description: Evidence metadata available to the caller + content: + application/json: + schema: + $ref: '#/components/schemas/EvidenceResponse' + '404': + $ref: '#/components/responses/NotObserved' + /v1/transactions/prepare: + post: + tags: [Transaction intents] + summary: Prepare unsigned transaction calldata + operationId: prepareTransaction + description: >- + Builds unsigned calldata only. The caller must review, authorize, sign, + broadcast, and wait for the chain receipt. The API never returns a + claim that the action succeeded. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PrepareTransactionRequest' + responses: + '200': + description: Unsigned transaction intent + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionIntentResponse' + '400': + $ref: '#/components/responses/InvalidRequest' +components: + parameters: + RootAccordId: + name: rootAccordId + in: path + required: true + schema: { $ref: '#/components/schemas/Bytes32' } + RoundId: + name: roundId + in: path + required: true + schema: { $ref: '#/components/schemas/Bytes32' } + DrawId: + name: drawId + in: path + required: true + schema: { $ref: '#/components/schemas/Bytes32' } + ResultDigest: + name: resultDigest + in: path + required: true + schema: { $ref: '#/components/schemas/Bytes32' } + responses: + NotObserved: + description: The requested authoritative object was not observed + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + InvalidRequest: + description: Invalid identifier or action arguments + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + InternalError: + description: The API could not produce a read model response + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + schemas: + Bytes32: + type: string + pattern: '^0x[0-9a-fA-F]{64}$' + description: Hex-encoded Solidity bytes32 value. + Address: + type: string + pattern: '^0x[0-9a-fA-F]{40}$' + DecimalString: + type: string + pattern: '^[0-9]+$' + description: Non-negative integer in base-10 notation; never a floating-point JSON number. + Observation: + type: object + required: [status, source, network, chainId, observedAt] + properties: + status: + type: string + enum: [observed, partial, not_observed] + source: + type: string + enum: [onchain, fcc_evidence, derived] + network: { type: string, example: coston2 } + chainId: { type: integer, example: 114 } + blockNumber: + $ref: '#/components/schemas/DecimalString' + observedAt: + type: string + format: date-time + warning: { type: string } + Meta: + type: object + required: [observation] + properties: + observation: { $ref: '#/components/schemas/Observation' } + apiVersion: { type: string, example: v1 } + RootState: + type: string + enum: [NONE, PROPOSED, SYNDICATING, FUNDING, ACTIVE, CLOSED, FROZEN, EXPIRED] + ChildState: + type: string + enum: [NONE, SELECTED, FUNDED, ACTIVE, CLOSED, EXPIRED, DEFAULTED] + RoundStatus: + type: string + enum: [NONE, OPEN, FINALIZED, EXPIRED] + LineageKind: + type: string + enum: [ROOT_ACCORD, MAKKARI_SESSION, COFILL_ALLOCATION, CHILD_ACCORD, DRAW, DRAW_LEG, SETTLEMENT, REPAYMENT] + StateCopy: + type: object + required: [state, label, explanation] + properties: + state: { type: string } + label: { type: string } + explanation: { type: string } + Asset: + type: object + required: [address, symbol, decimals] + properties: + address: { $ref: '#/components/schemas/Address' } + symbol: { type: string, example: USDT0 } + decimals: { type: integer, minimum: 0, maximum: 255 } + ChildAccord: + type: object + required: [id, rootAccordId, allocationId, provider, selectedCapacity, committedCapacity, drawnPrincipal, availableCapacity, feeBps, validUntil, termsCommitment, state, stateCopy] + properties: + id: { $ref: '#/components/schemas/Bytes32' } + rootAccordId: { $ref: '#/components/schemas/Bytes32' } + allocationId: { $ref: '#/components/schemas/Bytes32' } + provider: { $ref: '#/components/schemas/Address' } + selectedCapacity: { $ref: '#/components/schemas/DecimalString' } + committedCapacity: { $ref: '#/components/schemas/DecimalString' } + drawnPrincipal: { $ref: '#/components/schemas/DecimalString' } + availableCapacity: { $ref: '#/components/schemas/DecimalString' } + feeBps: { type: integer, minimum: 0, maximum: 10000 } + validUntil: { type: string, format: date-time } + validUntilUnix: { $ref: '#/components/schemas/DecimalString' } + termsCommitment: { $ref: '#/components/schemas/Bytes32' } + state: { $ref: '#/components/schemas/ChildState' } + stateCopy: { $ref: '#/components/schemas/StateCopy' } + Round: + type: object + required: [id, rootAccordId, targetCapacity, maxFeeBps, roundExpiry, status, stateCopy, eligibleProviderCount] + properties: + id: { $ref: '#/components/schemas/Bytes32' } + rootAccordId: { $ref: '#/components/schemas/Bytes32' } + targetCapacity: { $ref: '#/components/schemas/DecimalString' } + maxFeeBps: { type: integer, minimum: 0, maximum: 10000 } + roundExpiry: { type: string, format: date-time } + roundExpiryUnix: { $ref: '#/components/schemas/DecimalString' } + status: { $ref: '#/components/schemas/RoundStatus' } + stateCopy: { $ref: '#/components/schemas/StateCopy' } + eligibleProviderCount: { type: integer, minimum: 0 } + privateQuoteData: + type: string + enum: [withheld, authorized] + description: Indicates whether private quote fields are withheld from this caller. + Facility: + type: object + required: [id, borrower, collateralAsset, liquidityAsset, targetCapacity, committedCapacity, drawnPrincipal, collateralLocked, availableCapacity, validUntil, policyHash, syndicationRoundId, state, stateCopy, children, invariants] + properties: + id: { $ref: '#/components/schemas/Bytes32' } + borrower: { $ref: '#/components/schemas/Address' } + collateralAsset: { $ref: '#/components/schemas/Asset' } + liquidityAsset: { $ref: '#/components/schemas/Asset' } + targetCapacity: { $ref: '#/components/schemas/DecimalString' } + committedCapacity: { $ref: '#/components/schemas/DecimalString' } + drawnPrincipal: { $ref: '#/components/schemas/DecimalString' } + collateralLocked: { $ref: '#/components/schemas/DecimalString' } + availableCapacity: { $ref: '#/components/schemas/DecimalString' } + validUntil: { type: string, format: date-time } + validUntilUnix: { $ref: '#/components/schemas/DecimalString' } + policyHash: { $ref: '#/components/schemas/Bytes32' } + syndicationRoundId: { $ref: '#/components/schemas/Bytes32' } + state: { $ref: '#/components/schemas/RootState' } + stateCopy: { $ref: '#/components/schemas/StateCopy' } + round: { $ref: '#/components/schemas/Round' } + children: + type: array + items: { $ref: '#/components/schemas/ChildAccord' } + invariants: + type: object + required: [rootDrawWithinCommitment, childDrawWithinCommitment, rootExposureMatchesChildren, committedMatchesFundedChildren] + properties: + rootDrawWithinCommitment: { type: boolean } + childDrawWithinCommitment: { type: boolean } + rootExposureMatchesChildren: { type: boolean } + committedMatchesFundedChildren: { type: boolean } + LineageNode: + type: object + required: [id, parentId, kind, createdAt, children] + properties: + id: { $ref: '#/components/schemas/Bytes32' } + parentId: { $ref: '#/components/schemas/Bytes32' } + kind: { $ref: '#/components/schemas/LineageKind' } + createdAt: { type: string, format: date-time } + children: + type: array + items: { $ref: '#/components/schemas/Bytes32' } + DrawLeg: + type: object + required: [id, drawId, childAccordId, provider, principal, repaidPrincipal, outstandingPrincipal] + properties: + id: { $ref: '#/components/schemas/Bytes32' } + drawId: { $ref: '#/components/schemas/Bytes32' } + childAccordId: { $ref: '#/components/schemas/Bytes32' } + provider: { $ref: '#/components/schemas/Address' } + principal: { $ref: '#/components/schemas/DecimalString' } + repaidPrincipal: { $ref: '#/components/schemas/DecimalString' } + outstandingPrincipal: { $ref: '#/components/schemas/DecimalString' } + Draw: + type: object + required: [id, rootAccordId, principal, repaidPrincipal, outstandingPrincipal, createdAt, legs] + properties: + id: { $ref: '#/components/schemas/Bytes32' } + rootAccordId: { $ref: '#/components/schemas/Bytes32' } + principal: { $ref: '#/components/schemas/DecimalString' } + repaidPrincipal: { $ref: '#/components/schemas/DecimalString' } + outstandingPrincipal: { $ref: '#/components/schemas/DecimalString' } + createdAt: { type: string, format: date-time } + legs: + type: array + items: { $ref: '#/components/schemas/DrawLeg' } + Evidence: + type: object + required: [resultDigest, status, disclosure] + properties: + resultDigest: { $ref: '#/components/schemas/Bytes32' } + status: + type: string + enum: [verified, invalid, withheld, not_observed] + disclosure: + type: string + enum: [metadata_only, authorized_full, withheld] + extensionId: { $ref: '#/components/schemas/Bytes32' } + roundId: { $ref: '#/components/schemas/Bytes32' } + rootAccordId: { $ref: '#/components/schemas/Bytes32' } + instructionId: { $ref: '#/components/schemas/Bytes32' } + source: { type: string, enum: [coston2_fcc, local_simulated_tee, not_observed] } + warning: { type: string } + PrepareTransactionRequest: + type: object + required: [action] + properties: + action: + type: string + enum: [create_root, lock_collateral, approve_asset, fund_child, draw, repay] + rootAccordId: { $ref: '#/components/schemas/Bytes32' } + childAccordId: { $ref: '#/components/schemas/Bytes32' } + drawId: { $ref: '#/components/schemas/Bytes32' } + targetCapacity: { $ref: '#/components/schemas/DecimalString' } + amount: { $ref: '#/components/schemas/DecimalString' } + validUntilUnix: { $ref: '#/components/schemas/DecimalString' } + policyHash: { $ref: '#/components/schemas/Bytes32' } + asset: { $ref: '#/components/schemas/Address' } + spender: { $ref: '#/components/schemas/Address' } + actor: + type: string + enum: [treasury, provider, institution, agent] + TransactionIntent: + type: object + required: [action, chainId, to, data, value, summary, requiresExplicitApproval, preconditions] + properties: + action: { type: string } + chainId: { type: integer, example: 114 } + to: { $ref: '#/components/schemas/Address' } + data: { type: string, pattern: '^0x[0-9a-fA-F]*$' } + value: { $ref: '#/components/schemas/DecimalString' } + summary: { type: string } + requiresExplicitApproval: { type: boolean, const: true } + preconditions: + type: array + items: { type: string } + warnings: + type: array + items: { type: string } + Health: + type: object + required: [service, network, chainId, configured, readModel] + properties: + service: { type: string, const: concord-api } + network: { type: string } + chainId: { type: integer } + configured: { type: boolean } + readModel: { type: string, enum: [chain, unavailable] } + facilityAddress: { $ref: '#/components/schemas/Address' } + registryAddress: { $ref: '#/components/schemas/Address' } + FacilityResponse: + type: object + required: [data, meta] + properties: + data: { $ref: '#/components/schemas/Facility' } + meta: { $ref: '#/components/schemas/Meta' } + RoundResponse: + type: object + required: [data, meta] + properties: + data: { $ref: '#/components/schemas/Round' } + meta: { $ref: '#/components/schemas/Meta' } + DrawResponse: + type: object + required: [data, meta] + properties: + data: { $ref: '#/components/schemas/Draw' } + meta: { $ref: '#/components/schemas/Meta' } + LineageResponse: + type: object + required: [data, meta] + properties: + data: + type: object + required: [rootAccordId, nodes] + properties: + rootAccordId: { $ref: '#/components/schemas/Bytes32' } + nodes: + type: array + items: { $ref: '#/components/schemas/LineageNode' } + meta: { $ref: '#/components/schemas/Meta' } + EvidenceResponse: + type: object + required: [data, meta] + properties: + data: { $ref: '#/components/schemas/Evidence' } + meta: { $ref: '#/components/schemas/Meta' } + TransactionIntentResponse: + type: object + required: [data, meta] + properties: + data: { $ref: '#/components/schemas/TransactionIntent' } + meta: { $ref: '#/components/schemas/Meta' } + HealthResponse: + type: object + required: [data, meta] + properties: + data: { $ref: '#/components/schemas/Health' } + meta: { $ref: '#/components/schemas/Meta' } + ErrorResponse: + type: object + required: [error, meta] + properties: + error: + type: object + required: [code, message] + properties: + code: { type: string } + message: { type: string } + details: { type: object, additionalProperties: true } + meta: { $ref: '#/components/schemas/Meta' } diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 0000000..c5d1338 --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,33 @@ +# Concord CLI + +The unified CLI is available through `scripts/concord`: + +```bash +./scripts/concord version +./scripts/concord doctor --offline +./scripts/concord doctor +./scripts/concord api -facility 0x... -registry 0x... +./scripts/concord mcp -api-url http://127.0.0.1:8080 +``` + +If Go is not on the normal path, set `CONCORD_GO_BIN`. The live doctor check +requires the configured RPC to be reachable; `--offline` validates local +configuration and tools without making a network request. + +## What is ready + +- one command namespace for configuration checks, API startup, and MCP startup; +- Coston2 network configuration is read from `config/networks/coston2.json`; +- `doctor` validates the configured chain ID and optionally checks bytecode at + supplied facility/registry addresses; +- the API uses read-only chain calls and returns unsigned transaction intents; +- MCP exposes facility, round, draw, and lineage resources plus the same safe + read/prepare operations. + +## What is not ready + +The CLI is not a complete live-facility operator yet. It does not replace the +existing bounded FCC/deployment commands, does not hold a signer, and does not +claim that Concord contracts or an FCC extension are deployed on Coston2. The +live operator path still requires deployed addresses, funded accounts, a +configured FCC proxy/TEE flow, provider offers, and recorded receipts. diff --git a/docs/frontend-map.md b/docs/frontend-map.md new file mode 100644 index 0000000..34188a7 --- /dev/null +++ b/docs/frontend-map.md @@ -0,0 +1,156 @@ +# Concord frontend map + +This is the frontend implementation map for the first product phase. It is a +map over the shared product contract, not a second protocol design. Full UI +implementation follows the live contract/FCC integration evidence gate. + +## Product shell + +```text +┌──────────────────────────────────────────────────────────────────────┐ +│ Concord Facilities Activity Evidence Network Wallet │ +├───────────────┬──────────────────────────────────────────────────────┤ +│ role switcher │ Root Accord │ +│ │ FXRP collateral · committed · drawn · available │ +│ Treasury │ expiry · state explanation · next permitted action │ +│ Provider ├──────────────────────────────────────────────────────┤ +│ Institution │ Relationship map │ +│ Auditor │ Root → Makkari → CoFill → children → draw legs │ +│ Observer ├──────────────────────────────────────────────────────┤ +│ │ Child Accords │ +│ guided/detail │ provider · selected · funded · drawn · available │ +│ ├──────────────────────────────────────────────────────┤ +│ │ Activity / evidence / action review │ +└───────────────┴──────────────────────────────────────────────────────┘ +``` + +The relationship view is the primary screen. A chart or graph is subordinate +to readable relationship cards and exact amounts; the graph is not decoration +and cannot be the only way to understand causality. + +## Routes + +| Route | Purpose | Required contract data | +|---|---|---| +| `/` | Role-aware entry and facility list | health, accessible facilities, role context | +| `/facilities/:rootAccordId` | Primary Root Accord workspace | facility, round, children, invariants | +| `/facilities/:rootAccordId/lineage` | Full causal relationship trail | lineage, linked public actions | +| `/rounds/:roundId` | Makkari/CoFill status | round, authorization-scoped evidence | +| `/children/:childAccordId` | Provider relationship view | child, root summary, funding/exposure | +| `/draws/:drawId` | One draw and its explicit legs | draw, child/provider links, settlements | +| `/evidence/:resultDigest` | Verification and disclosure boundary | evidence metadata, source, warning | +| `/settings/network` | Network/assets and disclosure settings | network config, asset decimals | + +## Components + +```text +AppShell +├─ RoleContext +├─ NetworkStatus +├─ WalletBoundary +├─ FacilityList +└─ FacilityWorkspace + ├─ RelationshipHeader + ├─ FacilityMetrics + ├─ StateExplanation + ├─ NextActionCard + ├─ RelationshipTopology + │ ├─ RootAccordNode + │ ├─ MakkariNode + │ ├─ CoFillNode + │ ├─ ChildAccordNode[] + │ └─ DrawLegNode[] + ├─ ChildAccordTable + ├─ ActivityTimeline + ├─ EvidencePanel + └─ ActionReviewModal + ├─ HumanSummary + ├─ Preconditions + ├─ ContractTarget + ├─ WalletRequest + └─ ReceiptResult +``` + +The `ActionReviewModal` never displays “completed” merely because a wallet +request was submitted. Its states are `draft`, `awaiting_signature`, +`submitted`, `confirmed`, `reverted`, and `not_observed`. + +## Role surfaces + +### Treasury + +The treasury sees the facility lifecycle in order: + +1. Create Root Accord. +2. Lock FXRP collateral. +3. Open the Makkari round. +4. Review the verified CoFill allocation. +5. Monitor selected versus funded children. +6. Draw only within available capacity. +7. Repay and verify restored capacity. + +The primary call-to-action is derived from the authoritative root state, not a +generic “continue” button. + +### Provider + +The provider sees eligible rounds, its own confidential quote workflow, and its +own child relationships. A selected card must say “selected, not funded” until +the USDT0 transfer succeeds. Provider data must never disclose losing quotes or +another provider's confidential constraints. + +### Institution/operator + +The institution view adds organization context, approvals, evidence export, +and activity filtering. It uses the same Root Accord, Child Accord, exposure, +and lineage fields as the treasury view. + +### Auditor/observer + +The observer view is read-only and emphasizes public commitments, transfers, +exposure, transaction receipts, and lineage. Private quote fields are visibly +marked `withheld`, not shown as empty or zero. + +### Developer/agent + +The agent surface is a compact API/MCP-oriented view. It can inspect the same +projection, explain why an action is available, and prepare an unsigned intent. +It must show the human approval boundary before handing anything to a wallet. + +## State-to-action map + +| Observed state | Guided explanation | Primary action | +|---|---|---| +| `PROPOSED` | “Your facility exists. Collateral is the next requirement.” | Prepare FXRP lock | +| `SYNDICATING` | “Providers are privately offering capacity.” | Monitor round / authorized quote action | +| `FUNDING` | “Providers were selected; funding is still being transferred.” | Provider funding / monitor | +| `ACTIVE` | “The facility is fully funded.” | Prepare draw | +| Draw outstanding | “This draw is supplied by these child relationships.” | Prepare repayment | +| Repaid | “Exposure fell and capacity returned.” | Review receipt / next draw | +| `CLOSED` | “The relationship is complete.” | View lineage and evidence | +| `EXPIRED` or `FROZEN` | “New financial actions are unavailable.” | View reason and evidence | + +## Global and accessible behavior + +- Use the user's locale for dates and number grouping, while retaining exact + base-unit values in detail and copy-to-clipboard controls. +- Display token symbol, decimals, network, and contract address together when + an amount could be misunderstood. +- Never rely on color alone for state; use text, icons with labels, and status + announcements. +- Support keyboard navigation, reduced motion, readable contrast, zoom, and + narrow screens. The target is WCAG 2.2 AA where the chosen component stack + supports it. +- Use human-readable labels first and canonical IDs on demand. Do not shorten + two different identifiers to the same visible prefix in the same view. +- Surface stale, partial, and not-observed data as explicit states. +- Avoid regional assumptions in currency formatting, date order, language, and + legal or institutional terminology. + +## Frontend implementation boundary + +The eventual React/TypeScript app should consume `@concord-protocol/sdk` and +never call contract methods directly from arbitrary components. Wallet calls +are made from an action boundary after the API/SDK returns an unsigned intent. +This keeps the same semantics across web, CLI, REST, MCP, and institution +integrations. diff --git a/docs/shared-product-contract.md b/docs/shared-product-contract.md new file mode 100644 index 0000000..149e5bf --- /dev/null +++ b/docs/shared-product-contract.md @@ -0,0 +1,189 @@ +# Concord shared product contract + +Status: implementation contract for the first integration phase. + +This document is the semantic boundary shared by the web interface, REST API, +SDKs, CLI, MCP server, and future agent integrations. It does not create a +second source of truth. Onchain Accord and facility state, plus verified FCC +evidence where applicable, remain authoritative. + +## Canonical relationship + +Concord exposes one persistent economic relationship: + +```text +Root Accord + └─ Makkari session + └─ CoFill allocation + └─ Child Accords + └─ funded commitments + └─ Draw + └─ DrawLegs + └─ Settlement + └─ Repayment +``` + +The transaction is an event in this graph. It is not the canonical product +object. + +## Shared vocabulary + +| Canonical term | Plain-language meaning | Authoritative owner | +|---|---|---| +| Root Accord | The one facility relationship between the treasury and its providers | `CapitalFacility` + `AccordRegistry` | +| Makkari session | The bounded private coordination session for one syndication round | FCC extension and round state | +| CoFill allocation | The deterministic answer to which eligible providers supply which portions | FCC result, then verified digest | +| Child Accord | One provider's independently governed relationship under the root | `CapitalFacility` | +| Committed | Capacity actually funded by a provider's successful USDT0 transfer | `CapitalFacility` | +| Available | Committed capacity minus principal currently drawn | Derived from onchain state | +| Draw leg | The portion of one root draw supplied by one child Accord | `CapitalFacility` + `AccordRegistry` | +| Lineage | The causal relationship trail explaining why an action was allowed | `AccordRegistry` | + +The UI may use the plain-language labels, but it must preserve the canonical +terms in details, URLs, exports, and machine-readable responses. + +## State semantics + +### Root Accord + +```text +PROPOSED → SYNDICATING → FUNDING → ACTIVE → CLOSED + └──────────────→ EXPIRED + └──────────────→ FROZEN (exceptional) +``` + +| State | Meaning | What the interface may offer | +|---|---|---| +| `PROPOSED` | Root exists; collateral has not necessarily been locked | Lock collateral | +| `SYNDICATING` | Collateral is locked and the Makkari round is open | Submit/observe the round; finalize only through the verified flow | +| `FUNDING` | CoFill selected providers; selected capacity is not yet committed | Provider funding | +| `ACTIVE` | Target capacity is fully funded | Draw and observe exposure | +| `CLOSED` | Exposure and commitments are zero; recoverable balances were returned | Read-only history | +| `FROZEN` | Exceptional administrative or safety state | Read-only; no new financial action | +| `EXPIRED` | Validity ended with no outstanding exposure or commitments | Read-only history | + +### Child Accord + +```text +SELECTED → FUNDED → ACTIVE → CLOSED + └──────────────→ EXPIRED + └──────────────→ DEFAULTED (future resolution, not MVP) +``` + +`SELECTED` is an allocation result. It is never presented as funded or +committed capital. `FUNDED` is reached only after the provider's USDT0 +transfer succeeds. `ACTIVE` is reached when the root reaches full funding and +the facility activates the funded children. + +### Amount invariants + +Every projection and every write-preparation response must preserve: + +```text +root.drawnPrincipal <= root.committedCapacity +child.drawnPrincipal <= child.committedCapacity +root.drawnPrincipal == Σ child.drawnPrincipal +root.committedCapacity == Σ funded child commitments +root.availableCapacity = root.committedCapacity - root.drawnPrincipal +``` + +Amounts are transported as decimal strings in API and SDK payloads. A client +must use the asset decimals supplied by the network configuration; it must not +assume that FXRP and USDT0 use the same decimals. + +## Observation and evidence + +Every read response carries an observation envelope: + +- `observed`: authoritative state was read from the configured source; +- `partial`: some related data was unavailable or could not be traversed; +- `not_observed`: the system has no authoritative observation and must not + present a zero value as a fact. + +The envelope also carries network, chain ID, block number when known, source, +and observation time. “No children” and “children not observed” are different +responses. + +FCC evidence is separate from public facility state. A surface may expose +evidence metadata and verification status to an authorized caller, but it must +not expose losing quotes, private provider constraints, or decrypted payloads +as a general API feature. + +## Role model + +Roles change which actions are visible and which private inputs may be +returned. They do not change the underlying economic semantics. + +| Role | Read scope | Write/prepare scope | +|---|---|---| +| Treasury | Root, public round result, children, draws, repayments, lineage, authorized evidence | Root creation, collateral lock, round opening, materialization handoff, draw, repayment, close | +| Provider | Authorized rounds, own quote status, own child Accords, own funding/exposure, public lineage | Own signed quote, own child funding, own close/expiry action | +| Institution/operator | Facilities granted by its organization, evidence and exports allowed by policy | Same actions delegated by an organization; every transaction still requires a signer | +| Auditor/observer | Public relationship state, commitments, exposure, settlements, repayments, lineage, published evidence | None by default | +| Developer/agent | API/MCP scope granted by the caller; public state by default | Prepare unsigned intents; no implicit custody, signing, verifier authority, or broadcast | + +An agent is a delegate. It is not a new participant in the Accord and cannot +become the allocation verifier merely because it can call an API. + +## Layered surfaces + +All surfaces consume this contract through one control plane: + +```text +Contracts + FCC evidence + ↓ +Concord read model / intent builder + ┌────┼──────────┬─────────┐ + ↓ ↓ ↓ ↓ + REST SDK MCP CLI + └────┴──────────┴─────────┘ + ↓ + Frontend +``` + +- REST/OpenAPI is the stable external contract. +- SDKs are typed clients of REST and transaction-intent builders; they do not + reimplement lifecycle rules. +- MCP exposes read resources and safe preparation tools through the same API; + it is not a privileged backdoor. +- The CLI is an operator/developer client of the same endpoints and verifier + commands. It must not silently use a different state model. +- x402, when added, may gate access to API/MCP resources. It cannot bypass + Accord authority or become the facility's USDT0 settlement rail. + +## Transaction intent rules + +A write-capable surface returns an unsigned intent containing: + +- target contract and chain ID; +- calldata and value; +- the human-readable action and affected relationship IDs; +- preconditions read from the current projection; +- warnings for expiry, availability, or authorization assumptions; +- `requiresExplicitApproval: true`. + +The surface does not hold a private key, sign for a user, broadcast a +transaction, or report success before a receipt is observed. The frontend may +hand the intent to a connected wallet; the final result comes from the chain. + +## Frontend contract + +The primary route is `/facilities/:rootAccordId`. It must answer, in this +order: + +1. What relationship is this? +2. Who participates? +3. How much was selected, funded, drawn, and remains available? +4. Which child relationships supplied the current exposure? +5. Why is the next action allowed? +6. What evidence and lineage support the state? + +The interface has two disclosure modes over the same data: + +- Guided mode: plain-language labels, one next action, warnings, and a short + relationship explanation. +- Detailed mode: canonical IDs, contract addresses, fee bps, terms + commitments, block/transaction references, FCC evidence, and lineage. + +No screen may show a synthetic “success” state from a local mock when the +authoritative observation is missing. diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..ff12352 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,13 @@ +# Concord frontend + +The frontend is intentionally mapped before full implementation. The +implementation must use the shared product contract and the TypeScript SDK; +it must not create a parallel facility state model. + +Planned stack from the build handoff: React, TypeScript, Vite, Wagmi, Viem, +and TanStack Query. The first implementation checkpoint is the Root Accord +workspace described in [`docs/frontend-map.md`](../docs/frontend-map.md). + +Full frontend work begins after the contract/FCC semantics are demonstrated +with authoritative Coston2 evidence. Until then, this directory contains the +product-facing route and component boundary only. diff --git a/scripts/concord b/scripts/concord new file mode 100755 index 0000000..a159e93 --- /dev/null +++ b/scripts/concord @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Unified Concord operator/developer CLI entrypoint. +# +# Examples: +# ./scripts/concord doctor +# ./scripts/concord api -facility 0x... -registry 0x... +# ./scripts/concord mcp -api-url http://127.0.0.1:8080 +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +GO_BIN="${CONCORD_GO_BIN:-go}" + +cd "$PROJECT_DIR/tools" +exec "$GO_BIN" run ./cmd/concord "$@" diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json new file mode 100644 index 0000000..6b36b1d --- /dev/null +++ b/sdk/typescript/package.json @@ -0,0 +1,21 @@ +{ + "name": "@concord-protocol/sdk", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Typed read and transaction-intent client for Concord's v1 API", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@types/node": "22.10.2", + "typescript": "5.7.2" + } +} diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts new file mode 100644 index 0000000..dd0d612 --- /dev/null +++ b/sdk/typescript/src/index.ts @@ -0,0 +1,272 @@ +export type ObservationStatus = "observed" | "partial" | "not_observed"; +export type ObservationSource = "onchain" | "fcc_evidence" | "derived"; + +export interface Observation { + status: ObservationStatus; + source: ObservationSource; + network: string; + chainId: number; + blockNumber?: string; + observedAt: string; + warning?: string; +} + +export interface Meta { + observation: Observation; + apiVersion: string; +} + +export interface Envelope { + data: T; + meta: Meta; +} + +export type RootState = + | "NONE" + | "PROPOSED" + | "SYNDICATING" + | "FUNDING" + | "ACTIVE" + | "CLOSED" + | "FROZEN" + | "EXPIRED"; + +export type ChildState = + | "NONE" + | "SELECTED" + | "FUNDED" + | "ACTIVE" + | "CLOSED" + | "EXPIRED" + | "DEFAULTED"; + +export type RoundStatus = "NONE" | "OPEN" | "FINALIZED" | "EXPIRED"; + +export interface StateCopy { + state: string; + label: string; + explanation: string; +} + +export interface Asset { + address: string; + symbol: string; + decimals: number; +} + +export interface ChildAccord { + id: string; + rootAccordId: string; + allocationId: string; + provider: string; + selectedCapacity: string; + committedCapacity: string; + drawnPrincipal: string; + availableCapacity: string; + feeBps: number; + validUntil: string; + validUntilUnix: string; + termsCommitment: string; + state: ChildState; + stateCopy: StateCopy; +} + +export interface Round { + id: string; + rootAccordId: string; + targetCapacity: string; + maxFeeBps: number; + roundExpiry: string; + roundExpiryUnix: string; + status: RoundStatus; + stateCopy: StateCopy; + eligibleProviderCount: number; + privateQuoteData: "withheld" | "authorized"; +} + +export interface Facility { + id: string; + borrower: string; + collateralAsset: Asset; + liquidityAsset: Asset; + targetCapacity: string; + committedCapacity: string; + drawnPrincipal: string; + collateralLocked: string; + availableCapacity: string; + validUntil: string; + validUntilUnix: string; + policyHash: string; + syndicationRoundId: string; + state: RootState; + stateCopy: StateCopy; + round?: Round; + children: ChildAccord[]; + invariants: { + rootDrawWithinCommitment: boolean; + childDrawWithinCommitment: boolean; + rootExposureMatchesChildren: boolean; + committedMatchesFundedChildren: boolean; + }; +} + +export interface LineageNode { + id: string; + parentId: string; + kind: + | "ROOT_ACCORD" + | "MAKKARI_SESSION" + | "COFILL_ALLOCATION" + | "CHILD_ACCORD" + | "DRAW" + | "DRAW_LEG" + | "SETTLEMENT" + | "REPAYMENT"; + createdAt: string; + children: string[]; +} + +export interface Lineage { + rootAccordId: string; + nodes: LineageNode[]; +} + +export interface DrawLeg { + id: string; + drawId: string; + childAccordId: string; + provider: string; + principal: string; + repaidPrincipal: string; + outstandingPrincipal: string; +} + +export interface Draw { + id: string; + rootAccordId: string; + principal: string; + repaidPrincipal: string; + outstandingPrincipal: string; + createdAt: string; + legs: DrawLeg[]; +} + +export type PreparedAction = + | "create_root" + | "lock_collateral" + | "approve_asset" + | "fund_child" + | "draw" + | "repay"; + +export interface PrepareTransactionRequest { + action: PreparedAction; + rootAccordId?: string; + childAccordId?: string; + drawId?: string; + targetCapacity?: string; + amount?: string; + validUntilUnix?: string; + policyHash?: string; + asset?: string; + spender?: string; + actor?: "treasury" | "provider" | "institution" | "agent"; +} + +export interface TransactionIntent { + action: PreparedAction; + chainId: number; + to: string; + data: string; + value: string; + summary: string; + requiresExplicitApproval: true; + preconditions: string[]; + warnings?: string[]; +} + +export class ConcordAPIError extends Error { + constructor( + message: string, + readonly status: number, + readonly code?: string, + ) { + super(message); + this.name = "ConcordAPIError"; + } +} + +export interface ConcordClientOptions { + baseUrl: string; + fetcher?: typeof fetch; + headers?: Record; +} + +export class ConcordClient { + private readonly baseUrl: string; + private readonly fetcher: typeof fetch; + private readonly headers: Record; + + constructor(options: ConcordClientOptions) { + this.baseUrl = options.baseUrl.replace(/\/$/, ""); + this.fetcher = options.fetcher ?? fetch; + this.headers = { Accept: "application/json", ...options.headers }; + } + + getFacility(rootAccordId: string): Promise> { + return this.get(`/v1/facilities/${rootAccordId}`); + } + + getLineage(rootAccordId: string): Promise> { + return this.get(`/v1/facilities/${rootAccordId}/lineage`); + } + + getRound(roundId: string): Promise> { + return this.get(`/v1/rounds/${roundId}`); + } + + getDraw(drawId: string): Promise> { + return this.get(`/v1/draws/${drawId}`); + } + + prepareTransaction( + request: PrepareTransactionRequest, + ): Promise> { + return this.request>("/v1/transactions/prepare", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(request), + }); + } + + private async get(path: string): Promise { + return this.request(path, { method: "GET" }); + } + + private async request(path: string, init: RequestInit): Promise { + const response = await this.fetcher(`${this.baseUrl}${path}`, { + ...init, + headers: { ...this.headers, ...(init.headers ?? {}) }, + }); + const body: unknown = await response.json(); + if (!response.ok) { + const error = body as { error?: { code?: string; message?: string } }; + throw new ConcordAPIError( + error.error?.message ?? `Concord API request failed (${response.status})`, + response.status, + error.error?.code, + ); + } + return body as T; + } +} + +export function formatUnits(value: string, decimals: number, maximumFractionDigits = 6): string { + const integer = BigInt(value); + const base = 10n ** BigInt(decimals); + const whole = integer / base; + const fraction = integer % base; + if (fraction === 0n) return whole.toString(); + const fractionText = fraction.toString().padStart(decimals, "0").replace(/0+$/, ""); + return `${whole}.${fractionText.slice(0, maximumFractionDigits)}`; +} diff --git a/sdk/typescript/tsconfig.json b/sdk/typescript/tsconfig.json new file mode 100644 index 0000000..1e81d2c --- /dev/null +++ b/sdk/typescript/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true, + "lib": ["ES2022", "DOM"] + }, + "include": ["src/**/*.ts"] +} diff --git a/tools/cmd/concord/main.go b/tools/cmd/concord/main.go new file mode 100644 index 0000000..a99686f --- /dev/null +++ b/tools/cmd/concord/main.go @@ -0,0 +1,282 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "concord/tools/pkg/api" + "concord/tools/pkg/mcpserver" + "concord/tools/pkg/readmodel" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +const version = "0.1.0" + +type networkConfig struct { + Network string `json:"network"` + ChainID int64 `json:"chainId"` + RPCURL string `json:"rpcUrl"` + FXRP assetConfig `json:"fxrp"` + USDT0 assetConfig `json:"usdt0"` +} + +type assetConfig struct { + Token string `json:"token"` + Symbol string `json:"symbol"` + Decimals uint8 `json:"decimals"` +} + +func main() { + if len(os.Args) < 2 { + usage() + os.Exit(2) + } + switch os.Args[1] { + case "doctor": + exitIfError(runDoctor(os.Args[2:])) + case "api": + exitIfError(runAPI(os.Args[2:])) + case "mcp": + exitIfError(runMCP(os.Args[2:])) + case "version", "--version", "-v": + fmt.Println("concord", version) + case "help", "--help", "-h": + usage() + default: + fmt.Fprintf(os.Stderr, "concord: unknown command %q\n\n", os.Args[1]) + usage() + os.Exit(2) + } +} + +func usage() { + fmt.Println(`Concord operator/developer CLI + +Usage: + concord doctor [flags] Check local tools, network config, and optional contracts + concord api [flags] Serve the read-only REST API and intent builder + concord mcp [flags] Serve the MCP adapter over stdin/stdout + concord version Print the CLI version + +The CLI never stores keys, signs transactions, broadcasts transactions, or +authorizes FCC allocation results. Use the existing deployment/FCC commands +for those explicitly bounded operations.`) +} + +func runDoctor(args []string) error { + flags := flag.NewFlagSet("concord doctor", flag.ContinueOnError) + configPath := flags.String("config", defaultNetworkConfig(), "network JSON configuration") + rpcOverride := flags.String("rpc", "", "override RPC URL") + facility := flags.String("facility", "", "optional CapitalFacility address to check") + registry := flags.String("registry", "", "optional AccordRegistry address to check") + offline := flags.Bool("offline", false, "skip the live RPC check and validate local configuration only") + jsonOutput := flags.Bool("json", false, "emit machine-readable checks") + if err := flags.Parse(args); err != nil { + return err + } + + checks := []map[string]any{} + add := func(name string, ok bool, detail string) { + checks = append(checks, map[string]any{"name": name, "ok": ok, "detail": detail}) + } + + config, err := loadNetworkConfig(*configPath) + if err != nil { + add("network_config", false, err.Error()) + return printDoctor(checks, *jsonOutput) + } + add("network_config", config.Network != "" && config.ChainID != 0 && config.RPCURL != "" && config.FXRP.Token != "" && config.USDT0.Token != "", fmt.Sprintf("%s chain %d; FXRP=%s USDT0=%s", config.Network, config.ChainID, config.FXRP.Token, config.USDT0.Token)) + rpcURL := config.RPCURL + if *rpcOverride != "" { + rpcURL = *rpcOverride + } + for _, binary := range []string{"go", "forge", "jq"} { + path, lookupErr := exec.LookPath(binary) + add("tool_"+binary, lookupErr == nil, pathOrError(path, lookupErr)) + } + if *offline { + add("rpc", true, "skipped by --offline") + return printDoctor(checks, *jsonOutput) + } + + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + client, err := ethclient.DialContext(ctx, rpcURL) + if err != nil { + add("rpc", false, err.Error()) + return printDoctor(checks, *jsonOutput) + } + defer client.Close() + chainID, err := client.ChainID(ctx) + if err != nil { + add("chain_id", false, err.Error()) + } else { + add("chain_id", chainID.Int64() == config.ChainID, fmt.Sprintf("configured %d, RPC returned %s", config.ChainID, chainID.String())) + } + for name, value := range map[string]string{"facility": *facility, "registry": *registry} { + if value == "" { + continue + } + address, parseErr := parseAddress(value) + if parseErr != nil { + add(name+"_address", false, parseErr.Error()) + continue + } + code, codeErr := client.CodeAt(ctx, address, nil) + add(name+"_bytecode", codeErr == nil && len(code) > 0, fmt.Sprintf("%s: %d bytes", address.Hex(), len(code))) + } + return printDoctor(checks, *jsonOutput) +} + +func runAPI(args []string) error { + flags := flag.NewFlagSet("concord api", flag.ContinueOnError) + configPath := flags.String("config", defaultNetworkConfig(), "network JSON configuration") + rpcOverride := flags.String("rpc", "", "override RPC URL") + facility := flags.String("facility", "", "CapitalFacility address") + registry := flags.String("registry", "", "AccordRegistry address") + listen := flags.String("listen", "127.0.0.1:8080", "HTTP listen address") + if err := flags.Parse(args); err != nil { + return err + } + config, err := loadNetworkConfig(*configPath) + if err != nil { + return err + } + rpcURL := config.RPCURL + if *rpcOverride != "" { + rpcURL = *rpcOverride + } + facilityAddress, err := parseRequiredAddress(*facility, "facility") + if err != nil { + return err + } + registryAddress, err := parseRequiredAddress(*registry, "registry") + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + reader, err := readmodel.NewChainReader(ctx, readmodel.ChainReaderConfig{ + RPCURL: rpcURL, + Network: config.Network, + ChainID: config.ChainID, + FacilityAddress: facilityAddress, + RegistryAddress: registryAddress, + CollateralAsset: readmodel.Asset{Address: config.FXRP.Token, Symbol: "FXRP", Decimals: config.FXRP.Decimals}, + LiquidityAsset: readmodel.Asset{Address: config.USDT0.Token, Symbol: config.USDT0.Symbol, Decimals: config.USDT0.Decimals}, + }) + if err != nil { + return err + } + defer reader.Close() + server, err := api.NewServer(reader, config.Network, config.ChainID, facilityAddress, registryAddress) + if err != nil { + return err + } + fmt.Printf("Concord API listening on http://%s (network=%s chainId=%d)\n", *listen, config.Network, config.ChainID) + fmt.Printf("Read model: CapitalFacility=%s AccordRegistry=%s\n", facilityAddress.Hex(), registryAddress.Hex()) + return http.ListenAndServe(*listen, server.Handler()) +} + +func runMCP(args []string) error { + flags := flag.NewFlagSet("concord mcp", flag.ContinueOnError) + apiURL := flags.String("api-url", "http://127.0.0.1:8080", "Concord API base URL") + if err := flags.Parse(args); err != nil { + return err + } + return (&mcpserver.Server{APIBaseURL: *apiURL, Input: os.Stdin, Output: os.Stdout}).Serve(context.Background()) +} + +func loadNetworkConfig(path string) (networkConfig, error) { + data, err := os.ReadFile(path) + if err != nil { + return networkConfig{}, fmt.Errorf("read network config %s: %w", path, err) + } + var config networkConfig + if err := json.Unmarshal(data, &config); err != nil { + return networkConfig{}, fmt.Errorf("decode network config: %w", err) + } + if config.Network == "" || config.ChainID == 0 || config.RPCURL == "" { + return networkConfig{}, fmt.Errorf("network config is missing network, chainId, or rpcUrl") + } + return config, nil +} + +func defaultNetworkConfig() string { + if _, err := os.Stat("../config/networks/coston2.json"); err == nil { + return "../config/networks/coston2.json" + } + return filepath.Join("config", "networks", "coston2.json") +} + +func parseRequiredAddress(value, name string) (common.Address, error) { + if value == "" { + return common.Address{}, fmt.Errorf("-%s is required", name) + } + return parseAddress(value) +} + +func parseAddress(value string) (common.Address, error) { + if !strings.HasPrefix(value, "0x") || len(value) != 42 { + return common.Address{}, fmt.Errorf("invalid address %q", value) + } + for _, char := range value[2:] { + if !strings.ContainsRune("0123456789abcdefABCDEF", char) { + return common.Address{}, fmt.Errorf("invalid address %q", value) + } + } + return common.HexToAddress(value), nil +} + +func printDoctor(checks []map[string]any, jsonOutput bool) error { + ok := true + for _, check := range checks { + if value, exists := check["ok"].(bool); exists && !value { + ok = false + } + } + if jsonOutput { + data, _ := json.MarshalIndent(map[string]any{"ready": ok, "checks": checks}, "", " ") + fmt.Println(string(data)) + } else { + for _, check := range checks { + mark := "PASS" + if value, exists := check["ok"].(bool); exists && !value { + mark = "FAIL" + } + fmt.Printf("[%s] %-22s %s\n", mark, check["name"], check["detail"]) + } + if ok { + fmt.Println("CLI doctor: ready for the configured checks") + } else { + fmt.Println("CLI doctor: not ready; resolve the failed checks") + } + } + if !ok { + return fmt.Errorf("one or more doctor checks failed") + } + return nil +} + +func pathOrError(path string, err error) string { + if err != nil { + return err.Error() + } + return path +} + +func exitIfError(err error) { + if err != nil { + fmt.Fprintln(os.Stderr, "concord:", err) + os.Exit(1) + } +} diff --git a/tools/pkg/api/server.go b/tools/pkg/api/server.go new file mode 100644 index 0000000..b1b83cd --- /dev/null +++ b/tools/pkg/api/server.go @@ -0,0 +1,396 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "math/big" + "net/http" + "regexp" + "strings" + "time" + + "concord/tools/pkg/readmodel" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +type Server struct { + Reader readmodel.Reader + Network string + ChainID int64 + FacilityAddress common.Address + RegistryAddress common.Address + FacilityABI abi.ABI + TokenABI abi.ABI +} + +type PrepareRequest struct { + Action string `json:"action"` + RootAccordID string `json:"rootAccordId,omitempty"` + ChildAccordID string `json:"childAccordId,omitempty"` + DrawID string `json:"drawId,omitempty"` + TargetCapacity string `json:"targetCapacity,omitempty"` + Amount string `json:"amount,omitempty"` + ValidUntilUnix string `json:"validUntilUnix,omitempty"` + PolicyHash string `json:"policyHash,omitempty"` + Asset string `json:"asset,omitempty"` + Spender string `json:"spender,omitempty"` + Actor string `json:"actor,omitempty"` +} + +const prepareABIJSON = `[ + {"inputs":[{"name":"rootId","type":"bytes32"},{"name":"targetCapacity","type":"uint256"},{"name":"validUntil","type":"uint64"},{"name":"policyHash","type":"bytes32"}],"name":"createRootAccord","outputs":[],"stateMutability":"nonpayable","type":"function"}, + {"inputs":[{"name":"rootId","type":"bytes32"},{"name":"amount","type":"uint256"}],"name":"lockCollateral","outputs":[],"stateMutability":"nonpayable","type":"function"}, + {"inputs":[{"name":"childId","type":"bytes32"},{"name":"amount","type":"uint256"}],"name":"fundChild","outputs":[],"stateMutability":"nonpayable","type":"function"}, + {"inputs":[{"name":"drawId","type":"bytes32"},{"name":"rootId","type":"bytes32"},{"name":"amount","type":"uint256"}],"name":"draw","outputs":[],"stateMutability":"nonpayable","type":"function"}, + {"inputs":[{"name":"drawId","type":"bytes32"},{"name":"amount","type":"uint256"}],"name":"repay","outputs":[],"stateMutability":"nonpayable","type":"function"} +]` + +const tokenABIJSON = `[ + {"inputs":[{"name":"spender","type":"address"},{"name":"amount","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"} +]` + +func NewServer(reader readmodel.Reader, network string, chainID int64, facility, registry common.Address) (*Server, error) { + facilityABI, err := abi.JSON(strings.NewReader(prepareABIJSON)) + if err != nil { + return nil, fmt.Errorf("parse transaction ABI: %w", err) + } + tokenABI, err := abi.JSON(strings.NewReader(tokenABIJSON)) + if err != nil { + return nil, fmt.Errorf("parse token ABI: %w", err) + } + return &Server{ + Reader: reader, + Network: network, + ChainID: chainID, + FacilityAddress: facility, + RegistryAddress: registry, + FacilityABI: facilityABI, + TokenABI: tokenABI, + }, nil +} + +func (s *Server) Handler() http.Handler { + return http.HandlerFunc(s.serveHTTP) +} + +func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + if r.Method == http.MethodGet && r.URL.Path == "/v1/health" { + s.writeJSON(w, http.StatusOK, readmodel.Envelope[readmodel.Health]{ + Data: readmodel.Health{ + Service: "concord-api", + Network: s.Network, + ChainID: s.ChainID, + Configured: s.Reader != nil && s.FacilityAddress != (common.Address{}) && s.RegistryAddress != (common.Address{}), + ReadModel: map[bool]string{s.Reader != nil: "chain", false: "unavailable"}[s.Reader != nil], + FacilityAddress: s.FacilityAddress.Hex(), + RegistryAddress: s.RegistryAddress.Hex(), + }, + Meta: s.meta(r.Context(), readmodel.Observed, readmodel.SourceDerived, ""), + }) + return + } + + if r.Method == http.MethodPost && r.URL.Path == "/v1/transactions/prepare" { + s.prepare(w, r) + return + } + if r.Method != http.MethodGet { + s.writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "only GET and POST are supported") + return + } + + parts := splitPath(r.URL.Path) + if len(parts) == 3 && parts[0] == "v1" && parts[1] == "facilities" { + rootID, err := parseBytes32(parts[2]) + if err != nil { + s.writeError(w, http.StatusBadRequest, "invalid_root_accord_id", err.Error()) + return + } + facility, err := s.Reader.Facility(r.Context(), rootID) + if err != nil { + s.writeError(w, http.StatusNotFound, "not_observed", err.Error()) + return + } + s.writeJSON(w, http.StatusOK, readmodel.Envelope[readmodel.Facility]{Data: facility, Meta: s.meta(r.Context(), readmodel.Observed, readmodel.SourceOnchain, "")}) + return + } + if len(parts) == 4 && parts[0] == "v1" && parts[1] == "facilities" && parts[3] == "lineage" { + rootID, err := parseBytes32(parts[2]) + if err != nil { + s.writeError(w, http.StatusBadRequest, "invalid_root_accord_id", err.Error()) + return + } + lineage, err := s.Reader.Lineage(r.Context(), rootID) + if err != nil { + s.writeError(w, http.StatusNotFound, "not_observed", err.Error()) + return + } + s.writeJSON(w, http.StatusOK, readmodel.Envelope[readmodel.Lineage]{Data: lineage, Meta: s.meta(r.Context(), readmodel.Observed, readmodel.SourceOnchain, "")}) + return + } + if len(parts) == 3 && parts[0] == "v1" && parts[1] == "rounds" { + id, err := parseBytes32(parts[2]) + if err != nil { + s.writeError(w, http.StatusBadRequest, "invalid_round_id", err.Error()) + return + } + round, err := s.Reader.Round(r.Context(), id) + if err != nil { + s.writeError(w, http.StatusNotFound, "not_observed", err.Error()) + return + } + s.writeJSON(w, http.StatusOK, readmodel.Envelope[readmodel.Round]{Data: round, Meta: s.meta(r.Context(), readmodel.Observed, readmodel.SourceOnchain, "")}) + return + } + if len(parts) == 3 && parts[0] == "v1" && parts[1] == "draws" { + id, err := parseBytes32(parts[2]) + if err != nil { + s.writeError(w, http.StatusBadRequest, "invalid_draw_id", err.Error()) + return + } + draw, err := s.Reader.Draw(r.Context(), id) + if err != nil { + s.writeError(w, http.StatusNotFound, "not_observed", err.Error()) + return + } + s.writeJSON(w, http.StatusOK, readmodel.Envelope[readmodel.Draw]{Data: draw, Meta: s.meta(r.Context(), readmodel.Observed, readmodel.SourceOnchain, "")}) + return + } + if len(parts) == 3 && parts[0] == "v1" && parts[1] == "evidence" { + s.writeError(w, http.StatusNotFound, "not_observed", "FCC evidence storage is not configured for this API instance") + return + } + s.writeError(w, http.StatusNotFound, "not_found", "route not found") +} + +func (s *Server) prepare(w http.ResponseWriter, r *http.Request) { + var req PrepareRequest + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&req); err != nil { + s.writeError(w, http.StatusBadRequest, "invalid_request", fmt.Sprintf("decode request: %v", err)) + return + } + intent, err := s.buildIntent(r.Context(), req) + if err != nil { + s.writeError(w, http.StatusBadRequest, "invalid_intent", err.Error()) + return + } + s.writeJSON(w, http.StatusOK, readmodel.Envelope[readmodel.TransactionIntent]{Data: intent, Meta: s.meta(r.Context(), readmodel.Observed, readmodel.SourceDerived, "unsigned intent only")}) +} + +func (s *Server) buildIntent(ctx context.Context, req PrepareRequest) (readmodel.TransactionIntent, error) { + if req.Actor == "agent" { + // Agents can prepare, but this explicit marker makes the approval boundary + // visible in logs and downstream UI. It does not grant authority. + } + var data []byte + var to common.Address + var summary string + preconditions := []string{"The connected wallet or institution signer must review and approve this unsigned intent."} + amountValue, err := parseBig(req.Amount, "amount") + if req.Action != "create_root" && err != nil { + return readmodel.TransactionIntent{}, err + } + switch req.Action { + case "create_root": + rootID, err := parseBytes32(req.RootAccordID) + if err != nil { + return readmodel.TransactionIntent{}, err + } + target, err := parseBig(req.TargetCapacity, "targetCapacity") + if err != nil { + return readmodel.TransactionIntent{}, err + } + validUntil, err := parseUint64(req.ValidUntilUnix, "validUntilUnix") + if err != nil { + return readmodel.TransactionIntent{}, err + } + policyHash, err := parseBytes32(req.PolicyHash) + if err != nil { + return readmodel.TransactionIntent{}, err + } + data, err = s.FacilityABI.Pack("createRootAccord", rootID, target, validUntil, policyHash) + summary = "Create a new Root Accord with the supplied target, expiry, and policy commitment." + preconditions = append(preconditions, "The root Accord ID must not already exist.", "The expiry must be in the future.") + case "lock_collateral": + rootID, err := parseBytes32(req.RootAccordID) + if err != nil { + return readmodel.TransactionIntent{}, err + } + data, err = s.FacilityABI.Pack("lockCollateral", rootID, amountValue) + summary = "Lock FXRP collateral under the Root Accord." + preconditions = append(preconditions, "The signer must be the Root Accord borrower.", "The Root Accord must be PROPOSED.", "The FXRP allowance and balance must cover the amount.") + case "approve_asset": + asset, err := parseAddress(req.Asset, "asset") + if err != nil { + return readmodel.TransactionIntent{}, err + } + spender, err := parseAddress(req.Spender, "spender") + if err != nil { + return readmodel.TransactionIntent{}, err + } + data, err = s.TokenABI.Pack("approve", spender, amountValue) + to = asset + summary = "Approve the facility contract to use the specified token amount." + preconditions = append(preconditions, "Review the token, spender, and allowance amount before signing.") + case "fund_child": + childID, err := parseBytes32(req.ChildAccordID) + if err != nil { + return readmodel.TransactionIntent{}, err + } + data, err = s.FacilityABI.Pack("fundChild", childID, amountValue) + summary = "Fund the selected Child Accord with USDT0." + preconditions = append(preconditions, "The signer must be the selected provider.", "The child must be SELECTED or FUNDED.", "The amount must not exceed selected capacity.") + case "draw": + rootID, err := parseBytes32(req.RootAccordID) + if err != nil { + return readmodel.TransactionIntent{}, err + } + drawID, err := parseBytes32(req.DrawID) + if err != nil { + return readmodel.TransactionIntent{}, err + } + if s.Reader != nil { + facility, readErr := s.Reader.Facility(ctx, rootID) + if readErr != nil { + return readmodel.TransactionIntent{}, fmt.Errorf("read draw preconditions: %w", readErr) + } + available := mustBig(facility.AvailableCapacity) + if amountValue.Cmp(available) > 0 { + return readmodel.TransactionIntent{}, fmt.Errorf("draw amount %s exceeds observed available capacity %s", amountValue, available) + } + preconditions = append(preconditions, fmt.Sprintf("Observed available capacity: %s.", facility.AvailableCapacity)) + } + data, err = s.FacilityABI.Pack("draw", drawID, rootID, amountValue) + summary = "Draw USDT0 from the active Root Accord." + preconditions = append(preconditions, "The signer must be the treasury borrower.", "The Root Accord must be ACTIVE.", "The draw consumes explicit child DrawLegs onchain.") + case "repay": + drawID, err := parseBytes32(req.DrawID) + if err != nil { + return readmodel.TransactionIntent{}, err + } + if s.Reader != nil { + draw, readErr := s.Reader.Draw(ctx, drawID) + if readErr != nil { + return readmodel.TransactionIntent{}, fmt.Errorf("read repayment preconditions: %w", readErr) + } + outstanding := mustBig(draw.OutstandingPrincipal) + if amountValue.Cmp(outstanding) > 0 { + return readmodel.TransactionIntent{}, fmt.Errorf("repayment amount %s exceeds observed outstanding principal %s", amountValue, outstanding) + } + preconditions = append(preconditions, fmt.Sprintf("Observed outstanding principal: %s.", draw.OutstandingPrincipal)) + } + data, err = s.FacilityABI.Pack("repay", drawID, amountValue) + summary = "Repay principal on the Root Accord draw." + preconditions = append(preconditions, "The signer must be the treasury borrower.", "The liquidity-token allowance and balance must cover the amount.", "Capacity returns only after the ERC-20 transfer succeeds.") + default: + return readmodel.TransactionIntent{}, fmt.Errorf("unsupported action %q", req.Action) + } + if err != nil { + return readmodel.TransactionIntent{}, fmt.Errorf("encode %s: %w", req.Action, err) + } + if to == (common.Address{}) { + to = s.FacilityAddress + } + return readmodel.TransactionIntent{ + Action: req.Action, + ChainID: s.ChainID, + To: to.Hex(), + Data: "0x" + fmt.Sprintf("%x", data), + Value: "0", + Summary: summary, + RequiresExplicitApproval: true, + Preconditions: preconditions, + }, nil +} + +func (s *Server) meta(ctx context.Context, status readmodel.ObservationStatus, source readmodel.ObservationSource, warning string) readmodel.Meta { + return readmodel.Meta{ + APIVersion: readmodel.APIVersion, + Observation: readmodel.Observation{ + Status: status, + Source: source, + Network: s.Network, + ChainID: s.ChainID, + ObservedAt: time.Now().UTC(), + Warning: warning, + }, + } +} + +func (s *Server) writeJSON(w http.ResponseWriter, status int, value any) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(value) +} + +func (s *Server) writeError(w http.ResponseWriter, status int, code, message string) { + s.writeJSON(w, status, readmodel.ErrorResponse{ + Error: readmodel.ErrorBody{Code: code, Message: message}, + Meta: s.meta(context.Background(), readmodel.NotObserved, readmodel.SourceDerived, "no authoritative success was observed"), + }) +} + +var bytes32Pattern = regexp.MustCompile(`^0x[0-9a-fA-F]{64}$`) +var addressPattern = regexp.MustCompile(`^0x[0-9a-fA-F]{40}$`) + +func parseBytes32(value string) ([32]byte, error) { + if !bytes32Pattern.MatchString(value) { + return [32]byte{}, fmt.Errorf("expected a 32-byte 0x-prefixed hex value") + } + return common.HexToHash(value), nil +} + +func parseAddress(value, name string) (common.Address, error) { + if !addressPattern.MatchString(value) { + return common.Address{}, fmt.Errorf("%s must be a 20-byte 0x-prefixed hex address", name) + } + return common.HexToAddress(value), nil +} + +func parseBig(value, name string) (*big.Int, error) { + if value == "" { + return nil, fmt.Errorf("%s is required", name) + } + parsed, ok := new(big.Int).SetString(value, 10) + if !ok || parsed.Sign() < 0 { + return nil, fmt.Errorf("%s must be a non-negative base-10 integer", name) + } + if parsed.Sign() == 0 { + return nil, fmt.Errorf("%s must be greater than zero", name) + } + return parsed, nil +} + +func parseUint64(value, name string) (uint64, error) { + parsed, err := new(big.Int).SetString(value, 10) + if !err || parsed.Sign() < 0 || !parsed.IsUint64() { + return 0, fmt.Errorf("%s must be a non-negative uint64 in base-10 notation", name) + } + return parsed.Uint64(), nil +} + +func splitPath(path string) []string { + trimmed := strings.Trim(path, "/") + if trimmed == "" { + return nil + } + parts := strings.Split(trimmed, "/") + return parts +} + +func mustBig(value string) *big.Int { + parsed, ok := new(big.Int).SetString(value, 10) + if !ok { + return big.NewInt(0) + } + return parsed +} diff --git a/tools/pkg/api/server_test.go b/tools/pkg/api/server_test.go new file mode 100644 index 0000000..618ce7b --- /dev/null +++ b/tools/pkg/api/server_test.go @@ -0,0 +1,123 @@ +package api + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "concord/tools/pkg/readmodel" + "github.com/ethereum/go-ethereum/common" +) + +type fakeReader struct { + facility readmodel.Facility + round readmodel.Round + draw readmodel.Draw + lineage readmodel.Lineage +} + +func (f fakeReader) Facility(context.Context, [32]byte) (readmodel.Facility, error) { + return f.facility, nil +} +func (f fakeReader) Round(context.Context, [32]byte) (readmodel.Round, error) { return f.round, nil } +func (f fakeReader) Draw(context.Context, [32]byte) (readmodel.Draw, error) { return f.draw, nil } +func (f fakeReader) Lineage(context.Context, [32]byte) (readmodel.Lineage, error) { + return f.lineage, nil +} + +func testServer(t *testing.T) *httptest.Server { + t.Helper() + facilityID := "0x" + strings.Repeat("1", 64) + server, err := NewServer(fakeReader{ + facility: readmodel.Facility{ + ID: facilityID, + Borrower: "0x" + strings.Repeat("2", 40), + TargetCapacity: "100", + CommittedCapacity: "100", + DrawnPrincipal: "10", + AvailableCapacity: "90", + ValidUntil: time.Now().Add(time.Hour), + State: readmodel.RootActive, + Children: []readmodel.ChildAccord{{ + ID: "0x" + strings.Repeat("3", 64), + SelectedCapacity: "100", + CommittedCapacity: "100", + DrawnPrincipal: "10", + State: readmodel.ChildActive, + }}, + }, + }, "coston2", 114, common.HexToAddress("0x"+strings.Repeat("4", 40)), common.HexToAddress("0x"+strings.Repeat("5", 40))) + if err != nil { + t.Fatal(err) + } + return httptest.NewServer(server.Handler()) +} + +func TestGetFacilityReturnsSharedState(t *testing.T) { + server := testServer(t) + defer server.Close() + response, err := http.Get(server.URL + "/v1/facilities/0x" + strings.Repeat("1", 64)) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + t.Fatalf("status = %d", response.StatusCode) + } + var body struct { + Data readmodel.Facility `json:"data"` + Meta readmodel.Meta `json:"meta"` + } + if err := json.NewDecoder(response.Body).Decode(&body); err != nil { + t.Fatal(err) + } + if body.Data.State != readmodel.RootActive || body.Data.AvailableCapacity != "90" { + t.Fatalf("unexpected facility projection: %+v", body.Data) + } + if body.Meta.Observation.Status != readmodel.Observed { + t.Fatalf("unexpected observation: %+v", body.Meta.Observation) + } +} + +func TestPrepareDrawRejectsObservedCapacityViolation(t *testing.T) { + server := testServer(t) + defer server.Close() + body := `{"action":"draw","rootAccordId":"0x` + strings.Repeat("1", 64) + `","drawId":"0x` + strings.Repeat("6", 64) + `","amount":"91","actor":"treasury"}` + response, err := http.Post(server.URL+"/v1/transactions/prepare", "application/json", strings.NewReader(body)) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusBadRequest { + t.Fatalf("status = %d", response.StatusCode) + } +} + +func TestPrepareDrawReturnsUnsignedIntent(t *testing.T) { + server := testServer(t) + defer server.Close() + body := `{"action":"draw","rootAccordId":"0x` + strings.Repeat("1", 64) + `","drawId":"0x` + strings.Repeat("6", 64) + `","amount":"90","actor":"agent"}` + response, err := http.Post(server.URL+"/v1/transactions/prepare", "application/json", strings.NewReader(body)) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + data, _ := io.ReadAll(response.Body) + t.Fatalf("status = %d body = %s", response.StatusCode, data) + } + var bodyResponse struct { + Data readmodel.TransactionIntent `json:"data"` + } + if err := json.NewDecoder(response.Body).Decode(&bodyResponse); err != nil { + t.Fatal(err) + } + if !bodyResponse.Data.RequiresExplicitApproval || !strings.HasPrefix(bodyResponse.Data.Data, "0x") { + t.Fatalf("unexpected intent: %+v", bodyResponse.Data) + } +} diff --git a/tools/pkg/mcpserver/server.go b/tools/pkg/mcpserver/server.go new file mode 100644 index 0000000..b3ad689 --- /dev/null +++ b/tools/pkg/mcpserver/server.go @@ -0,0 +1,259 @@ +package mcpserver + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +// Server is a small stdio MCP adapter over Concord's REST control plane. It +// deliberately exposes read resources and unsigned transaction preparation; +// it has no wallet, signer, broadcast, or verifier capability. +type Server struct { + APIBaseURL string + HTTPClient *http.Client + Input io.Reader + Output io.Writer +} + +type rpcRequest struct { + JSONRPC string `json:"jsonrpc"` + ID json.RawMessage `json:"id"` + Method string `json:"method"` + Params json.RawMessage `json:"params,omitempty"` +} + +type rpcResponse struct { + JSONRPC string `json:"jsonrpc"` + ID json.RawMessage `json:"id,omitempty"` + Result any `json:"result,omitempty"` + Error *rpcError `json:"error,omitempty"` +} + +type rpcError struct { + Code int `json:"code"` + Message string `json:"message"` + Data any `json:"data,omitempty"` +} + +type toolCallResult struct { + Content []contentBlock `json:"content"` + IsError bool `json:"isError,omitempty"` +} + +type contentBlock struct { + Type string `json:"type"` + Text string `json:"text,omitempty"` + URI string `json:"uri,omitempty"` + MimeType string `json:"mimeType,omitempty"` +} + +func (s *Server) Serve(ctx context.Context) error { + if s.Input == nil || s.Output == nil { + return fmt.Errorf("MCP input and output are required") + } + if s.APIBaseURL == "" { + return fmt.Errorf("Concord API base URL is required") + } + if s.HTTPClient == nil { + s.HTTPClient = &http.Client{Timeout: 30 * time.Second} + } + scanner := bufio.NewScanner(s.Input) + scanner.Buffer(make([]byte, 1024), 4*1024*1024) + encoder := json.NewEncoder(s.Output) + for scanner.Scan() { + line := bytes.TrimSpace(scanner.Bytes()) + if len(line) == 0 { + continue + } + var request rpcRequest + if err := json.Unmarshal(line, &request); err != nil { + if err := encoder.Encode(rpcResponse{JSONRPC: "2.0", Error: &rpcError{Code: -32700, Message: "parse error"}}); err != nil { + return err + } + continue + } + response := s.handle(ctx, request) + // Notifications have no id and do not receive a response. + if len(request.ID) == 0 || string(request.ID) == "null" { + continue + } + if err := encoder.Encode(response); err != nil { + return err + } + } + return scanner.Err() +} + +func (s *Server) handle(ctx context.Context, request rpcRequest) rpcResponse { + response := rpcResponse{JSONRPC: "2.0", ID: request.ID} + result, err := s.dispatch(ctx, request.Method, request.Params) + if err != nil { + response.Error = &rpcError{Code: -32602, Message: err.Error()} + return response + } + response.Result = result + return response +} + +func (s *Server) dispatch(ctx context.Context, method string, params json.RawMessage) (any, error) { + switch method { + case "initialize": + return map[string]any{ + "protocolVersion": "2025-06-18", + "capabilities": map[string]any{ + "resources": map[string]any{"subscribe": false, "listChanged": false}, + "tools": map[string]any{"listChanged": false}, + }, + "serverInfo": map[string]string{"name": "concord-mcp", "version": "0.1.0"}, + }, nil + case "ping": + return map[string]any{}, nil + case "resources/list": + return map[string]any{"resources": []any{}, "resourceTemplates": resourceTemplates()}, nil + case "resources/read": + var input struct { + URI string `json:"uri"` + } + if err := json.Unmarshal(params, &input); err != nil || input.URI == "" { + return nil, fmt.Errorf("resources/read requires uri") + } + body, err := s.readResource(ctx, input.URI) + if err != nil { + return nil, err + } + return map[string]any{"contents": []contentBlock{{Type: "resource", URI: input.URI, MimeType: "application/json", Text: string(body)}}}, nil + case "tools/list": + return map[string]any{"tools": tools()}, nil + case "tools/call": + var input struct { + Name string `json:"name"` + Arguments map[string]any `json:"arguments"` + } + if err := json.Unmarshal(params, &input); err != nil || input.Name == "" { + return nil, fmt.Errorf("tools/call requires name") + } + body, err := s.callTool(ctx, input.Name, input.Arguments) + if err != nil { + return toolCallResult{Content: []contentBlock{{Type: "text", Text: err.Error()}}, IsError: true}, nil + } + return toolCallResult{Content: []contentBlock{{Type: "text", Text: string(body)}}}, nil + default: + return nil, fmt.Errorf("unsupported MCP method %q", method) + } +} + +func resourceTemplates() []map[string]any { + return []map[string]any{ + {"uriTemplate": "concord://facility/{rootAccordId}", "name": "facility", "description": "Observed Root Accord, round, and child state"}, + {"uriTemplate": "concord://facility/{rootAccordId}/lineage", "name": "facility-lineage", "description": "Observed causal lineage rooted at a Root Accord"}, + {"uriTemplate": "concord://round/{roundId}", "name": "round", "description": "Observed Makkari round metadata without private quotes"}, + {"uriTemplate": "concord://draw/{drawId}", "name": "draw", "description": "Observed draw with explicit DrawLegs"}, + } +} + +func tools() []map[string]any { + return []map[string]any{ + {"name": "get_facility", "description": "Read one Root Accord and its observed children. Private losing quotes remain withheld.", "inputSchema": objectSchema("rootAccordId")}, + {"name": "get_lineage", "description": "Read the causal relationship graph for one Root Accord.", "inputSchema": objectSchema("rootAccordId")}, + {"name": "get_round", "description": "Read one public Makkari round summary without private quote data.", "inputSchema": objectSchema("roundId")}, + {"name": "get_draw", "description": "Read one draw and its explicit child DrawLegs.", "inputSchema": objectSchema("drawId")}, + {"name": "prepare_transaction", "description": "Prepare unsigned calldata for a reviewed action. This tool never signs or broadcasts.", "inputSchema": map[string]any{"type": "object", "required": []string{"action"}, "properties": map[string]any{ + "action": map[string]any{"type": "string", "enum": []string{"create_root", "lock_collateral", "approve_asset", "fund_child", "draw", "repay"}}, + "rootAccordId": map[string]string{"type": "string"}, "childAccordId": map[string]string{"type": "string"}, "drawId": map[string]string{"type": "string"}, + "targetCapacity": map[string]string{"type": "string"}, "amount": map[string]string{"type": "string"}, "validUntilUnix": map[string]string{"type": "string"}, + "policyHash": map[string]string{"type": "string"}, "asset": map[string]string{"type": "string"}, "spender": map[string]string{"type": "string"}, + "actor": map[string]any{"type": "string", "enum": []string{"treasury", "provider", "institution", "agent"}}, + }}}, + } +} + +func objectSchema(property string) map[string]any { + return map[string]any{"type": "object", "required": []string{property}, "properties": map[string]any{property: map[string]string{"type": "string"}}} +} + +func (s *Server) readResource(ctx context.Context, uri string) ([]byte, error) { + if strings.HasPrefix(uri, "concord://facility/") { + id := strings.TrimPrefix(uri, "concord://facility/") + if strings.HasSuffix(id, "/lineage") { + id = strings.TrimSuffix(id, "/lineage") + return s.request(ctx, http.MethodGet, "/v1/facilities/"+id+"/lineage", nil) + } + return s.request(ctx, http.MethodGet, "/v1/facilities/"+id, nil) + } + if strings.HasPrefix(uri, "concord://round/") { + return s.request(ctx, http.MethodGet, "/v1/rounds/"+strings.TrimPrefix(uri, "concord://round/"), nil) + } + if strings.HasPrefix(uri, "concord://draw/") { + return s.request(ctx, http.MethodGet, "/v1/draws/"+strings.TrimPrefix(uri, "concord://draw/"), nil) + } + return nil, fmt.Errorf("unsupported Concord resource URI %q", uri) +} + +func (s *Server) callTool(ctx context.Context, name string, args map[string]any) ([]byte, error) { + var path string + switch name { + case "get_facility": + path = "/v1/facilities/" + stringArg(args, "rootAccordId") + case "get_lineage": + path = "/v1/facilities/" + stringArg(args, "rootAccordId") + "/lineage" + case "get_round": + path = "/v1/rounds/" + stringArg(args, "roundId") + case "get_draw": + path = "/v1/draws/" + stringArg(args, "drawId") + case "prepare_transaction": + encoded, err := json.Marshal(args) + if err != nil { + return nil, err + } + return s.request(ctx, http.MethodPost, "/v1/transactions/prepare", encoded) + default: + return nil, fmt.Errorf("unsupported Concord tool %q", name) + } + if path == "" || strings.HasSuffix(path, "/") { + return nil, fmt.Errorf("tool %s requires its identifier", name) + } + return s.request(ctx, http.MethodGet, path, nil) +} + +func stringArg(args map[string]any, name string) string { + value, ok := args[name].(string) + if !ok || value == "" { + return "" + } + return value +} + +func (s *Server) request(ctx context.Context, method, path string, body []byte) ([]byte, error) { + if s.HTTPClient == nil { + s.HTTPClient = &http.Client{Timeout: 30 * time.Second} + } + url := strings.TrimRight(s.APIBaseURL, "/") + path + request, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body)) + if err != nil { + return nil, err + } + request.Header.Set("Accept", "application/json") + if body != nil { + request.Header.Set("Content-Type", "application/json") + } + response, err := s.HTTPClient.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + data, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + if response.StatusCode < 200 || response.StatusCode >= 300 { + return nil, fmt.Errorf("Concord API returned HTTP %d: %s", response.StatusCode, strings.TrimSpace(string(data))) + } + return data, nil +} diff --git a/tools/pkg/mcpserver/server_test.go b/tools/pkg/mcpserver/server_test.go new file mode 100644 index 0000000..d2a802e --- /dev/null +++ b/tools/pkg/mcpserver/server_test.go @@ -0,0 +1,38 @@ +package mcpserver + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestServeSupportsResourcesAndReadTools(t *testing.T) { + api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"data":{"state":"ACTIVE"},"meta":{"observation":{"status":"observed"}}}`)) + })) + defer api.Close() + + id := "0x" + strings.Repeat("1", 64) + input := strings.NewReader(fmt.Sprintf( + "%s\n%s\n%s\n", + `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}`, + `{"jsonrpc":"2.0","id":2,"method":"resources/read","params":{"uri":"concord://facility/`+id+`"}}`, + `{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_facility","arguments":{"rootAccordId":"`+id+`"}}}`, + )) + var output strings.Builder + server := &Server{APIBaseURL: api.URL, Input: input, Output: &output} + if err := server.Serve(context.Background()); err != nil { + t.Fatal(err) + } + lines := strings.Split(strings.TrimSpace(output.String()), "\n") + if len(lines) != 3 { + t.Fatalf("expected 3 responses, got %d: %s", len(lines), output.String()) + } + if !strings.Contains(output.String(), "protocolVersion") || !strings.Contains(output.String(), "ACTIVE") { + t.Fatalf("unexpected MCP output: %s", output.String()) + } +} diff --git a/tools/pkg/readmodel/chain.go b/tools/pkg/readmodel/chain.go new file mode 100644 index 0000000..440cb28 --- /dev/null +++ b/tools/pkg/readmodel/chain.go @@ -0,0 +1,439 @@ +package readmodel + +import ( + "context" + "fmt" + "math/big" + "reflect" + "strings" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" +) + +// ChainReader is a read-only projection adapter. It never signs, broadcasts, +// or infers state when the configured chain did not return it. +type ChainReader struct { + client *ethclient.Client + facilityABI abi.ABI + registryABI abi.ABI + facilityAddress common.Address + registryAddress common.Address + collateralAsset Asset + liquidityAsset Asset + chainID int64 + network string +} + +type ChainReaderConfig struct { + RPCURL string + Network string + ChainID int64 + FacilityAddress common.Address + RegistryAddress common.Address + CollateralAsset Asset + LiquidityAsset Asset +} + +type rawRoot struct { + ID [32]byte `abi:"id"` + Borrower common.Address `abi:"borrower"` + CollateralAsset common.Address `abi:"collateralAsset"` + LiquidityAsset common.Address `abi:"liquidityAsset"` + TargetCapacity *big.Int `abi:"targetCapacity"` + CommittedCapacity *big.Int `abi:"committedCapacity"` + DrawnPrincipal *big.Int `abi:"drawnPrincipal"` + CollateralLocked *big.Int `abi:"collateralLocked"` + ValidUntil uint64 `abi:"validUntil"` + PolicyHash [32]byte `abi:"policyHash"` + SyndicationRoundID [32]byte `abi:"syndicationRoundId"` + State uint8 `abi:"state"` +} + +type rawChild struct { + ID [32]byte `abi:"id"` + RootID [32]byte `abi:"rootId"` + AllocationID [32]byte `abi:"allocationId"` + Provider common.Address `abi:"provider"` + SelectedCapacity *big.Int `abi:"selectedCapacity"` + CommittedCapacity *big.Int `abi:"committedCapacity"` + DrawnPrincipal *big.Int `abi:"drawnPrincipal"` + FeeBPS uint32 `abi:"feeBps"` + ValidUntil uint64 `abi:"validUntil"` + TermsCommitment [32]byte `abi:"termsCommitment"` + State uint8 `abi:"state"` +} + +type rawRound struct { + ID [32]byte `abi:"id"` + RootAccordID [32]byte `abi:"rootAccordId"` + TargetCapacity *big.Int `abi:"targetCapacity"` + MaxFeeBPS uint32 `abi:"maxFeeBps"` + RoundExpiry uint64 `abi:"roundExpiry"` + Status uint8 `abi:"status"` +} + +type rawDraw struct { + ID [32]byte `abi:"id"` + RootAccordID [32]byte `abi:"rootAccordId"` + Principal *big.Int `abi:"principal"` + RepaidPrincipal *big.Int `abi:"repaidPrincipal"` + CreatedAt uint64 `abi:"createdAt"` + Exists bool `abi:"exists"` +} + +type rawDrawLeg struct { + ID [32]byte `abi:"id"` + DrawID [32]byte `abi:"drawId"` + ChildAccordID [32]byte `abi:"childAccordId"` + Principal *big.Int `abi:"principal"` + RepaidPrincipal *big.Int `abi:"repaidPrincipal"` +} + +type rawNode struct { + ID [32]byte `abi:"id"` + ParentID [32]byte `abi:"parentId"` + Kind uint8 `abi:"kind"` + CreatedAt uint64 `abi:"createdAt"` + Exists bool `abi:"exists"` +} + +const capitalFacilityABIJSON = `[ + {"inputs":[{"name":"rootId","type":"bytes32"}],"name":"getRoot","outputs":[{"components":[{"name":"id","type":"bytes32"},{"name":"borrower","type":"address"},{"name":"collateralAsset","type":"address"},{"name":"liquidityAsset","type":"address"},{"name":"targetCapacity","type":"uint256"},{"name":"committedCapacity","type":"uint256"},{"name":"drawnPrincipal","type":"uint256"},{"name":"collateralLocked","type":"uint256"},{"name":"validUntil","type":"uint64"},{"name":"policyHash","type":"bytes32"},{"name":"syndicationRoundId","type":"bytes32"},{"name":"state","type":"uint8"}],"name":"","type":"tuple"}],"stateMutability":"view","type":"function"}, + {"inputs":[{"name":"childId","type":"bytes32"}],"name":"getChild","outputs":[{"components":[{"name":"id","type":"bytes32"},{"name":"rootId","type":"bytes32"},{"name":"allocationId","type":"bytes32"},{"name":"provider","type":"address"},{"name":"selectedCapacity","type":"uint256"},{"name":"committedCapacity","type":"uint256"},{"name":"drawnPrincipal","type":"uint256"},{"name":"feeBps","type":"uint32"},{"name":"validUntil","type":"uint64"},{"name":"termsCommitment","type":"bytes32"},{"name":"state","type":"uint8"}],"name":"","type":"tuple"}],"stateMutability":"view","type":"function"}, + {"inputs":[{"name":"roundId","type":"bytes32"}],"name":"rounds","outputs":[{"components":[{"name":"id","type":"bytes32"},{"name":"rootAccordId","type":"bytes32"},{"name":"targetCapacity","type":"uint256"},{"name":"maxFeeBps","type":"uint32"},{"name":"roundExpiry","type":"uint64"},{"name":"status","type":"uint8"}],"name":"","type":"tuple"}],"stateMutability":"view","type":"function"}, + {"inputs":[{"name":"rootId","type":"bytes32"}],"name":"getChildIds","outputs":[{"name":"","type":"bytes32[]"}],"stateMutability":"view","type":"function"}, + {"inputs":[{"name":"roundId","type":"bytes32"}],"name":"getEligibleProviders","outputs":[{"name":"","type":"address[]"}],"stateMutability":"view","type":"function"}, + {"inputs":[{"name":"drawId","type":"bytes32"}],"name":"draws","outputs":[{"components":[{"name":"id","type":"bytes32"},{"name":"rootAccordId","type":"bytes32"},{"name":"principal","type":"uint256"},{"name":"repaidPrincipal","type":"uint256"},{"name":"createdAt","type":"uint64"},{"name":"exists","type":"bool"}],"name":"","type":"tuple"}],"stateMutability":"view","type":"function"}, + {"inputs":[{"name":"drawId","type":"bytes32"}],"name":"getDrawLegIds","outputs":[{"name":"","type":"bytes32[]"}],"stateMutability":"view","type":"function"}, + {"inputs":[{"name":"legId","type":"bytes32"}],"name":"drawLegs","outputs":[{"components":[{"name":"id","type":"bytes32"},{"name":"drawId","type":"bytes32"},{"name":"childAccordId","type":"bytes32"},{"name":"principal","type":"uint256"},{"name":"repaidPrincipal","type":"uint256"}],"name":"","type":"tuple"}],"stateMutability":"view","type":"function"}, + {"inputs":[],"name":"collateralAsset","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"}, + {"inputs":[],"name":"liquidityAsset","outputs":[{"name":"","type":"address"}],"stateMutability":"view","type":"function"} +]` + +const registryABIJSON = `[ + {"inputs":[{"name":"id","type":"bytes32"}],"name":"nodes","outputs":[{"components":[{"name":"id","type":"bytes32"},{"name":"parentId","type":"bytes32"},{"name":"kind","type":"uint8"},{"name":"createdAt","type":"uint64"},{"name":"exists","type":"bool"}],"name":"","type":"tuple"}],"stateMutability":"view","type":"function"}, + {"inputs":[{"name":"parentId","type":"bytes32"}],"name":"getChildren","outputs":[{"name":"","type":"bytes32[]"}],"stateMutability":"view","type":"function"} +]` + +func NewChainReader(ctx context.Context, cfg ChainReaderConfig) (*ChainReader, error) { + if cfg.RPCURL == "" || cfg.Network == "" || cfg.ChainID == 0 { + return nil, fmt.Errorf("rpc URL, network, and chain ID are required") + } + if cfg.FacilityAddress == (common.Address{}) || cfg.RegistryAddress == (common.Address{}) { + return nil, fmt.Errorf("facility and registry addresses are required") + } + if cfg.CollateralAsset.Address == "" || cfg.LiquidityAsset.Address == "" { + return nil, fmt.Errorf("collateral and liquidity asset metadata are required") + } + client, err := ethclient.DialContext(ctx, cfg.RPCURL) + if err != nil { + return nil, fmt.Errorf("dial chain: %w", err) + } + chainID, err := client.ChainID(ctx) + if err != nil { + client.Close() + return nil, fmt.Errorf("read chain ID: %w", err) + } + if !chainID.IsInt64() || chainID.Int64() != cfg.ChainID { + client.Close() + return nil, fmt.Errorf("chain ID mismatch: configured %d, RPC returned %s", cfg.ChainID, chainID.String()) + } + facilityABI, err := abi.JSON(strings.NewReader(capitalFacilityABIJSON)) + if err != nil { + client.Close() + return nil, fmt.Errorf("parse facility ABI: %w", err) + } + registryABI, err := abi.JSON(strings.NewReader(registryABIJSON)) + if err != nil { + client.Close() + return nil, fmt.Errorf("parse registry ABI: %w", err) + } + return &ChainReader{ + client: client, + facilityABI: facilityABI, + registryABI: registryABI, + facilityAddress: cfg.FacilityAddress, + registryAddress: cfg.RegistryAddress, + collateralAsset: cfg.CollateralAsset, + liquidityAsset: cfg.LiquidityAsset, + chainID: cfg.ChainID, + network: cfg.Network, + }, nil +} + +func (r *ChainReader) Close() { r.client.Close() } + +func (r *ChainReader) ChainID() int64 { return r.chainID } + +func (r *ChainReader) Network() string { return r.network } + +func (r *ChainReader) call(ctx context.Context, contractABI abi.ABI, contractAddress common.Address, method string, dest interface{}, args ...interface{}) error { + if err := ctx.Err(); err != nil { + return err + } + data, err := contractABI.Pack(method, args...) + if err != nil { + return fmt.Errorf("pack %s: %w", method, err) + } + output, err := r.client.CallContract(ctx, ethereum.CallMsg{To: &contractAddress, Data: data}, nil) + if err != nil { + return fmt.Errorf("call %s: %w", method, err) + } + values, err := contractABI.Unpack(method, output) + if err != nil { + return fmt.Errorf("unpack %s: %w", method, err) + } + if len(values) != 1 { + return fmt.Errorf("unpack %s: expected one projected output, got %d", method, len(values)) + } + converted := reflect.ValueOf(abi.ConvertType(values[0], dest)) + destination := reflect.ValueOf(dest) + if !converted.IsValid() || !destination.IsValid() || converted.Type() != destination.Type() { + return fmt.Errorf("unpack %s: output type %T cannot be assigned to %T", method, values[0], dest) + } + destination.Elem().Set(converted.Elem()) + return nil +} + +// The wrappers below keep the call sites readable and avoid exposing ABI +// plumbing to the API and MCP layers. +func (r *ChainReader) facilityCall(ctx context.Context, method string, dest interface{}, args ...interface{}) error { + return r.call(ctx, r.facilityABI, r.facilityAddress, method, dest, args...) +} + +func (r *ChainReader) registryCall(ctx context.Context, method string, dest interface{}, args ...interface{}) error { + return r.call(ctx, r.registryABI, r.registryAddress, method, dest, args...) +} + +func (r *ChainReader) Facility(ctx context.Context, rootID [32]byte) (Facility, error) { + var root rawRoot + if err := r.facilityCall(ctx, "getRoot", &root, rootID); err != nil { + return Facility{}, fmt.Errorf("read root Accord %s: %w", bytes32String(rootID), err) + } + childIDs := [][32]byte{} + if err := r.facilityCall(ctx, "getChildIds", &childIDs, rootID); err != nil { + return Facility{}, fmt.Errorf("read child Accords: %w", err) + } + children := make([]ChildAccord, 0, len(childIDs)) + for _, childID := range childIDs { + child, err := r.child(ctx, childID) + if err != nil { + return Facility{}, err + } + children = append(children, child) + } + + rootState := stateFromRoot(root.State) + if !strings.EqualFold(root.CollateralAsset.Hex(), r.collateralAsset.Address) { + return Facility{}, fmt.Errorf("facility collateral asset %s does not match configured %s", root.CollateralAsset.Hex(), r.collateralAsset.Address) + } + if !strings.EqualFold(root.LiquidityAsset.Hex(), r.liquidityAsset.Address) { + return Facility{}, fmt.Errorf("facility liquidity asset %s does not match configured %s", root.LiquidityAsset.Hex(), r.liquidityAsset.Address) + } + committedChildren := big.NewInt(0) + childExposure := big.NewInt(0) + for _, child := range children { + if child.State != ChildClosed && child.State != ChildExpired { + committedChildren.Add(committedChildren, mustBig(child.CommittedCapacity)) + } + childExposure.Add(childExposure, mustBig(child.DrawnPrincipal)) + } + available := new(big.Int).Sub(root.CommittedCapacity, root.DrawnPrincipal) + return Facility{ + ID: bytes32String(root.ID), + Borrower: root.Borrower.Hex(), + CollateralAsset: r.collateralAsset, + LiquidityAsset: r.liquidityAsset, + TargetCapacity: amount(root.TargetCapacity), + CommittedCapacity: amount(root.CommittedCapacity), + DrawnPrincipal: amount(root.DrawnPrincipal), + CollateralLocked: amount(root.CollateralLocked), + AvailableCapacity: amount(available), + ValidUntil: timestamp(root.ValidUntil), + ValidUntilUnix: amount(new(big.Int).SetUint64(root.ValidUntil)), + PolicyHash: bytes32String(root.PolicyHash), + SyndicationRoundID: bytes32String(root.SyndicationRoundID), + State: rootState, + StateCopy: rootCopy(rootState), + Round: r.roundIfPresent(ctx, root.SyndicationRoundID), + Children: children, + Invariants: Invariants{ + RootDrawWithinCommitment: root.DrawnPrincipal.Cmp(root.CommittedCapacity) <= 0, + ChildDrawWithinCommitment: allChildrenWithinCommitment(children), + RootExposureMatchesChildren: root.DrawnPrincipal.Cmp(childExposure) == 0, + CommittedMatchesFundedChildren: root.CommittedCapacity.Cmp(committedChildren) == 0, + }, + }, nil +} + +func (r *ChainReader) child(ctx context.Context, childID [32]byte) (ChildAccord, error) { + var child rawChild + if err := r.facilityCall(ctx, "getChild", &child, childID); err != nil { + return ChildAccord{}, fmt.Errorf("read child Accord %s: %w", bytes32String(childID), err) + } + state := stateFromChild(child.State) + available := new(big.Int).Sub(child.CommittedCapacity, child.DrawnPrincipal) + return ChildAccord{ + ID: bytes32String(child.ID), + RootAccordID: bytes32String(child.RootID), + AllocationID: bytes32String(child.AllocationID), + Provider: child.Provider.Hex(), + SelectedCapacity: amount(child.SelectedCapacity), + CommittedCapacity: amount(child.CommittedCapacity), + DrawnPrincipal: amount(child.DrawnPrincipal), + AvailableCapacity: amount(available), + FeeBPS: child.FeeBPS, + ValidUntil: timestamp(child.ValidUntil), + ValidUntilUnix: amount(new(big.Int).SetUint64(child.ValidUntil)), + TermsCommitment: bytes32String(child.TermsCommitment), + State: state, + StateCopy: childCopy(state), + }, nil +} + +func (r *ChainReader) Round(ctx context.Context, roundID [32]byte) (Round, error) { + var raw rawRound + if err := r.facilityCall(ctx, "rounds", &raw, roundID); err != nil { + return Round{}, fmt.Errorf("read round %s: %w", bytes32String(roundID), err) + } + providers := []common.Address{} + if err := r.facilityCall(ctx, "getEligibleProviders", &providers, roundID); err != nil { + return Round{}, fmt.Errorf("read eligible provider count: %w", err) + } + status := stateFromRound(raw.Status) + return Round{ + ID: bytes32String(raw.ID), + RootAccordID: bytes32String(raw.RootAccordID), + TargetCapacity: amount(raw.TargetCapacity), + MaxFeeBPS: raw.MaxFeeBPS, + RoundExpiry: timestamp(raw.RoundExpiry), + RoundExpiryUnix: amount(new(big.Int).SetUint64(raw.RoundExpiry)), + Status: status, + StateCopy: roundCopy(status), + EligibleProviderCount: len(providers), + PrivateQuoteData: "withheld", + }, nil +} + +func (r *ChainReader) roundIfPresent(ctx context.Context, roundID [32]byte) *Round { + if roundID == ([32]byte{}) { + return nil + } + round, err := r.Round(ctx, roundID) + if err != nil { + return nil + } + return &round +} + +func (r *ChainReader) Draw(ctx context.Context, drawID [32]byte) (Draw, error) { + var raw rawDraw + if err := r.facilityCall(ctx, "draws", &raw, drawID); err != nil { + return Draw{}, fmt.Errorf("read draw %s: %w", bytes32String(drawID), err) + } + if !raw.Exists { + return Draw{}, fmt.Errorf("draw %s was not observed", bytes32String(drawID)) + } + legIDs := [][32]byte{} + if err := r.facilityCall(ctx, "getDrawLegIds", &legIDs, drawID); err != nil { + return Draw{}, fmt.Errorf("read draw legs: %w", err) + } + legs := make([]DrawLeg, 0, len(legIDs)) + for _, legID := range legIDs { + var leg rawDrawLeg + if err := r.facilityCall(ctx, "drawLegs", &leg, legID); err != nil { + return Draw{}, fmt.Errorf("read draw leg %s: %w", bytes32String(legID), err) + } + child, err := r.child(ctx, leg.ChildAccordID) + if err != nil { + return Draw{}, err + } + outstanding := new(big.Int).Sub(leg.Principal, leg.RepaidPrincipal) + legs = append(legs, DrawLeg{ + ID: bytes32String(leg.ID), + DrawID: bytes32String(leg.DrawID), + ChildAccordID: bytes32String(leg.ChildAccordID), + Provider: child.Provider, + Principal: amount(leg.Principal), + RepaidPrincipal: amount(leg.RepaidPrincipal), + OutstandingPrincipal: amount(outstanding), + }) + } + outstanding := new(big.Int).Sub(raw.Principal, raw.RepaidPrincipal) + return Draw{ + ID: bytes32String(raw.ID), + RootAccordID: bytes32String(raw.RootAccordID), + Principal: amount(raw.Principal), + RepaidPrincipal: amount(raw.RepaidPrincipal), + OutstandingPrincipal: amount(outstanding), + CreatedAt: timestamp(raw.CreatedAt), + Legs: legs, + }, nil +} + +func (r *ChainReader) Lineage(ctx context.Context, rootID [32]byte) (Lineage, error) { + seen := map[[32]byte]bool{} + nodes := make([]LineageNode, 0) + var visit func([32]byte, int) error + visit = func(id [32]byte, depth int) error { + if depth > 256 { + return fmt.Errorf("lineage exceeds traversal depth") + } + if seen[id] { + return nil + } + seen[id] = true + var node rawNode + if err := r.registryCall(ctx, "nodes", &node, id); err != nil { + return err + } + if !node.Exists { + return fmt.Errorf("lineage node %s was not observed", bytes32String(id)) + } + children := [][32]byte{} + if err := r.registryCall(ctx, "getChildren", &children, id); err != nil { + return err + } + childStrings := make([]string, 0, len(children)) + for _, child := range children { + childStrings = append(childStrings, bytes32String(child)) + } + nodes = append(nodes, LineageNode{ + ID: bytes32String(node.ID), + ParentID: bytes32String(node.ParentID), + Kind: kindFromRegistry(node.Kind), + CreatedAt: timestamp(node.CreatedAt), + Children: childStrings, + }) + for _, child := range children { + if err := visit(child, depth+1); err != nil { + return err + } + } + return nil + } + if err := visit(rootID, 0); err != nil { + return Lineage{}, fmt.Errorf("read lineage: %w", err) + } + return Lineage{RootAccordID: bytes32String(rootID), Nodes: nodes}, nil +} + +func allChildrenWithinCommitment(children []ChildAccord) bool { + for _, child := range children { + if mustBig(child.DrawnPrincipal).Cmp(mustBig(child.CommittedCapacity)) > 0 { + return false + } + } + return true +} + +func mustBig(value string) *big.Int { + v, ok := new(big.Int).SetString(value, 10) + if !ok { + return big.NewInt(0) + } + return v +} diff --git a/tools/pkg/readmodel/chain_codec_test.go b/tools/pkg/readmodel/chain_codec_test.go new file mode 100644 index 0000000..80b9703 --- /dev/null +++ b/tools/pkg/readmodel/chain_codec_test.go @@ -0,0 +1,43 @@ +package readmodel + +import ( + "math/big" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +func TestCapitalFacilityTupleCodec(t *testing.T) { + parsed, err := abi.JSON(strings.NewReader(capitalFacilityABIJSON)) + if err != nil { + t.Fatal(err) + } + want := rawRoot{ + ID: common.HexToHash("0x" + strings.Repeat("1", 64)), + Borrower: common.HexToAddress("0x" + strings.Repeat("2", 40)), + CollateralAsset: common.HexToAddress("0x" + strings.Repeat("3", 40)), + LiquidityAsset: common.HexToAddress("0x" + strings.Repeat("4", 40)), + TargetCapacity: big.NewInt(100), + CommittedCapacity: big.NewInt(90), + DrawnPrincipal: big.NewInt(10), + CollateralLocked: big.NewInt(7), + ValidUntil: 123, + PolicyHash: common.HexToHash("0x" + strings.Repeat("5", 64)), + SyndicationRoundID: common.HexToHash("0x" + strings.Repeat("6", 64)), + State: 4, + } + encoded, err := parsed.Methods["getRoot"].Outputs.Pack(want) + if err != nil { + t.Fatal(err) + } + values, err := parsed.Unpack("getRoot", encoded) + if err != nil { + t.Fatal(err) + } + got := abi.ConvertType(values[0], new(rawRoot)).(*rawRoot) + if got.Borrower != want.Borrower || got.TargetCapacity.Cmp(want.TargetCapacity) != 0 || got.State != want.State { + t.Fatalf("decoded root mismatch: got %+v want %+v", got, want) + } +} diff --git a/tools/pkg/readmodel/model.go b/tools/pkg/readmodel/model.go new file mode 100644 index 0000000..8650c47 --- /dev/null +++ b/tools/pkg/readmodel/model.go @@ -0,0 +1,348 @@ +package readmodel + +import ( + "context" + "fmt" + "math/big" + "time" +) + +const APIVersion = "v1" + +type ObservationStatus string + +const ( + Observed ObservationStatus = "observed" + Partial ObservationStatus = "partial" + NotObserved ObservationStatus = "not_observed" +) + +type ObservationSource string + +const ( + SourceOnchain ObservationSource = "onchain" + SourceFCCEvidence ObservationSource = "fcc_evidence" + SourceDerived ObservationSource = "derived" +) + +type Observation struct { + Status ObservationStatus `json:"status"` + Source ObservationSource `json:"source"` + Network string `json:"network"` + ChainID int64 `json:"chainId"` + BlockNumber string `json:"blockNumber,omitempty"` + ObservedAt time.Time `json:"observedAt"` + Warning string `json:"warning,omitempty"` +} + +type Meta struct { + Observation Observation `json:"observation"` + APIVersion string `json:"apiVersion"` +} + +type Envelope[T any] struct { + Data T `json:"data"` + Meta Meta `json:"meta"` +} + +type ErrorBody struct { + Code string `json:"code"` + Message string `json:"message"` + Details map[string]any `json:"details,omitempty"` +} + +type ErrorResponse struct { + Error ErrorBody `json:"error"` + Meta Meta `json:"meta"` +} + +type RootState string + +const ( + RootNone RootState = "NONE" + RootProposed RootState = "PROPOSED" + RootSyndicating RootState = "SYNDICATING" + RootFunding RootState = "FUNDING" + RootActive RootState = "ACTIVE" + RootClosed RootState = "CLOSED" + RootFrozen RootState = "FROZEN" + RootExpired RootState = "EXPIRED" +) + +type ChildState string + +const ( + ChildNone ChildState = "NONE" + ChildSelected ChildState = "SELECTED" + ChildFunded ChildState = "FUNDED" + ChildActive ChildState = "ACTIVE" + ChildClosed ChildState = "CLOSED" + ChildExpired ChildState = "EXPIRED" + ChildDefaulted ChildState = "DEFAULTED" +) + +type RoundStatus string + +const ( + RoundNone RoundStatus = "NONE" + RoundOpen RoundStatus = "OPEN" + RoundFinalized RoundStatus = "FINALIZED" + RoundExpired RoundStatus = "EXPIRED" +) + +type LineageKind string + +const ( + KindRootAccord LineageKind = "ROOT_ACCORD" + KindMakkariSession LineageKind = "MAKKARI_SESSION" + KindCoFillAllocation LineageKind = "COFILL_ALLOCATION" + KindChildAccord LineageKind = "CHILD_ACCORD" + KindDraw LineageKind = "DRAW" + KindDrawLeg LineageKind = "DRAW_LEG" + KindSettlement LineageKind = "SETTLEMENT" + KindRepayment LineageKind = "REPAYMENT" +) + +type StateCopy struct { + State string `json:"state"` + Label string `json:"label"` + Explanation string `json:"explanation"` +} + +type Asset struct { + Address string `json:"address"` + Symbol string `json:"symbol"` + Decimals uint8 `json:"decimals"` +} + +type ChildAccord struct { + ID string `json:"id"` + RootAccordID string `json:"rootAccordId"` + AllocationID string `json:"allocationId"` + Provider string `json:"provider"` + SelectedCapacity string `json:"selectedCapacity"` + CommittedCapacity string `json:"committedCapacity"` + DrawnPrincipal string `json:"drawnPrincipal"` + AvailableCapacity string `json:"availableCapacity"` + FeeBPS uint32 `json:"feeBps"` + ValidUntil time.Time `json:"validUntil"` + ValidUntilUnix string `json:"validUntilUnix"` + TermsCommitment string `json:"termsCommitment"` + State ChildState `json:"state"` + StateCopy StateCopy `json:"stateCopy"` +} + +type Round struct { + ID string `json:"id"` + RootAccordID string `json:"rootAccordId"` + TargetCapacity string `json:"targetCapacity"` + MaxFeeBPS uint32 `json:"maxFeeBps"` + RoundExpiry time.Time `json:"roundExpiry"` + RoundExpiryUnix string `json:"roundExpiryUnix"` + Status RoundStatus `json:"status"` + StateCopy StateCopy `json:"stateCopy"` + EligibleProviderCount int `json:"eligibleProviderCount"` + PrivateQuoteData string `json:"privateQuoteData"` +} + +type Facility struct { + ID string `json:"id"` + Borrower string `json:"borrower"` + CollateralAsset Asset `json:"collateralAsset"` + LiquidityAsset Asset `json:"liquidityAsset"` + TargetCapacity string `json:"targetCapacity"` + CommittedCapacity string `json:"committedCapacity"` + DrawnPrincipal string `json:"drawnPrincipal"` + CollateralLocked string `json:"collateralLocked"` + AvailableCapacity string `json:"availableCapacity"` + ValidUntil time.Time `json:"validUntil"` + ValidUntilUnix string `json:"validUntilUnix"` + PolicyHash string `json:"policyHash"` + SyndicationRoundID string `json:"syndicationRoundId"` + State RootState `json:"state"` + StateCopy StateCopy `json:"stateCopy"` + Round *Round `json:"round,omitempty"` + Children []ChildAccord `json:"children"` + Invariants Invariants `json:"invariants"` +} + +type Invariants struct { + RootDrawWithinCommitment bool `json:"rootDrawWithinCommitment"` + ChildDrawWithinCommitment bool `json:"childDrawWithinCommitment"` + RootExposureMatchesChildren bool `json:"rootExposureMatchesChildren"` + CommittedMatchesFundedChildren bool `json:"committedMatchesFundedChildren"` +} + +type LineageNode struct { + ID string `json:"id"` + ParentID string `json:"parentId"` + Kind LineageKind `json:"kind"` + CreatedAt time.Time `json:"createdAt"` + Children []string `json:"children"` +} + +type Lineage struct { + RootAccordID string `json:"rootAccordId"` + Nodes []LineageNode `json:"nodes"` +} + +type DrawLeg struct { + ID string `json:"id"` + DrawID string `json:"drawId"` + ChildAccordID string `json:"childAccordId"` + Provider string `json:"provider"` + Principal string `json:"principal"` + RepaidPrincipal string `json:"repaidPrincipal"` + OutstandingPrincipal string `json:"outstandingPrincipal"` +} + +type Draw struct { + ID string `json:"id"` + RootAccordID string `json:"rootAccordId"` + Principal string `json:"principal"` + RepaidPrincipal string `json:"repaidPrincipal"` + OutstandingPrincipal string `json:"outstandingPrincipal"` + CreatedAt time.Time `json:"createdAt"` + Legs []DrawLeg `json:"legs"` +} + +type Evidence struct { + ResultDigest string `json:"resultDigest"` + Status string `json:"status"` + Disclosure string `json:"disclosure"` + ExtensionID string `json:"extensionId,omitempty"` + RoundID string `json:"roundId,omitempty"` + RootAccordID string `json:"rootAccordId,omitempty"` + InstructionID string `json:"instructionId,omitempty"` + Source string `json:"source"` + Warning string `json:"warning,omitempty"` +} + +type Health struct { + Service string `json:"service"` + Network string `json:"network"` + ChainID int64 `json:"chainId"` + Configured bool `json:"configured"` + ReadModel string `json:"readModel"` + FacilityAddress string `json:"facilityAddress,omitempty"` + RegistryAddress string `json:"registryAddress,omitempty"` +} + +type TransactionIntent struct { + Action string `json:"action"` + ChainID int64 `json:"chainId"` + To string `json:"to"` + Data string `json:"data"` + Value string `json:"value"` + Summary string `json:"summary"` + RequiresExplicitApproval bool `json:"requiresExplicitApproval"` + Preconditions []string `json:"preconditions"` + Warnings []string `json:"warnings,omitempty"` +} + +type Reader interface { + Facility(ctx context.Context, rootID [32]byte) (Facility, error) + Round(ctx context.Context, roundID [32]byte) (Round, error) + Draw(ctx context.Context, drawID [32]byte) (Draw, error) + Lineage(ctx context.Context, rootID [32]byte) (Lineage, error) +} + +func amount(v *big.Int) string { + if v == nil { + return "0" + } + return v.String() +} + +func timestamp(v uint64) time.Time { + return time.Unix(int64(v), 0).UTC() +} + +func bytes32String(v [32]byte) string { + return fmt.Sprintf("0x%x", v[:]) +} + +func rootCopy(state RootState) StateCopy { + switch state { + case RootProposed: + return StateCopy{string(state), "Draft facility", "The treasury relationship exists and can be prepared for collateral."} + case RootSyndicating: + return StateCopy{string(state), "Collecting private offers", "The bounded Makkari session is open for provider coordination."} + case RootFunding: + return StateCopy{string(state), "Waiting for provider funding", "CoFill selected children; selected capacity is not committed until USDT0 arrives."} + case RootActive: + return StateCopy{string(state), "Ready for draw", "The target capacity is funded and the treasury can draw within available capacity."} + case RootClosed: + return StateCopy{string(state), "Closed", "Exposure and commitments are zero and recoverable balances were returned."} + case RootFrozen: + return StateCopy{string(state), "Frozen", "New financial actions are not available while this relationship is frozen."} + case RootExpired: + return StateCopy{string(state), "Expired", "The relationship validity ended without outstanding exposure or commitments."} + default: + return StateCopy{string(state), "Unknown", "No authoritative state description is available."} + } +} + +func childCopy(state ChildState) StateCopy { + switch state { + case ChildSelected: + return StateCopy{string(state), "Selected", "CoFill selected this provider; no capital is committed yet."} + case ChildFunded: + return StateCopy{string(state), "Funded", "This provider has transferred part or all of its selected USDT0 capacity."} + case ChildActive: + return StateCopy{string(state), "Active", "This provider relationship is participating in the active facility."} + case ChildClosed: + return StateCopy{string(state), "Closed", "The provider relationship has no outstanding exposure."} + case ChildExpired: + return StateCopy{string(state), "Expired", "The provider relationship validity ended."} + case ChildDefaulted: + return StateCopy{string(state), "Defaulted", "A future default-resolution path would be required; it is outside the MVP."} + default: + return StateCopy{string(state), "Unknown", "No authoritative child-state description is available."} + } +} + +func roundCopy(state RoundStatus) StateCopy { + switch state { + case RoundOpen: + return StateCopy{string(state), "Open", "Eligible providers may submit bounded private offers."} + case RoundFinalized: + return StateCopy{string(state), "Finalized", "The verified allocation has been materialized or is ready for that handoff."} + case RoundExpired: + return StateCopy{string(state), "Expired", "The round expiry boundary has passed."} + default: + return StateCopy{string(state), "Unknown", "No authoritative round-state description is available."} + } +} + +func stateFromRoot(v uint8) RootState { + states := []RootState{RootNone, RootProposed, RootSyndicating, RootFunding, RootActive, RootClosed, RootFrozen, RootExpired} + if int(v) >= len(states) { + return RootNone + } + return states[v] +} + +func stateFromChild(v uint8) ChildState { + states := []ChildState{ChildNone, ChildSelected, ChildFunded, ChildActive, ChildClosed, ChildExpired, ChildDefaulted} + if int(v) >= len(states) { + return ChildNone + } + return states[v] +} + +func stateFromRound(v uint8) RoundStatus { + states := []RoundStatus{RoundNone, RoundOpen, RoundFinalized, RoundExpired} + if int(v) >= len(states) { + return RoundNone + } + return states[v] +} + +func kindFromRegistry(v uint8) LineageKind { + kinds := []LineageKind{KindRootAccord, KindMakkariSession, KindCoFillAllocation, KindChildAccord, KindDraw, KindDrawLeg, KindSettlement, KindRepayment} + if int(v) >= len(kinds) { + return LineageKind(fmt.Sprintf("UNKNOWN_%d", v)) + } + return kinds[v] +} From 62e48865adef5d2310ec80d76b0a9544e831070c Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 13:25:35 +0100 Subject: [PATCH 008/107] test: cover chain-backed read model projection --- tools/pkg/readmodel/chain_rpc_test.go | 134 ++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 tools/pkg/readmodel/chain_rpc_test.go diff --git a/tools/pkg/readmodel/chain_rpc_test.go b/tools/pkg/readmodel/chain_rpc_test.go new file mode 100644 index 0000000..54a11c7 --- /dev/null +++ b/tools/pkg/readmodel/chain_rpc_test.go @@ -0,0 +1,134 @@ +package readmodel + +import ( + "context" + "encoding/json" + "fmt" + "math/big" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +func TestChainReaderProjectsFacilityFromRPC(t *testing.T) { + parsed, err := abi.JSON(strings.NewReader(capitalFacilityABIJSON)) + if err != nil { + t.Fatal(err) + } + rootID := common.HexToHash("0x" + strings.Repeat("1", 64)) + childID := common.HexToHash("0x" + strings.Repeat("2", 64)) + collateral := common.HexToAddress("0x" + strings.Repeat("3", 40)) + liquidity := common.HexToAddress("0x" + strings.Repeat("4", 40)) + root := rawRoot{ + ID: rootID, + Borrower: common.HexToAddress("0x" + strings.Repeat("5", 40)), + CollateralAsset: collateral, + LiquidityAsset: liquidity, + TargetCapacity: big.NewInt(100), + CommittedCapacity: big.NewInt(100), + DrawnPrincipal: big.NewInt(25), + CollateralLocked: big.NewInt(50), + ValidUntil: 200, + PolicyHash: common.HexToHash("0x" + strings.Repeat("6", 64)), + State: 4, + } + child := rawChild{ + ID: childID, + RootID: rootID, + AllocationID: common.HexToHash("0x" + strings.Repeat("7", 64)), + Provider: common.HexToAddress("0x" + strings.Repeat("8", 40)), + SelectedCapacity: big.NewInt(100), + CommittedCapacity: big.NewInt(100), + DrawnPrincipal: big.NewInt(25), + ValidUntil: 150, + State: 3, + } + childParsed, err := abi.JSON(strings.NewReader(capitalFacilityABIJSON)) + if err != nil { + t.Fatal(err) + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var request struct { + ID any `json:"id"` + Method string `json:"method"` + Params []json.RawMessage `json:"params"` + } + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + t.Errorf("decode RPC request: %v", err) + return + } + result := "0x72" + if request.Method == "eth_call" && len(request.Params) > 0 { + var call struct { + Data string `json:"data"` + Input string `json:"input"` + } + if err := json.Unmarshal(request.Params[0], &call); err != nil { + t.Errorf("decode eth_call: %v", err) + return + } + if call.Data == "" { + call.Data = call.Input + } + if len(call.Data) < 10 { + t.Errorf("eth_call data too short: %q", call.Data) + } else { + switch strings.ToLower(call.Data[:10]) { + case fmt.Sprintf("0x%x", parsed.Methods["getRoot"].ID): + encoded, packErr := parsed.Methods["getRoot"].Outputs.Pack(root) + if packErr != nil { + t.Errorf("pack root: %v", packErr) + return + } + result = "0x" + fmt.Sprintf("%x", encoded) + case fmt.Sprintf("0x%x", parsed.Methods["getChildIds"].ID): + encoded, packErr := parsed.Methods["getChildIds"].Outputs.Pack([][32]byte{childID}) + if packErr != nil { + t.Errorf("pack child IDs: %v", packErr) + return + } + result = "0x" + fmt.Sprintf("%x", encoded) + case fmt.Sprintf("0x%x", childParsed.Methods["getChild"].ID): + encoded, packErr := childParsed.Methods["getChild"].Outputs.Pack(child) + if packErr != nil { + t.Errorf("pack child: %v", packErr) + return + } + result = "0x" + fmt.Sprintf("%x", encoded) + } + } + } + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%v,"result":%q}`, request.ID, result) + })) + defer server.Close() + + reader, err := NewChainReader(context.Background(), ChainReaderConfig{ + RPCURL: server.URL, + Network: "coston2", + ChainID: 114, + FacilityAddress: common.HexToAddress("0x" + strings.Repeat("9", 40)), + RegistryAddress: common.HexToAddress("0x" + strings.Repeat("a", 40)), + CollateralAsset: Asset{Address: collateral.Hex(), Symbol: "FXRP", Decimals: 6}, + LiquidityAsset: Asset{Address: liquidity.Hex(), Symbol: "USDT0", Decimals: 18}, + }) + if err != nil { + t.Fatal(err) + } + defer reader.Close() + facility, err := reader.Facility(context.Background(), rootID) + if err != nil { + t.Fatal(err) + } + if facility.State != RootActive || facility.AvailableCapacity != "75" || len(facility.Children) != 1 { + t.Fatalf("unexpected facility projection: %+v", facility) + } + if !facility.Invariants.RootExposureMatchesChildren || !facility.Invariants.CommittedMatchesFundedChildren { + t.Fatalf("invariants were not preserved: %+v", facility.Invariants) + } +} From 4de52ac8e75e07c6e1e060d7db05646a2689d3fc Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 14:41:31 +0100 Subject: [PATCH 009/107] docs: define Concord frontend design system --- docs/frontend-design-system.md | 560 +++++++++++++++++++++++++++++++++ frontend/README.md | 21 +- 2 files changed, 572 insertions(+), 9 deletions(-) create mode 100644 docs/frontend-design-system.md diff --git a/docs/frontend-design-system.md b/docs/frontend-design-system.md new file mode 100644 index 0000000..f2ffe5e --- /dev/null +++ b/docs/frontend-design-system.md @@ -0,0 +1,560 @@ +# Concord frontend design system + +Status: design specification only. This document defines the interface before +the React implementation begins. It must remain subordinate to +[`shared-product-contract.md`](./shared-product-contract.md) and +[`frontend-map.md`](./frontend-map.md). + +## Design objective + +Concord should feel like a relationship workspace for real people moving +capital, not like a speculative trading dashboard or a generic DeFi pool. + +The first screen must let a treasury, provider, institution, auditor, or +developer answer six questions without decoding a graph: + +1. What relationship is this? +2. Who participates? +3. What was selected, funded, drawn, and remains available? +4. Which child relationships supplied the current exposure? +5. Why is the next action allowed? +6. What public evidence and lineage support the state? + +The interface is a readable relationship ledger. Visual topology supports the +explanation; it never replaces the explanation. + +## Governing design decisions + +### One product object + +The primary object is the Root Accord workspace. A transaction receipt, +provider quote, or draw is a linked event in that workspace, not the home +screen's organizing concept. + +### Progressive disclosure + +Every screen has two disclosure modes over the same read model: + +- **Guided**: plain-language labels, one permitted next action, short + explanations, and clear warnings. +- **Detailed**: canonical IDs, contract addresses, fee bps, terms commitments, + block/transaction references, FCC evidence, and full lineage. + +Guided mode is the default. Detailed mode is a user-controlled view preference, +not a different data model. + +### State before styling + +State labels must be derived from the observed projection. The UI must never +turn an unobserved value into `0`, a selected allocation into committed capital, +or a submitted wallet request into a confirmed action. + +Use these exact distinctions in copy: + +| Machine state | User-facing label | Required explanation | +|---|---|---| +| `SELECTED` | Selected — funding not observed | The provider was selected by CoFill, but its USDT0 transfer has not been observed. | +| `FUNDED` | Funded | The provider's USDT0 commitment transfer was observed. | +| `ACTIVE` | Active | The child relationship is participating in an active facility. | +| `EXPIRED` | Expired | The relationship is no longer valid for new financial actions. | +| `PARTIAL` observation | Partial data | Some linked state could not be observed from the configured source. | +| `NOT_OBSERVED` | Not observed | Concord has no authoritative observation and is not asserting a value. | + +### Real-world vocabulary first + +Use “facility”, “provider”, “funded”, “draw”, “repay”, and “available” in +headings and actions. Show “Root Accord”, “Child Accord”, “Makkari”, “CoFill”, +and canonical IDs in the relationship spine, detail panels, exports, and API +links. Do not force a new user to know protocol nouns before understanding the +economic relationship. + +## Visual direction + +### Style lane + +**Quiet financial operations / relationship ledger.** + +- Light-first neutral canvas with a dark ink text system. +- Dense enough for institutional work, calm enough for a first-time treasury + user. +- Thin rules and measured containers create structure; they do not become + decoration. +- One proof accent is reserved for verified/observed states. Pending and + blocked states use separate semantic colors plus text and icons. +- No rainbow gradients, crypto-market candlesticks, floating neon cards, + decorative 3D objects, or anonymous “AI” surfaces. +- Elevation is restrained: borders and surface contrast carry hierarchy; + shadows are soft and rare. + +This is an interface for accountable capital coordination. The visual language +should communicate precision and calm, not yield or price excitement. + +### Layout constraints + +The desktop content container is `1120px` maximum with responsive horizontal +padding: + +```css +--container-max: 1120px; +--container-pad: clamp(20px, 4vw, 48px); +``` + +Use a 12-column grid on desktop, 8 columns on tablet, and a single readable +column on mobile. The main shell has three levels: + +```text +global context bar + └─ relationship header + ├─ metrics band + ├─ state explanation / next action + ├─ relationship spine + └─ children, activity, evidence +``` + +Subtle one-pixel vertical container guides may appear at the outer content +edges on desktop and major sections. They must align with the real container, +sit behind content, use `pointer-events: none`, and disappear when they harm +mobile readability. Do not draw lines around every nested card. + +### Type hierarchy + +The type system is intentionally restrained: + +| Level | Use | Treatment | +|---|---|---| +| Display | facility name and one primary amount | strong sans, tight leading, never all caps for a sentence | +| Section | “Relationship”, “Children”, “Evidence” | medium/semibold, compact | +| Label | metric labels, state labels, role labels | small, tracked, sentence case unless canonical state | +| Body | explanations and instructions | readable line height, plain language | +| Data | IDs, hashes, addresses, base units | monospace, copyable, never used as marketing typography | + +Do not use more than two font families. The exact font can be chosen during +implementation, but it must prioritize legibility, numeral clarity, and broad +Unicode coverage for international names and organizations. + +### Amounts + +Every amount block has: + +```text +formatted amount + token symbol +human label +network / decimals on demand +exact base-unit value in the detail drawer +``` + +Never infer FXRP and USDT0 decimals from one another. Do not display a local +currency conversion as if it were an onchain value. If a fiat estimate is +added later, it must be explicitly labeled as an estimate with its source and +timestamp. + +## Screen composition + +### 1. Facility list `/` + +Purpose: orient the user without pretending the list is a portfolio analytics +product. + +```text +Concord Network · Wallet +Your facilities [Create Root Accord] +Short explanation of what a facility relationship is. + +[Search] [Role / state filter] [Guided · Detailed] + +Facility name State Committed Drawn Available Open +Treasury working cap Active 600,000 USDT0 400,000 200,000 → +... +``` + +Each row shows observation status and network. If the list is not observed, +show a non-zero loading/unknown state rather than an empty list that implies +there are no facilities. + +### 2. Root Accord workspace `/facilities/:rootAccordId` + +This is the canonical first screen. + +#### Header + +```text +ROOT ACCORD ACTIVE · Observed +Treasury working capital facility +Root ID 0x… [Copy] [Detailed] +FXRP-backed · USDT0 liquidity · expires 12 Aug 2026 + [Next action] +``` + +The primary action is state-derived. Examples: “Prepare FXRP lock”, “Review +selected providers”, “Fund your allocation”, “Prepare draw”, or “Prepare +repayment”. Never use a generic “Continue” button when the protocol knows the +specific next action. + +#### Metrics band + +Use four primary metrics in this order: + +```text +Target Committed Drawn Available +1,000,000 600,000 400,000 200,000 USDT0 +``` + +Under `Committed`, show `600,000 funded` and, when relevant, a separate +`250,000 selected · not funded` figure. The selected figure must not be added +to committed capacity. + +#### State explanation + +A short sentence explains the observed state and the reason for the next +action. Example: + +> This facility is active. Three child relationships have funded 600,000 +> USDT0; 400,000 is currently drawn, leaving 200,000 available. + +If an invariant is not observed or fails, this area becomes a prominent +warning with a link to evidence. It must not be hidden in a developer panel. + +#### Relationship spine + +Use a horizontal stepper on wide screens and a vertical sequence on narrow +screens: + +```text +Root Accord → Makkari → CoFill → Child Accords → Draw → Repayment +``` + +Each node is a compact, expandable relationship card: + +- what this node means in plain language; +- current state; +- observed timestamp/block; +- number of linked records; +- “View details” affordance. + +The Child Accords node expands into provider cards. The Draw node expands into +explicit DrawLegs. Do not draw arbitrary edges between every record; the +causal order is the visual hierarchy. + +#### Child table + +Columns in Guided mode: + +```text +Provider State Funded Drawn Available +Provider A Funded · Active 250,000 250,000 0 +Provider B Selected · funding pending — — — +Provider C Funded · Active 350,000 150,000 200,000 +``` + +Detailed mode adds child ID, fee bps, terms commitment, valid-until, funding +transaction, and source block. Losing quotes and private constraints remain +withheld unless the role is authorized to see them. + +#### Activity and evidence + +Use two adjacent sections rather than a single undifferentiated feed: + +- **Activity**: human-readable lifecycle events ordered by time. +- **Evidence**: source, observation status, transaction/hash links, FCC result + digest, and disclosure boundary. + +An activity item always links back to the relationship node that authorized it. + +### 3. Makkari / CoFill round `/rounds/:roundId` + +This screen explains confidential coordination without exposing confidential +inputs by default. + +```text +SYNDICATION ROUND +Objective Fill 1,000,000 USDT0 +Policy Maximum fee 680 bps +Expiry 12 Aug 2026 +Status Allocation verified + +Makkari session → CoFill result → materialization handoff + +Selected capacity Funded capacity Quotes withheld / visible by role +``` + +Authorized provider view: its own quote and status. Treasury view: accepted +providers, accepted executable capacity, terms commitments, and verification +metadata. Observer view: public result and disclosure notice. No role gets a +visualized “losing quote” by accident. + +### 4. Child Accord `/children/:childAccordId` + +Provider-focused but still rooted in the parent relationship: + +```text +CHILD ACCORD +Provider relationship under Treasury working capital facility +Parent Root Accord → [open] + +Your allocation Funded Drawn Available Valid until +250,000 USDT0 250,000 250,000 0 12 Aug 2026 + +[Funding / exposure timeline] +[Terms and evidence] +``` + +The provider must see a clear boundary between an allocation offer and a +successful funding transfer. + +### 5. Draw `/draws/:drawId` + +The draw screen must make split supply obvious: + +```text +DRAW D-01 400,000 USDT0 +Authorized by Root Accord Active · Observed + +Draw legs +├─ Child Accord A / Provider A 250,000 +└─ Child Accord C / Provider C 150,000 + +Principal remaining · settlement receipt · repayment preparation +``` + +Never flatten the draw into a single provider or a single anonymous transfer. + +### 6. Evidence `/evidence/:resultDigest` + +Evidence is a provenance view, not a claim of private execution. + +Show: + +- result digest and linked round/root IDs; +- verification status and verifier contract/source; +- network, chain ID, block, and transaction references where observed; +- what is public, what is withheld, and what was not observed; +- simulated/development TEE labeling where applicable. + +Do not use a shield icon alone to imply “private” or “secure”. The disclosure +boundary must be written in text. + +## Role and permission behavior + +The role selector is a view of authorized scope, not a cosmetic theme switch. +In production, role context comes from the caller's wallet/organization +authorization. A demo-only role switch must be visibly labeled as a preview. + +| Role | Default landing emphasis | Visible actions | +|---|---|---| +| Treasury | facility lifecycle and available capacity | prepare authorized root, draw, repay, close actions | +| Provider | eligible rounds and own child relationships | submit own quote, fund own child, inspect own exposure | +| Institution/operator | approvals, evidence, multiple facilities | delegated preparation within granted scope | +| Auditor/observer | public state, evidence, lineage | read-only | +| Developer/agent | structured projection and intent details | read and prepare unsigned intents; never sign/broadcast | + +Disabled actions must explain the missing precondition. Do not hide an action +when its existence helps explain the lifecycle; disable it with a reason when +the caller can see the relationship but cannot act. + +## Action review pattern + +Every write uses the same review surface. It is a side panel on desktop and a +full-screen sheet on mobile. + +```text +1. What will happen? + Prepare a 250,000 USDT0 funding transfer for Child Accord … + +2. Why is it allowed? + Selected allocation observed; amount is within the selected capacity. + +3. What will the wallet receive? + Network · chain ID · target contract · value · calldata disclosure + +4. What must still be verified? + Wallet signature, transaction receipt, and indexed state observation. + + [Cancel] [Review in wallet] +``` + +After approval, show `Awaiting signature`, `Submitted`, `Confirmed`, +`Reverted`, or `Not observed`. “Confirmed” requires an observed receipt and +the relevant state read; a wallet callback alone is not confirmation. + +## Heroicons usage rules + +Heroicons is the single icon family for the first frontend. Use the React +library rather than copying arbitrary SVG markup into components. Keep icons +supportive and labeled; the product's meaning comes from text and state. + +### Style and sizing + +- Outline icons for navigation, buttons, metadata, and relationship actions. +- Solid icons only for compact status markers where the adjacent text remains + visible. +- Default control size: `20px`; standard relationship/status size: `24px`; + empty-state illustration maximum: `32px`. +- Use one icon per action. Do not stack multiple decorative icons beside a + short label. +- Icon-only controls require an accessible name and a visible tooltip or + adjacent context; primary actions are always text-labelled. +- Do not use token-specific currency icons. Keep `FXRP` and `USDT0` as text + symbols with asset metadata. + +### Concord icon matrix + +| Meaning | Heroicon | Variant / use | +|---|---|---| +| Facility / relationship | `rectangle-group` | outline, Root Accord header | +| Parent-child lineage | `share` | outline, lineage affordance | +| Provider group | `user-group` | outline, participant summary | +| Confidential boundary | `lock-closed` | outline, Makkari explanation; never privacy proof by itself | +| Allocation / composition | `squares-2x2` | outline, CoFill node | +| Selected / queued | `queue-list` | outline, selected but not funded | +| Funded / committed | `banknotes` | outline, funded capacity label | +| Draw | `arrow-down-tray` | outline, liquidity entering treasury | +| Repayment | `arrow-up-tray` | outline, liquidity returning | +| Verified evidence | `shield-check` | solid/outline with text “Verified” | +| Public receipt | `receipt-percent` | outline, settlement/fee evidence | +| Partial observation | `ellipsis-horizontal-circle` | outline plus “Partial data” | +| Not observed / withheld | `eye-slash` | outline plus explicit text | +| Warning / invariant issue | `exclamation-triangle` | outline plus sentence-level warning | +| Expiry | `clock` | outline, valid-until metadata | +| Frozen / unavailable | `pause-circle` | outline plus reason | +| Copy ID | `document-duplicate` | outline, accessible label “Copy Root Accord ID” | +| External explorer | `arrow-top-right-on-square` | outline, external link | +| Network settings | `server-stack` | outline, network context | +| Wallet boundary | `wallet` | outline, connection/approval context | + +Status meaning is always encoded by icon + label + shape/placement. Color is a +secondary cue only. + +## Responsive behavior + +### Desktop + +- Persistent left navigation for facilities, activity, evidence, and settings. +- Relationship workspace uses the full container; metrics stay above the + fold. +- Relationship spine is horizontal; child table remains tabular. + +### Tablet + +- Collapse left navigation into a top navigation row. +- Keep the metric band in two rows if needed; never shrink numbers below + readable size to preserve one row. +- Relationship spine may wrap to two rows but retains left-to-right causal + order. + +### Mobile + +- One column, with a sticky relationship header containing state and the next + action. +- Metrics become stacked cards with `Available` first when a draw is active. +- Relationship spine becomes a vertical numbered sequence. +- Child rows become provider cards; DrawLegs remain individually visible. +- Action review becomes a full-screen sheet with the final wallet action fixed + to the bottom safe area. +- Never hide evidence or lineage behind a graph-only interaction. + +## Motion + +Motion is functional and bounded: + +- 150–220ms for state changes and panel transitions; +- no perpetual shimmer after data has loaded; +- no animation that changes the apparent amount or state without an explicit + observation update; +- respect `prefers-reduced-motion`; +- use a one-time reveal for the causal spine only when it helps the user read + order, not as a branded spectacle. + +## Accessibility and international use + +- Target WCAG 2.2 AA for the implemented component stack. +- Keyboard access for every disclosure, filter, table action, and wallet step. +- Visible focus indicator independent of the semantic state color. +- Screen-reader labels include relationship type and ID where needed. +- Use locale-aware grouping and dates, but expose the exact base unit and + decimals in details. +- Do not assume USD, English date order, a two-party relationship, or a + borrower/lender vocabulary in generic components. +- Support long organization names, non-Latin names, right-to-left locale + testing, and narrow screens before launch. + +## First design review gate + +Before any frontend implementation, the design review must show static +wireframes or a design prototype for: + +1. an active Root Accord with three children and a two-child Draw; +2. a selected-but-not-funded provider; +3. a partial/not-observed read response; +4. a repayment action review and receipt state; +5. the same facility in Guided and Detailed modes; +6. desktop, tablet, and mobile layouts. + +Acceptance means a reviewer can trace: + +```text +Root Accord → Makkari → CoFill → Child Accord → DrawLeg → Repayment +``` + +without opening a developer console, and can distinguish selected capacity +from funded committed capacity without reading protocol documentation. + +## Design-first prompt card + +Use this as the fixed prompt for future visual prototypes and iterations: + +```text +GOAL +Design the Concord Root Accord relationship workspace for a treasury and its +independent liquidity providers. Make the persistent relationship legible +before showing transaction detail. + +FORMAT +Desktop 1440px first; responsive tablet and mobile variants. +Content max 1120px; safe horizontal padding clamp(20px, 4vw, 48px). + +LAYOUT +Global context → Root Accord header → metrics band → state explanation and +next action → causal relationship spine → child relationships → activity and +evidence. Use a 12-column editorial grid and quiet 1px container guides. + +TYPE +Legible sans for headings/body; monospace only for IDs, hashes, addresses, and +exact base units. Tight headings, readable body copy, tracked small labels. + +COLOR + MATERIAL +Neutral light canvas, dark ink text, restrained borders, one proof accent, +explicit pending and blocked states. No rainbow gradient, no glassmorphism, +no speculative-trading visual language. + +ICONOGRAPHY +Heroicons React outline icons at 20–24px, one icon per action, always paired +with a label. Use the Concord icon matrix; do not invent token icons. + +COPY +Use “Selected — funding not observed”, never “Committed” before USDT0 funding. +Use “Not observed” instead of zero when authoritative state is unavailable. + +CONSTRAINTS +RELATIONSHIP-FIRST · GUIDED-BY-DEFAULT · EVIDENCE-VISIBLE · WALLET-APPROVAL-EXPLICIT + +NEGATIVE PROMPT +No fake dashboard metrics, no generic “Continue”, no flattened provider pool, +no private-transfer claims, no hidden losing quotes, no decorative graph that +cannot explain causal authorization, no text baked into generated imagery. +``` + +## Sources and skill application + +- [Heroicons](https://heroicons.com/) — current catalog/version, MIT license, + and React/Vue library availability. +- [MengTo Skills](https://github.com/MengTo/Skills) — public skill collection + and design-first principle of explicit specs, constraints, hierarchy, and + one-variable iteration. +- [Concord shared product contract](./shared-product-contract.md) — source of + truth for lifecycle, roles, amounts, observations, and intent boundaries. +- [Concord frontend map](./frontend-map.md) — route and component baseline. + +The exact Interface Foundry package and the local MengTo package are not +currently present in the pruned workspace. This document records the design +decisions so the frontend remains reproducible instead of depending on +ephemeral skill files. diff --git a/frontend/README.md b/frontend/README.md index ff12352..cfcbe72 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,13 +1,16 @@ # Concord frontend -The frontend is intentionally mapped before full implementation. The -implementation must use the shared product contract and the TypeScript SDK; -it must not create a parallel facility state model. +The frontend is currently in the design phase. The semantic route/component +map is in [`docs/frontend-map.md`](../docs/frontend-map.md); the visual, +interaction, icon, responsive, and accessibility specification is in +[`docs/frontend-design-system.md`](../docs/frontend-design-system.md). -Planned stack from the build handoff: React, TypeScript, Vite, Wagmi, Viem, -and TanStack Query. The first implementation checkpoint is the Root Accord -workspace described in [`docs/frontend-map.md`](../docs/frontend-map.md). +The implementation must use the shared product contract and the TypeScript +SDK; it must not create a parallel facility state model. Heroicons is the +single icon family for the first frontend. -Full frontend work begins after the contract/FCC semantics are demonstrated -with authoritative Coston2 evidence. Until then, this directory contains the -product-facing route and component boundary only. +Planned stack from the build handoff: React, TypeScript, Vite, Wagmi, Viem, +and TanStack Query. The first implementation checkpoint remains the Root +Accord workspace, but implementation begins only after the design review gate +in the design-system document is satisfied and contract/FCC semantics are +demonstrated with authoritative Coston2 evidence. From 4bc2294f330f85f00e53d8dd4409632b22dec32e Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 15:13:58 +0100 Subject: [PATCH 010/107] docs: define complete Concord frontend art direction --- docs/assets/concord-visual-territories-v1.png | Bin 0 -> 786444 bytes docs/frontend-art-direction.md | 586 ++++++++++++++++++ frontend/README.md | 3 + 3 files changed, 589 insertions(+) create mode 100644 docs/assets/concord-visual-territories-v1.png create mode 100644 docs/frontend-art-direction.md diff --git a/docs/assets/concord-visual-territories-v1.png b/docs/assets/concord-visual-territories-v1.png new file mode 100644 index 0000000000000000000000000000000000000000..02c5ec50a684c6d29d93699ebb126e317c108bbe GIT binary patch literal 786444 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 z�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 g+^+DiusBdUM)G#Gqp0+X`g6r@hmeJL20 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%0A9J{eK3F3Ww>u1%j`D-(cUNPs#vK|g*7J6hR) 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 literal 0 HcmV?d00001 diff --git a/docs/frontend-art-direction.md b/docs/frontend-art-direction.md new file mode 100644 index 0000000..04df119 --- /dev/null +++ b/docs/frontend-art-direction.md @@ -0,0 +1,586 @@ +# Concord frontend art direction and information architecture + +Status: design phase, before React implementation. + +This document turns the first visual territory exploration into a complete +product direction. It governs the webapp's visual language, navigation, +responsive behavior, motion, imagery, typography, and user-facing page +architecture. It does not change Concord's product semantics. + +## Direction decision + +Concord uses a hybrid visual system: + +1. **Light Ledger** is the default product mode. +2. **Midnight Treasury** is the optional dark mode. +3. **Signal Atlas** is a supporting language for lineage, evidence, empty + states, onboarding, and selected marketing moments. + +This gives Concord institutional seriousness without making the first-use +experience feel like a terminal. The default interface is bright, calm, and +highly legible. Dark mode is not a separate brand or data model. + +The visual territory board is a reference study, not a production asset: +`docs/assets/concord-visual-territories-v1.png`. + +## Product feeling + +The intended feeling is: + +> “I understand what this relationship is, what is happening to the capital, +> and what I can safely do next.” + +The product should feel closer to a high-quality institutional treasury +workspace, a modern custody operations console, and an excellent banking +product than to a crypto exchange. + +### Qualities to optimize + +- calm rather than loud; +- precise rather than technical for its own sake; +- warm rather than sterile; +- globally legible rather than region-specific; +- evidence-aware rather than trust-me; +- beautiful through proportion, typography, and hierarchy rather than effects. + +### Explicitly avoid + +- generic SaaS dashboard sameness; +- neon DeFi styling; +- candlestick charts and yield theatrics; +- glassmorphism as a substitute for hierarchy; +- giant graphs with unreadable nodes; +- decorative “AI” language; +- stock photographs of traders, skyscrapers, or handshakes; +- fake transaction success, fake zeros, or fake privacy indicators. + +## Typography + +### Primary recommendation + +Use **IBM Plex Sans** for interface, headings, labels, and body copy, paired +with **IBM Plex Mono** for IDs, hashes, addresses, block references, and exact +base-unit values. + +This pairing is appropriate because the IBM Plex family is designed for UI +environments, is available under the SIL Open Font License, and provides broad +script coverage including Latin, Arabic, Chinese, Cyrillic, Devanagari, Greek, +Hebrew, Japanese, Korean, and Thai. The product must still test actual +fallback rendering for every supported locale. + +### Type roles + +```text +Plex Sans primary UI, headings, explanations, navigation, buttons +Plex Mono IDs, hashes, addresses, exact units, technical evidence +System fallback unavailable font or unsupported glyph fallback +``` + +Do not add a display font merely to appear premium. Concord's premium quality +should come from rhythm, whitespace, rule placement, and information clarity. + +### Type scale + +```text +Display 40 / 46 facility title, only on spacious desktop views +Heading 1 28 / 34 page title +Heading 2 20 / 26 section title +Heading 3 16 / 22 card and table group title +Body 15 / 23 explanations and instructions +Body small 13 / 19 supporting metadata +Label 11 / 15 tracked state/eyebrow labels +Data 13 / 18 mono IDs and exact values +``` + +The scale is a starting token set. It must be tested with long organization +names, large amounts, and translated labels before being frozen. + +## Color and material system + +### Light Ledger tokens + +```css +:root { + --canvas: #f6f5f1; + --surface: #ffffff; + --surface-subtle: #efeee9; + --ink: #10161d; + --ink-muted: #66717c; + --rule: #d8dce0; + --rule-strong: #b8c0c7; + --proof: #087d68; + --proof-surface: #e1f3ed; + --pending: #a56a00; + --pending-surface: #fff1d5; + --blocked: #b8495b; + --blocked-surface: #fbe7eb; + --focus: #145fd7; +} +``` + +### Midnight Treasury tokens + +```css +[data-theme="dark"] { + --canvas: #0a0e12; + --surface: #121820; + --surface-subtle: #19212a; + --ink: #f4f1ea; + --ink-muted: #aeb8c0; + --rule: #2b353e; + --rule-strong: #52606c; + --proof: #7ee8c3; + --proof-surface: #14392f; + --pending: #e8b451; + --pending-surface: #3a2e17; + --blocked: #ee7a8b; + --blocked-surface: #3c2028; + --focus: #75a9ff; +} +``` + +Proof color means an observed/verified state only. It must not be reused for +primary buttons, decoration, or generic links everywhere. Pending and blocked +states always include text and icon cues in addition to color. + +### Surface rules + +- Use one main canvas, one raised surface, and one subtle surface. +- Prefer borders and contrast to heavy shadows. +- Use radius sparingly: `8px` for controls, `12px` for major panels, no + pill-shaped everything. +- Use corner details and container lines only at page/section scale. +- Avoid background textures in data-dense screens. +- Any grain or atlas texture belongs in onboarding/marketing, not the facility + ledger where it reduces contrast. + +## Global navigation + +### Desktop header + +Use one 72px global header. Do not add a permanent left sidebar to the first +version; it creates a second hierarchy and makes the product feel like an +internal admin tool. + +```text +┌──────────────────────────────────────────────────────────────────────┐ +│ Concord Facilities Activity Evidence Coston2 Wallet │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +#### Header zones + +Left: + +- Concord wordmark; +- active product area: `Facilities`. + +Center: + +- `Facilities`; +- `Activity`; +- `Evidence` (visible to authorized users; advanced users can pin it); +- no protocol primitive names in the primary nav. + +Right: + +- network status: `Coston2` with observation indicator; +- connected wallet or organization identity; +- compact help/preferences menu. + +The header must remain stable across roles. The content and available actions +change by authorization, not the global navigation vocabulary. + +### Facility-local navigation + +Inside a facility, use a local sub-navigation row below the relationship +header: + +```text +Overview Funding Activity Evidence +``` + +`Overview` is the default. `Funding` contains the Makkari/CoFill/child +formation context. `Activity` contains public lifecycle history. `Evidence` +contains sources and verification. Draws and child relationships open from +the relevant rows/cards and do not need to become global nav items. + +### Mobile header and hamburger menu + +The mobile header is 64px: + +```text +☰ Concord wallet/status +``` + +The hamburger button is a 44px accessible target using Heroicons `bars-3`. +It opens a left-side navigation drawer with a scrim. It is not a “more” menu +and it must not contain the primary facility action. + +#### Drawer structure + +```text +Concord [close] + +WORKSPACE + Facilities + Activity + +TRUST & EVIDENCE + Evidence + Network status + +ACCOUNT + Wallet / organization + Appearance + Language and region + Help +``` + +Rules: + +- keep the drawer one level deep; +- show the active destination with label, icon, and a left rule; +- close on navigation, Escape, scrim click, or close button; +- trap focus while open; +- preserve scroll position when closing; +- keep the facility's next action outside the drawer; +- respect reduced-motion preferences; +- never put hidden protocol settings in the ordinary user menu. + +### Footer + +The authenticated application footer is quiet and functional. It is not a +marketing block at the end of every screen. + +```text +Concord Coston2 · chain 114 · Observed +Status Documentation Source Privacy / disclosure +``` + +On public onboarding or future marketing pages, the footer can expand into +product, developers, evidence, and legal columns. Those pages must not imply +production readiness or privacy guarantees that the implementation does not +prove. + +## Page architecture + +### Public and entry pages + +#### `/welcome` + +Purpose: explain Concord in one screen before wallet connection. + +```text +Coordinate one treasury need across independent capital providers. + +One facility. Multiple providers. Visible obligations. + +[Start as treasury] [Provide capital] [View a facility] +``` + +The page uses a restrained relationship animation or generated abstract visual, +not a fake screenshot of a live facility. + +#### `/connect` + +Purpose: establish wallet/organization context and network. + +Show: + +- what connecting enables; +- selected network; +- supported asset names and decimals; +- data disclosure boundary; +- wallet/organization approval step. + +Do not ask users to understand chain IDs before showing the human reason for +connection. + +### Core application pages + +#### `/` + +Facilities home. Show active and relevant facilities, a plain-language +explanation, search/filter, and one role-appropriate creation or participation +action. + +#### `/facilities/new` + +Treasury setup wizard. Four steps maximum: + +1. facility need: target liquidity, asset, expiry; +2. collateral and policy; +3. provider coordination settings; +4. review and wallet approval. + +Each step has a short “why we ask” explanation. Canonical IDs and policy hashes +are available in Detailed mode, not in the first form view. + +#### `/facilities/:rootAccordId` + +The primary relationship workspace. The first viewport shows: + +```text +facility identity → state → committed/drawn/available → next action +``` + +The causal relationship spine begins below the primary facts and can be +expanded. This page is not a graph explorer. + +#### `/facilities/:rootAccordId/funding` + +Funding view for the round, selected allocations, funded children, and +provider-specific actions. It clearly separates: + +```text +selected capacity ≠ funded commitment +``` + +#### `/facilities/:rootAccordId/activity` + +Chronological public lifecycle events, each linked to the relationship node +that authorized it. + +#### `/facilities/:rootAccordId/evidence` + +Evidence and disclosure page. It shows public source, observation state, +transaction references, FCC result metadata, and what remains withheld. It +does not claim private transfers or production TEE security. + +### Contextual detail pages + +These exist for deep linking, exports, and agents, but are reached from the +workspace rather than promoted as primary nav: + +- `/children/:childAccordId` — one provider relationship; +- `/draws/:drawId` — one root draw with explicit DrawLegs; +- `/rounds/:roundId` — Makkari/CoFill round detail; +- `/evidence/:resultDigest` — direct evidence lookup. + +### Settings and support + +`/settings` contains: + +- network and asset configuration; +- wallet/organization context; +- appearance and reduced-motion preference; +- language/region/number formatting; +- notification preferences; +- developer/API access where authorized. + +Do not build a separate admin console for MVP. Institutional approval and +organization controls should appear as role-scoped panels inside the same +relationship workspace until a demonstrated need requires separation. + +## Core screen behavior + +### Above-the-fold contract + +Every facility workspace must show, without scrolling: + +1. facility name and relationship identity; +2. current root state and observation state; +3. committed, drawn, and available liquidity; +4. expiry; +5. one next action or a reason no action is available. + +### Progressive disclosure layers + +```text +Layer 1 human facts and next action +Layer 2 relationship explanation and provider allocation +Layer 3 activity, receipts, and evidence +Layer 4 canonical IDs, calldata, fee bps, hashes, and block references +``` + +The interface should feel simple because the layers are ordered, not because +the protocol is falsely simplified. + +### Empty, loading, and error states + +Every page needs distinct states for: + +- loading/connecting; +- no facilities found; +- no facilities observed; +- partial related data; +- not observed; +- expired/frozen; +- wallet rejected; +- transaction submitted but not yet confirmed; +- receipt observed but projection not yet indexed; +- invariant failure. + +Never show a blank card or `$0`/`0 USDT0` when the value is unavailable. + +## Motion system + +Motion should explain relationship and state. It must never perform spectacle +around capital. + +### Motion primitives + +| Interaction | Motion | Purpose | +|---|---:|---| +| Drawer open/close | 180ms | establish navigation context | +| Local tab transition | 160ms | preserve page continuity | +| Expand relationship node | 220ms | reveal causal detail | +| New observed state | 240ms | acknowledge source update | +| Evidence receipt reveal | 320ms | guide the eye to provenance | +| Toast/notification | 180ms in, 160ms out | confirm UI state only | + +Use a standard ease-out curve for entry and ease-in for exit. No perpetual +movement, no auto-advancing carousel, no number animation from fake zero, and no +animation that suggests a transfer happened before the chain observation. + +### First-visit relationship animation + +On `/welcome`, a thin line can form: + +```text +treasury need → provider contributions → shared facility → repayment return +``` + +It should play once, finish quickly, and be replaceable by a static diagram for +reduced-motion users. The facility workspace uses a static or user-triggered +version of the same causal line. + +### Motion accessibility + +- respect `prefers-reduced-motion`; +- never communicate state through motion alone; +- allow keyboard users to reach every expanded state without animation; +- keep the action-review panel usable if all transitions are disabled. + +## Image and visual asset policy + +### Where images belong + +Images are appropriate for: + +- `/welcome` and onboarding; +- empty states; +- future education pages; +- a carefully constrained public product overview. + +Images do not belong behind dense facility metrics, child tables, or evidence +rows. + +### Visual language + +Use generated or commissioned abstract visuals built from: + +- relationship lines; +- measured grids; +- paper/metal/glass only when materially useful; +- restrained light and shadow; +- global, non-geographic abstractions; +- no token logos or financial clichés. + +The visual territory board is for selection and critique. Any final raster +asset must be generated or sourced only after its intended placement, crop, +contrast, alt text, loading cost, and licensing are documented. + +### Accessibility and performance + +- every meaningful image has alt text; +- decorative images use empty alt text and do not compete with content; +- first-load visuals must not block facility state; +- use responsive sizes and modern formats; +- motion/video must have a static poster and reduced-motion alternative; +- no text is baked into generated imagery when the text is part of the product + interface. + +## User-friendliness contract + +### Language + +Write like a careful human operator explaining capital movement: + +- “Your allocation was selected. Funding has not been observed yet.” +- “This draw uses 250,000 from Provider A and 150,000 from Provider C.” +- “The wallet request is ready. Nothing has been signed yet.” + +Avoid: + +- “Execute” when the user is only preparing a transaction; +- “Committed” for selected but unfunded capital; +- “Private” as a blanket claim; +- “Success” before a receipt and state observation; +- “Continue” when the next action is known. + +### Forms + +- one decision per step; +- show unit, token, decimals, and expiry near the input; +- show impact before wallet approval; +- preserve entered values after validation errors; +- explain rejection in plain language; +- provide a detailed view without forcing it on first-time users. + +### Trust cues + +Trust is expressed through: + +- source and observation timestamps; +- explicit public/withheld data labels; +- readable receipts; +- relationship lineage; +- invariant warnings; +- an unmistakable wallet approval boundary. + +Badges, shields, and green checkmarks are secondary cues only. + +## Institutional completeness gate + +The design is not complete until static prototypes demonstrate: + +1. treasury onboarding from `/welcome` to `/facilities/new`; +2. active facility overview with three child relationships; +3. selected-but-not-funded provider state; +4. multi-child draw with explicit DrawLegs; +5. repayment and restored capacity; +6. evidence and partial/not-observed states; +7. wallet action review and rejected/confirmed/not-observed outcomes; +8. provider view with another provider's private data withheld; +9. auditor read-only view; +10. developer/agent unsigned intent view; +11. mobile hamburger drawer and facility-local navigation; +12. light mode, dark mode, keyboard navigation, reduced motion, and long + localized strings. + +Reviewers must be able to complete the core journey without knowing the words +Accord, Makkari, or CoFill first. Reviewers must still be able to reach those +canonical concepts when they need to inspect lineage or evidence. + +## Design workflow from here + +```text +1. Select/adjust the hybrid visual direction +2. Freeze type, color, spacing, radius, and icon tokens +3. Produce static wireframes for the required states +4. Produce high-fidelity screen compositions +5. Define motion clips and reduced-motion alternatives +6. Run usability and semantic review against the contract +7. Only then implement React components +``` + +The first high-fidelity prototype should be the Root Accord workspace in +Light Ledger mode, with the mobile variant and action-review sheet designed in +the same pass. The first generated hero/onboarding image should remain outside +the core facility screen until the information hierarchy has passed review. + +## Source notes + +- [IBM Plex repository](https://github.com/IBM/plex) — UI-oriented family, + script coverage, and OFL licensing. +- [IBM Design Language typography](https://www.ibm.com/design/language/typography/typeface/) + — official distribution and licensing guidance. +- [Heroicons](https://heroicons.com/) — icon family and React/Vue library. +- [MengTo Skills](https://github.com/MengTo/Skills) — explicit design + constraints, hierarchy, and controlled iteration. +- [`shared-product-contract.md`](./shared-product-contract.md) — product and + authority semantics. +- [`frontend-design-system.md`](./frontend-design-system.md) — prior + component-level design and icon decisions. diff --git a/frontend/README.md b/frontend/README.md index cfcbe72..95e5034 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -4,6 +4,9 @@ The frontend is currently in the design phase. The semantic route/component map is in [`docs/frontend-map.md`](../docs/frontend-map.md); the visual, interaction, icon, responsive, and accessibility specification is in [`docs/frontend-design-system.md`](../docs/frontend-design-system.md). +The complete art direction, navigation, page architecture, typography, motion, +imagery, and usability gate is in +[`docs/frontend-art-direction.md`](../docs/frontend-art-direction.md). The implementation must use the shared product contract and the TypeScript SDK; it must not create a parallel facility state model. Heroicons is the From 2094a6050a688c3c6dc33623ba80b0cde0c67234 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 16:59:21 +0100 Subject: [PATCH 011/107] docs: reconcile Concord with canonical product documents --- README.md | 6 +- docs/README.md | 6 +- docs/canonical-reconciliation-audit.md | 207 +++++++++++++++++++++++++ docs/getting-started.md | 7 +- docs/testing.md | 5 +- 5 files changed, 221 insertions(+), 10 deletions(-) create mode 100644 docs/canonical-reconciliation-audit.md diff --git a/README.md b/README.md index 0cec4cd..d02b66e 100644 --- a/README.md +++ b/README.md @@ -52,14 +52,16 @@ The canonical extension runtime is the current Flare FCE scaffold, with Go as the Concord implementation language. Install Foundry and Go 1.25.1+, then: ```bash -PATH=/workspace/tooling/foundry:/workspace/tooling/go/bin:$PATH \ -FOUNDRY_SOLC=/workspace/tooling/solc-0.8.27 \ forge test (cd go && GOTOOLCHAIN=local go test ./...) (cd tools && GOTOOLCHAIN=local go test ./...) ``` +Use an installed Foundry/Go toolchain, or set `PATH` and `FOUNDRY_SOLC` to +your own toolchain locations. The repository does not depend on a +workspace-specific tool path. + Generate deployment bindings after changing the instruction sender: ```bash diff --git a/docs/README.md b/docs/README.md index 5581847..9359517 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,7 +15,11 @@ inputs, and [CONCORD_STATUS.md](CONCORD_STATUS.md) for the evidence boundary. ## Official scaffold reference The remaining scaffold documents describe the official FCC runtime and remain -reference material for operating Concord: +reference material for operating Concord. They are not Concord product +instructions. In particular, do not follow their inherited Hello World, +Orderbook, Python, or TypeScript rename examples, and do not expect files that +are absent from this Concord repository. Use the Concord-specific documents +above for product semantics, commands, and evidence boundaries: [deployment-steps.md](deployment-steps.md) · [testing-against-coston2.md](testing-against-coston2.md) · diff --git a/docs/canonical-reconciliation-audit.md b/docs/canonical-reconciliation-audit.md new file mode 100644 index 0000000..318feaf --- /dev/null +++ b/docs/canonical-reconciliation-audit.md @@ -0,0 +1,207 @@ +# Concord canonical reconciliation audit + +Status: completed for the durable `agent/concord-rebuild` checkpoint on 2026-08-12. + +This audit compares the implementation with the following authority order: + +1. Canonical Build Handoff — what the MVP must build. +2. Canonical Product Definition Addendum — current product semantics. +3. Concord Whitepaper — architectural thesis and future direction. +4. Current official Flare sources — changing implementation facts. + +The whitepaper and Product Definition Addendum were read in full. The +Foundry Skills Pack named in the handoff was not present in the available +filesystem during this audit, so no claim is made that its package-specific +instructions were loaded. The repository's durable source and current +official Flare-controlled sources were used instead. + +## Verdict + +No contradiction was found between the Build Handoff, Product Definition +Addendum, and whitepaper. The implementation still expresses Concord as a +persistent programmable capital relationship rather than as an RFQ venue, +lending pool, matching engine, or isolated transaction system. + +The MVP is not complete. The outstanding blockers are evidence and execution +gates, not a product reinterpretation: + +- no live Concord deployment, FCC registration, funded Coston2 accounts, + instruction IDs, settlement receipts, or explorer evidence are recorded; +- the current runner does not contain `forge` or Go, so this audit could not + re-run the Solidity and Go suites; +- the full frontend remains intentionally unimplemented until the economic and + FCC path is evidenced. + +## Cross-layer reconciliation + +| Area | Result | Current truth | Decision | +| --- | --- | --- | --- | +| Product object | PASS | Root Accord is the persistent facility relationship; transactions and FCC actions are lineage nodes. | Preserve. | +| Primitive composition | PASS | Accord, Makkari, CoFill, and Lineage remain separate responsibilities. | Preserve. | +| MVP scope | PASS | The repository implements the FXRP-backed facility slice and does not add receivables, KYC/KYB, FDC, cross-border FX, collections, or legal enforcement. | Keep future capabilities as seams only. | +| Economic states | PASS | `selectedCapacity` is separate from `committedCapacity`; provider funding increases committed capacity only after `transferFrom` succeeds. | Preserve the distinction in every surface. | +| Allocation binding | PASS | Result digest binds extension, round, root, success, expiry, providers, amounts, fees, and terms. Materialization is verifier-gated and replay-protected. | Preserve. | +| Draw model | PASS | A root draw creates explicit DrawLeg objects and consumes multiple child relationships deterministically when capacity requires it. | Preserve; do not flatten draws. | +| Repayment | PASS | ERC-20 transfer occurs before exposure reduction; repayment reduces legs, child exposure, and root exposure. | Preserve. | +| Privacy | PASS | Quote inputs and FCC computation are inside the intended confidential boundary; transfers, exposure, accepted capacity, and lineage are not claimed private. | Keep claim language bounded. | +| API / SDK / MCP / CLI | PASS | Read surfaces share one read model; write surfaces prepare unsigned intents and do not custody, sign, broadcast, or become the verifier. | Preserve. | +| Frontend design | PASS / DESIGN ONLY | The design is relationship-first, progressive-disclosure, role-aware, and explicit about observed versus unavailable state. | Implement only after the live economic path. | +| Future authority | GAP / INTENTIONAL | General parent-delegated authority and external evidence fields are not yet modeled. | Roadmap seam; do not add to this MVP. | +| Live Coston2 proof | BLOCKER | No deployment or end-to-end evidence is recorded. | Required before completion claims. | +| Local validation | BLOCKER | `forge` and the recorded Go toolchain are absent from this runner. | Restore toolchain, then rerun the suites. | + +## Implementation evidence + +### Solidity + +`contracts/ConcordTypes.sol` contains the Root Accord, Child Accord, round, +and allocation shapes required by the handoff. `contracts/CapitalFacility.sol` +owns collateral, provider funding, activation, deterministic multi-child draws, +DrawLegs, repayment, exposure, expiry, and close rules. It rejects: + +- root or child overdraw; +- provider funding above selected capacity; +- materialization without a configured verifier authorization; +- duplicate allocation materialization; +- allocation results bound to another extension, round, or root; +- draws above available capacity; +- provider capital return while that child has exposure; +- collateral return while the root has exposure or commitments. + +`contracts/AccordRegistry.sol` records the causal chain: + +```text +ROOT_ACCORD + → MAKKARI_SESSION + → COFILL_ALLOCATION + → CHILD_ACCORD + → DRAW + → DRAW_LEG + → SETTLEMENT + → REPAYMENT +``` + +The registry currently enforces parent-node existence and facility authority. +It does not yet implement the whitepaper's future generalized delegated +authority model. That is an intentional MVP boundary, not a contradiction. + +### FCC / Makkari / CoFill + +The Go extension remains inside the official FCE scaffold. It requires an +encrypted payload and calls the TEE decrypt endpoint; its local tests inject a +decrypt function only for unit isolation. Signed provider quotes are validated +with EIP-191 recovery. CoFill validates round/root/eligibility/fee/expiry +bounds, sorts by fee then provider address then nonce, permits a partial final +allocation, and fails explicitly when capacity is insufficient. + +The offchain `verify-allocation` command is deliberately a separate evidence +boundary. It checks the returned action envelope, signed TEE information, +active machine binding, extension/round/root binding, round state and bounds, +eligibility, canonical digest, and facility verifier address before its +optional `markAllocationVerified` broadcast. Local Solidity actors are not +presented as native FCC proof verification. + +### Layered surfaces + +The following surfaces use the shared contract in +`docs/shared-product-contract.md`: + +- REST/OpenAPI: observed facilities, rounds, draws, lineage, evidence status, + health, and unsigned transaction intents; +- TypeScript SDK: typed REST client and decimal-safe formatting; +- MCP: read resources and safe preparation tools over REST; +- CLI: doctor, API, MCP, and FCC/deployment operator commands; +- frontend boundary: relationship workspace and role-aware disclosure, + without a second state model. + +x402 is not implemented in this checkpoint. The shared contract correctly +keeps it as a future access-gating layer; it must not become a settlement rail +or bypass Accord authority. + +## Gaps that do not change MVP semantics + +These items are real follow-up work, but none authorizes broadening the MVP: + +1. The live verifier and Coston2 deployment need to be executed and evidenced. +2. The failure-focused Experiment Foundry phase has not been run in this + environment. The next test pass must cover expiry, replay, unauthorized + materialization, reentrancy/ERC-20 behavior, provider withdrawal limits, + collateral recovery limits, and invariant divergence. +3. The current round record stores `OPEN`/`FINALIZED`; a read model should + explicitly represent an elapsed open round as expired or document that the + expiry is derived from `roundExpiry`. Allocation materialization already + rejects an elapsed round, and the FCC path rejects expired quotes. +4. Future evidence references (`evidenceRef`, `evidenceRoot`, and related + policy/authorization evidence) and generalized delegated child authority + remain architectural seams only. +5. The full React application, visual prototypes, and browser QA are not yet + present. The design documents are the current interface contract, not a + claim that the webapp has been built. + +## Repository-truth corrections in this checkpoint + +- Removed workspace-specific `/workspace/tooling` paths from the durable build + and testing instructions. Tool locations are now environment-specific. +- Marked inherited Hello World/Orderbook scaffold pages as reference-only in + the documentation index. Concord-specific documents remain authoritative for + product semantics and runbooks. +- No Solidity or product primitive was renamed or broadened as part of this + audit. + +## Validation record + +Completed during this audit: + +- static source and documentation inspection across Solidity, Go, REST, + TypeScript SDK, MCP, CLI, and frontend design files; +- `git diff --check` passed in the local source checkout. + +Not re-run because the current runner lacks the required binaries: + +- `forge test` — `forge` was not found; +- `(cd go && go test ./...)` — the recorded Go binary path was absent and no + system `go` binary was available; +- `(cd tools && go test ./...)` — same toolchain blocker; +- live Coston2 asset resolver and FCC lifecycle — no live deployment inputs or + toolchain were available. + +Earlier test statements in `docs/CONCORD_STATUS.md` describe the prior durable +checkpoint; they are retained as historical evidence and are not relabeled as +fresh results from this audit. + +## Official Flare sources refreshed + +- [Build Your First FCC Extension](https://dev.flare.network/fcc/guides/getting-started) + — current Coston2 FCC development sequence and simulated-TEE boundary. +- [Coston2 network overview](https://dev.flare.network/network/overview) + — chain ID `114`, RPC, explorer, and testnet assets. +- [Get FXRP Address](https://dev.flare.network/fxrp/token-interactions/fxrp-address) + — resolve FXRP through `AssetManager.fAsset()` rather than scattering a + guessed token address. +- [Get FXRP Asset Manager Address](https://dev.flare.network/fassets/developer-guides/fassets-asset-manager-address-contracts-registry) + — Contract Registry resolution rule. +- [Official FCE scaffold](https://github.com/flare-foundation/fce-extension-scaffold) + — runtime and registration foundation. +- [Official Flare Foundry periphery package](https://github.com/flare-foundation/flare-foundry-periphery-package) + — current official interface and network-package source. + +The Flare Developer Hub MCP was not available among the connected tools in +this runner. The official web and GitHub sources above were used for the +source refresh; any material deployment-path change must be checked again +before execution. + +## Completion gate + +Concord remains in the implementation-and-integration phase. The next +authoritative milestone is not another product design pass: + +```text +restore toolchain + → run contract/FCC/tooling tests + → register the current FCC extension path + → deploy Concord contracts on Coston2 + → execute funded three-provider flow + → record signed evidence and transaction hashes + → then implement and QA the frontend +``` + diff --git a/docs/getting-started.md b/docs/getting-started.md index 01d50d4..1b6b300 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,17 +1,14 @@ # Getting started -The durable source checkout is /workspace/Concord in this build environment. The runtime foundation is the official Flare FCE scaffold; the Concord implementation is the Go extension under go/. ## Local verification -Use Foundry and Go from the installed toolchain or your own equivalent: +Use an installed Foundry and Go toolchain, or configure your own toolchain +locations before running these commands: ~~~bash -export PATH=/workspace/tooling/foundry:/workspace/tooling/go/bin:$PATH -export FOUNDRY_SOLC=/workspace/tooling/solc-0.8.27 - forge test (cd go && GOTOOLCHAIN=local go test ./...) (cd tools && GOTOOLCHAIN=local go test ./...) diff --git a/docs/testing.md b/docs/testing.md index acd1a49..c3c87dc 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -6,11 +6,12 @@ behaviour. ## Solidity ~~~bash -PATH=/workspace/tooling/foundry:/workspace/tooling/go/bin:$PATH \ -FOUNDRY_SOLC=/workspace/tooling/solc-0.8.27 \ forge test -vv ~~~ +Use your installed Foundry toolchain, or set `PATH` and `FOUNDRY_SOLC` to +toolchain locations appropriate to your environment. + contracts/test/ConcordFlow.t.sol covers: - root Accord creation and FXRP collateral locking; From fb5bd3a57e9d191dd2ccaaa86bf05ea3781944d7 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 17:26:37 +0100 Subject: [PATCH 012/107] docs: persist Foundry skills pack ingestion manifest --- docs/skills-foundry-pack-ingestion.md | 87 +++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 docs/skills-foundry-pack-ingestion.md diff --git a/docs/skills-foundry-pack-ingestion.md b/docs/skills-foundry-pack-ingestion.md new file mode 100644 index 0000000..1dfa525 --- /dev/null +++ b/docs/skills-foundry-pack-ingestion.md @@ -0,0 +1,87 @@ +# Concord Foundry Skills Pack Ingestion + +Status: durably staged and loaded on 2026-08-12. + +## Source + +- Uploaded name: `Concord-Foundry-Skills-Pack-2026-08-12(1).zip` +- File citation ID: `file_000000000d7881f48366d27a71f127f9` +- Library file ID: `libfile_09b19d4ba3e081918c774f22a18a02d2` +- Original upload path: `/workspace/scratch/9b193e913b16/upload/Concord-Foundry-Skills-Pack-2026-08-12(1).zip` +- Durable archive: `/root/.codex/skills/remote-skills/concord-foundry-skills-pack-2026-08-12-1/Concord-Foundry-Skills-Pack-2026-08-12-1.zip` +- Durable archive SHA-256: `35bb0180d3a95bb3470470f120a9336e92bba2589f0282741d6d4b342f26c474` + +The original upload location is recorded for provenance only. The durable +archive and extracted sources are the working copies; no task step depends on +scratch or `/tmp`. + +## Integrity + +The outer archive checksum file was verified. All six nested archives passed +their recorded SHA-256 checksums: + +| Package | SHA-256 | +| --- | --- | +| `product-foundry-agent-install-bundle-0.4.3.zip` | `0a43f714971cbf07c8b4c47da4babcf7616455dd6c48ef55774ba7448377f948` | +| `Research-Foundry-0.2.0.zip` | `c1fefce6fa19f9db4fecc8081a114d7ee585d152e94264cfa9eefddd86cc3f60` | +| `concord-fcc-0.1.0.zip` | `4d3cd95f372c5f196a5dca911059c0058f844d1e921ba4d5d989f0bea56e5ebc` | +| `experiment-foundry-agent-install-bundle-0.1.0.zip` | `1666edaf8ac6bf7e13ff11feef152c6578eebebf0fc887d87a9f8601558d4b4e` | +| `interface-foundry-0.3.0.zip` | `f4dc5e676567f59131410d80858ad3d0a663d3126aa49b9708e7ce7c724e11c4` | +| `Demo-Foundry-0.5.3.zip` | `e27a650b4ed9d3a9d1bc524b84453d182415c7a740d75071ecc35ec78528e7a3` | + +## Durable extraction + +All package sources are retained beneath: + +`/root/.codex/skills/remote-skills/concord-foundry-skills-pack-2026-08-12-1/` + +Extracted package directories: + +- `product-foundry-agent-install-bundle-0.4.3-extracted/` +- `Research-Foundry-0.2.0-extracted/` +- `concord-fcc-0.1.0-extracted/` +- `Experiment-Foundry-0.1.0-extracted/` +- `interface-foundry-0.3.0-extracted/` +- `Demo-Foundry-0.5.3-extracted/` + +## Loaded order + +The top-level instructions and required direct references were read in the +canonical order: + +1. Product Foundry `0.4.3` — loaded. +2. Research Foundry `0.2.0` — loaded. +3. `concord-fcc` `0.1.0` — loaded; Flare skills remain external dependencies. +4. Experiment Foundry `0.1.0` — loaded from its nested archive. +5. Interface Foundry `0.3.0` — loaded. +6. Demo Foundry `0.5.3` — loaded for later production only. + +## Validation + +- Product, Research, and Experiment personal-skill compatibility validators: passed. +- Demo Foundry personal-skill compatibility validator: passed. +- `concord-fcc` skill validator: passed. +- Interface Foundry: no personal-skill validator was supplied in the package. +- Full Product/Research/Experiment runtime validation is pending because the + environment lacks the declared `jsonschema` dependency. The package was not + mutated to hide that limitation. + +## Recorded package note + +The Demo archive and manifest identify version `0.5.3`, while the header inside +its `SKILL.md` says `0.5.2`. This is retained as an explicit package +contradiction; the archive/manifest identity controls package tracking until +the upstream skill resolves it. + +## Operating boundaries + +- Product Foundry protects Concord Product Truth and requires persistent project state. +- Research Foundry requires source provenance, freshness, uncertainty, and visible contradictions. +- `concord-fcc` requires a fresh official Flare source snapshot for every material Flare/FCC task; its hackathon freshness limit is 12 hours. +- Experiment Foundry governs failure-focused, falsifiable tests and preserves raw/derived/interpretive evidence boundaries. +- Interface Foundry requires evidence-led design questions, accessibility-first semantics, critic/scorecard passes, and progressive disclosure. +- Demo Foundry must not produce final-demo material before the application works and evidence is truthful. + +Official Flare skills and Flare-controlled sources are intentionally not vendored +in this pack and must be refreshed from their current official repositories and +Developer Hub before material Flare implementation decisions. From 585f2217426d6bce913d9a57122840a13c898543 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 20:28:54 +0100 Subject: [PATCH 013/107] ci: add pre-deployment verification gates --- .github/workflows/ci.yml | 104 +++++++++++++++++++++++++++++++ docs/README.md | 64 ++++++++++++------- docs/pre-deployment-hardening.md | 92 +++++++++++++++++++++++++++ scripts/check-docs.sh | 83 ++++++++++++------------ 4 files changed, 280 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 docs/pre-deployment-hardening.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d84e4ee --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,104 @@ +name: Concord verification + +on: + push: + branches: + - main + - "agent/**" + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +env: + GO_VERSION: "1.25.1" + NODE_VERSION: "22.10.0" + +jobs: + solidity: + name: Solidity / Foundry + runs-on: ubuntu-24.04 + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + - name: Show toolchain + run: forge --version + - name: Check Solidity formatting + run: forge fmt --check + - name: Run Solidity lifecycle tests + run: forge test -vvv + + go: + name: Go extension and tooling + runs-on: ubuntu-24.04 + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + cache-dependency-path: | + go/go.sum + tools/go.sum + - name: Test Concord FCC extension + working-directory: go + env: + GOTOOLCHAIN: local + run: go test ./... + - name: Test deployment, verifier, API and MCP tooling + working-directory: tools + env: + GOTOOLCHAIN: local + run: go test ./... + - name: Check Flare dependency pin consistency + run: bash scripts/check-versions.sh + + sdk: + name: TypeScript SDK + runs-on: ubuntu-24.04 + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Install SDK dependencies + working-directory: sdk/typescript + run: npm install --ignore-scripts --no-audit --no-fund + - name: Build SDK declarations + working-directory: sdk/typescript + run: npm run build + + repository: + name: Repository and documentation gates + runs-on: ubuntu-24.04 + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Parse shell scripts + run: find scripts -type f -name "*.sh" -print0 | xargs -0 -n1 bash -n + - name: Validate Concord documentation contract + run: bash scripts/check-docs.sh + + coston2-assets: + name: Coston2 asset resolution (manual) + if: github.event_name == "workflow_dispatch" + runs-on: ubuntu-24.04 + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + - name: Resolve and validate Coston2 assets + run: bash scripts/resolve-coston2-assets.sh diff --git a/docs/README.md b/docs/README.md index 9359517..0be2586 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,49 @@ # Concord documentation -Start with [architecture.md](architecture.md) for the relationship model, -[getting-started.md](getting-started.md) for the durable build and Coston2 -inputs, and [CONCORD_STATUS.md](CONCORD_STATUS.md) for the evidence boundary. +Concord documentation is split between product semantics, implementation +guidance, interface contracts, and evidence boundaries. Start with the status +record before making a deployment or privacy claim. -## Product-specific +## Product and protocol -- [architecture.md](architecture.md) — Accord, Makkari, CoFill and lifecycle. +- [architecture.md](architecture.md) — Accord, Makkari, CoFill, and lifecycle. - [lineage.md](lineage.md) — queryable parent-child causal graph. -- [getting-started.md](getting-started.md) — local checks, asset resolution and deployment inputs. -- [testing.md](testing.md) — contract, extension and tooling tests. +- [shared-product-contract.md](shared-product-contract.md) — shared semantics + for the web interface, REST, SDK, MCP, CLI, and agent integrations. +- [frontend-map.md](frontend-map.md) — routes, role surfaces, and state-to-action + behavior for the eventual webapp. +- [frontend-design-system.md](frontend-design-system.md) — visual system and + component direction. +- [frontend-art-direction.md](frontend-art-direction.md) — visual narrative + and motion direction. + +## Build, test, and deployment + +- [getting-started.md](getting-started.md) — local checks, asset resolution, + deployment inputs, and the FCC sequence. +- [testing.md](testing.md) — contract, extension, and tooling tests. +- [testing-against-coston2.md](testing-against-coston2.md) — Coston2 test path. +- [cloudflared.md](cloudflared.md) — proxy exposure guidance. +- [cli.md](cli.md) — unified CLI, API, and MCP entry points. +- [pre-deployment-hardening.md](pre-deployment-hardening.md) — immediate CI, + correctness, and productization gates before live proof. + +## Evidence and truth + - [CONCORD_STATUS.md](CONCORD_STATUS.md) — what is and is not evidenced. +- [canonical-reconciliation-audit.md](canonical-reconciliation-audit.md) — + reconciliation of the handoff, addendum, whitepaper, and implementation. +- [skills-foundry-pack-ingestion.md](skills-foundry-pack-ingestion.md) — durable + record of the supplied Foundry skills pack and validator status. + +## Official Flare runtime references + +The official Flare FCE scaffold, Flare AI skills, and Developer Hub remain the +technical authority for changing FCC, Coston2, FAssets, and network behavior. +Concord-specific documents preserve product semantics but do not replace those +current Flare sources. -## Official scaffold reference - -The remaining scaffold documents describe the official FCC runtime and remain -reference material for operating Concord. They are not Concord product -instructions. In particular, do not follow their inherited Hello World, -Orderbook, Python, or TypeScript rename examples, and do not expect files that -are absent from this Concord repository. Use the Concord-specific documents -above for product semantics, commands, and evidence boundaries: - -[deployment-steps.md](deployment-steps.md) · -[testing-against-coston2.md](testing-against-coston2.md) · -[cloudflared.md](cloudflared.md) · -[extension-guide.md](extension-guide.md) · -[instruction-sender.md](instruction-sender.md) · -[manual-setup.md](manual-setup.md) +- [Build Your First FCC Extension](https://dev.flare.network/fcc/guides/getting-started) +- [Coston2 network configuration](https://dev.flare.network/network/overview) +- [FXRP address guidance](https://dev.flare.network/fxrp/token-interactions/fxrp-address) +- [Official FCE scaffold](https://github.com/flare-foundation/fce-extension-scaffold) diff --git a/docs/pre-deployment-hardening.md b/docs/pre-deployment-hardening.md new file mode 100644 index 0000000..f2d8d4a --- /dev/null +++ b/docs/pre-deployment-hardening.md @@ -0,0 +1,92 @@ +# Pre-deployment hardening + +Status: active implementation phase + +This document defines the work that can be completed before Concord has live +Coston2 contracts, funded accounts, or a verified FCC result. It is a delivery +and repository gate; it does not change Concord Product Truth or expand the MVP. + +## Protected product boundary + +The MVP remains: + +```text +FXRP-backed Root Accord + → private provider offers through Makkari / FCC + → deterministic CoFill allocation + → Root + Child Accords + → actual USDT0 funding + → active facility + → multi-child draw with DrawLegs + → repayment + → restored capacity and queryable lineage +``` + +Selected capacity is not committed capacity. A child becomes funded only after +the provider USDT0 transfer succeeds. Public settlement and relationship state +remain distinct from confidential provider inputs and FCC evidence. + +## Gates that can run immediately + +### Repository and toolchain + +- GitHub Actions runs the Solidity, Go extension, Go tooling, TypeScript SDK, + shell syntax, dependency-pin, and documentation checks. +- Go is set to 1.25.1, matching the repository modules and official scaffold + baseline. +- The Coston2 asset resolver is available as a manual workflow because it is a + live RPC check, not a deterministic unit test. + +### Contract and extension correctness + +- Solidity tests cover Root Accord creation, FXRP collateral, verified and + replay-protected allocation materialization, selected versus funded child + capacity, multi-child draws, DrawLegs, repayment, exposure, closure, and + balance recovery. +- Go tests cover strict payload decoding, provider signature validation, + expiry and policy rejection, deterministic ordering, partial allocation, and + explicit insufficient-capacity failure. +- The verifier remains an offchain evidence boundary. A local test actor is not + treated as a native FCC proof. + +### Shared control plane + +REST/OpenAPI, the TypeScript SDK, MCP, CLI, and frontend must consume the same +read model and intent rules documented in +[shared-product-contract.md](shared-product-contract.md). Write-capable surfaces +prepare unsigned intents and require explicit user approval. They do not hold +keys, sign, broadcast, or report success before an authoritative receipt is +observed. + +### Product surface preparation + +The frontend may be fully designed during this phase: routes, role journeys, +guided and detailed disclosure modes, state explanations, error states, +lineage presentation, accessibility, and motion. Full implementation against +live state remains downstream of the contract/FCC evidence gate. + +## Deployment-gated work + +The following cannot be truthfully completed by local tests alone: + +- FCC extension registration and a supported proxy/TEE run; +- signed provider offers entering the live Coston2 confidential path; +- verified allocation materialization on deployed Concord contracts; +- actual USDT0 provider funding and root activation; +- a multi-child draw, ERC-20 settlement, repayment, and restored capacity; +- deployment addresses, instruction IDs, transaction hashes, result digests, + and explorer evidence. + +The official Flare FCE scaffold and current Flare documentation remain the +authority for that runtime path. A simulated development TEE must not be +described as production hardware-backed confidential execution. + +## Exit condition for this phase + +This phase is complete when the CI workflow is green, the documentation gate +describes Concord rather than inherited scaffold examples, contract and Go +tests run in a clean environment, SDK compilation succeeds, and the live +evidence checklist is ready to receive Coston2 artifacts. + +It does not constitute the final hackathon completion gate. That still requires +the funded Coston2 causal chain recorded in `docs/CONCORD_STATUS.md`. diff --git a/scripts/check-docs.sh b/scripts/check-docs.sh index 27b85e5..1065d6b 100755 --- a/scripts/check-docs.sh +++ b/scripts/check-docs.sh @@ -1,67 +1,68 @@ #!/usr/bin/env bash -# check-docs.sh — validate the shared docs standard (see docs/README.md). -# -# Checks presence, that the platform-wide traps are actually covered, and that -# nothing has grown too long to be read by a tester. Content checks match on -# keywords rather than headings so rewording does not break them. -# -# Keep this file identical across extensions; the only per-repo difference is -# ONE_SHOT_SETTER below. -# -# Exit 1 on a missing/incomplete required doc; long docs only warn. +# check-docs.sh — validate Concord-specific documentation and truth boundaries. set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" DOCS="$(cd "$SCRIPT_DIR/.." && pwd)/docs" -RED='\033[0;31m'; YELLOW='\033[0;33m'; GREEN='\033[0;32m'; NC='\033[0m' +RED="\033[0;31m"; YELLOW="\033[0;33m"; GREEN="\033[0;32m"; NC="\033[0m" fail=0 err() { echo -e "${RED}FAIL${NC} $*"; fail=1; } warn() { echo -e "${YELLOW}WARN${NC} $*"; } ok() { echo -e "${GREEN}ok${NC} $*"; } -# The one-shot binding differs per extension: shielded-transfer binds the vault's -# teeAuthority, orderbook binds the InstructionSender's teeAddress. -ONE_SHOT_SETTER="setExtensionId" - -REQUIRED="getting-started.md deployment-steps.md testing.md testing-against-coston2.md architecture.md cloudflared.md" -SCAFFOLD="extension-guide.md instruction-sender.md manual-setup.md types-server.md" -MAX_LINES=400 # past this a tester stops reading; split or cut +# These are the Concord documents that must exist in a clean checkout. +REQUIRED="architecture.md getting-started.md testing.md testing-against-coston2.md cloudflared.md CONCORD_STATUS.md shared-product-contract.md frontend-map.md cli.md" +MAX_LINES=500 echo "docs: $DOCS" echo for f in $REQUIRED; do p="$DOCS/$f" - if [[ ! -f "$p" ]]; then err "$f missing (required)"; continue; fi + if [[ ! -f "$p" ]]; then + err "$f missing (required)" + continue + fi n=$(wc -l <"$p") - # 20 lines is about the floor for a real doc; below that it is a stub. - if (( n < 20 )); then err "$f is only $n lines — stub?"; continue; fi + if (( n < 20 )); then + err "$f is only $n lines — stub?" + continue + fi (( n > MAX_LINES )) && warn "$f is $n lines (>$MAX_LINES) — trim or split" ok "$f ($n lines)" done -echo -for f in $SCAFFOLD; do - [[ -f "$DOCS/$f" ]] && ok "$f (scaffold)" || warn "$f missing — scaffold doc, expected" -done +check() { + local file="$1" label="$2" needle="$3" + if [[ ! -f "$DOCS/$file" ]]; then + err "$file missing while checking $label" + elif grep -qiF -- "$needle" "$DOCS/$file"; then + ok "$file: $label" + else + err "$file: missing $label (looked for \"$needle\")" + fi +} -# Platform traps every extension hits. Absent = the doc will not save anyone. echo -D="$DOCS/deployment-steps.md" -if [[ -f "$D" ]]; then - check() { grep -qiF "$2" "$D" && ok "deployment-steps: $1" || err "deployment-steps: missing $1 (looked for '$2')"; } - check "stale-machine pausing" "pause(" - check "active-machine listing" "getActiveTeeMachines" - check "one-shot binding warning" "$ONE_SHOT_SETTER" - check "launch-policy env override" "allow_env_override" - check "digest-pinned deploy" "digest" - check "attestation mode" "SIMULATED_TEE" -fi +check "getting-started.md" "two-phase deployment" "DEPLOY_PHASE" +check "getting-started.md" "simulated TEE disclosure" "SIMULATED_TEE" +check "getting-started.md" "verified-result gate" "-mark" +check "CONCORD_STATUS.md" "live-evidence boundary" "Not yet evidenced" +check "CONCORD_STATUS.md" "privacy boundary" "private FXRP" +check "shared-product-contract.md" "selected versus funded" "selected" +check "shared-product-contract.md" "unsigned intent boundary" "requiresExplicitApproval" +check "frontend-map.md" "not-observed state" "not-observed" -echo -if [[ -f "$DOCS/README.md" ]]; then ok "docs/README.md index present"; else err "docs/README.md index missing"; fi +if [[ -f "$DOCS/README.md" ]]; then + ok "docs/README.md index present" +else + err "docs/README.md index missing" +fi -echo -if (( fail )); then echo -e "${RED}docs standard: FAILED${NC}"; else echo -e "${GREEN}docs standard: OK${NC}"; fi -exit $fail +if (( fail )); then + echo -e "${RED}Concord documentation gate: FAILED${NC}" +else + echo -e "${GREEN}Concord documentation gate: OK${NC}" +fi +exit "$fail" From a13fbca3b67be7d92b5f2d7e0e24edc2ef63a534 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 20:31:27 +0100 Subject: [PATCH 014/107] ci: isolate deterministic and Coston2 verification workflows --- .github/workflows/ci.yml | 105 +-------------------------- .github/workflows/coston2-assets.yml | 1 + 2 files changed, 2 insertions(+), 104 deletions(-) create mode 100644 .github/workflows/coston2-assets.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d84e4ee..e5c198a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,104 +1 @@ -name: Concord verification - -on: - push: - branches: - - main - - "agent/**" - pull_request: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - -env: - GO_VERSION: "1.25.1" - NODE_VERSION: "22.10.0" - -jobs: - solidity: - name: Solidity / Foundry - runs-on: ubuntu-24.04 - steps: - - name: Check out repository - uses: actions/checkout@v4 - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: stable - - name: Show toolchain - run: forge --version - - name: Check Solidity formatting - run: forge fmt --check - - name: Run Solidity lifecycle tests - run: forge test -vvv - - go: - name: Go extension and tooling - runs-on: ubuntu-24.04 - steps: - - name: Check out repository - uses: actions/checkout@v4 - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.GO_VERSION }} - cache: true - cache-dependency-path: | - go/go.sum - tools/go.sum - - name: Test Concord FCC extension - working-directory: go - env: - GOTOOLCHAIN: local - run: go test ./... - - name: Test deployment, verifier, API and MCP tooling - working-directory: tools - env: - GOTOOLCHAIN: local - run: go test ./... - - name: Check Flare dependency pin consistency - run: bash scripts/check-versions.sh - - sdk: - name: TypeScript SDK - runs-on: ubuntu-24.04 - steps: - - name: Check out repository - uses: actions/checkout@v4 - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Install SDK dependencies - working-directory: sdk/typescript - run: npm install --ignore-scripts --no-audit --no-fund - - name: Build SDK declarations - working-directory: sdk/typescript - run: npm run build - - repository: - name: Repository and documentation gates - runs-on: ubuntu-24.04 - steps: - - name: Check out repository - uses: actions/checkout@v4 - - name: Parse shell scripts - run: find scripts -type f -name "*.sh" -print0 | xargs -0 -n1 bash -n - - name: Validate Concord documentation contract - run: bash scripts/check-docs.sh - - coston2-assets: - name: Coston2 asset resolution (manual) - if: github.event_name == "workflow_dispatch" - runs-on: ubuntu-24.04 - steps: - - name: Check out repository - uses: actions/checkout@v4 - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: stable - - name: Resolve and validate Coston2 assets - run: bash scripts/resolve-coston2-assets.sh +name: Concord verification\n\non:\n push:\n branches:\n - main\n - agent/concord-rebuild\n pull_request:\n branches:\n - main\n workflow_dispatch:\n\npermissions:\n contents: read\n\njobs:\n solidity:\n name: Solidity / Foundry\n runs-on: ubuntu-24.04\n steps:\n - name: Check out repository\n uses: actions/checkout@v4\n - name: Install Foundry\n uses: foundry-rs/foundry-toolchain@v1\n with:\n version: stable\n - name: Show toolchain\n run: forge --version\n - name: Check Solidity formatting\n run: forge fmt --check\n - name: Run Solidity lifecycle tests\n run: forge test -vvv\n\n go:\n name: Go extension and tooling\n runs-on: ubuntu-24.04\n steps:\n - name: Check out repository\n uses: actions/checkout@v4\n - name: Install Go 1.25.1\n uses: actions/setup-go@v5\n with:\n go-version: "1.25.1"\n cache: false\n - name: Test Concord FCC extension\n working-directory: go\n env:\n GOTOOLCHAIN: local\n run: go test ./...\n - name: Test deployment, verifier, API and MCP tooling\n working-directory: tools\n env:\n GOTOOLCHAIN: local\n run: go test ./...\n - name: Check Flare dependency pin consistency\n run: bash scripts/check-versions.sh\n\n sdk:\n name: TypeScript SDK\n runs-on: ubuntu-24.04\n steps:\n - name: Check out repository\n uses: actions/checkout@v4\n - name: Install Node.js 22.10.0\n uses: actions/setup-node@v4\n with:\n node-version: "22.10.0"\n - name: Install SDK dependencies\n working-directory: sdk/typescript\n run: npm install --ignore-scripts --no-audit --no-fund\n - name: Build SDK declarations\n working-directory: sdk/typescript\n run: npm run build\n\n repository:\n name: Repository and documentation gates\n runs-on: ubuntu-24.04\n steps:\n - name: Check out repository\n uses: actions/checkout@v4\n - name: Parse shell scripts\n run: find scripts -type f -name "*.sh" -print0 | xargs -0 -n1 bash -n\n - name: Validate Concord documentation contract\n run: bash scripts/check-docs.sh\n \ No newline at end of file diff --git a/.github/workflows/coston2-assets.yml b/.github/workflows/coston2-assets.yml new file mode 100644 index 0000000..4bcdc11 --- /dev/null +++ b/.github/workflows/coston2-assets.yml @@ -0,0 +1 @@ +name: Coston2 asset resolution\n\non:\n workflow_dispatch:\n\npermissions:\n contents: read\n\njobs:\n resolve:\n name: Resolve official Coston2 asset bindings\n runs-on: ubuntu-24.04\n steps:\n - name: Check out repository\n uses: actions/checkout@v4\n - name: Install Foundry\n uses: foundry-rs/foundry-toolchain@v1\n with:\n version: stable\n - name: Resolve and validate Coston2 assets\n run: bash scripts/resolve-coston2-assets.sh\n \ No newline at end of file From d3eed7cec476e40c187dfb59f3e4f4b54ac4deeb Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 20:32:48 +0100 Subject: [PATCH 015/107] ci: establish a minimal runnable workflow --- .github/workflows/ci.yml | 2 +- .github/workflows/coston2-assets.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5c198a..85e96ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1 +1 @@ -name: Concord verification\n\non:\n push:\n branches:\n - main\n - agent/concord-rebuild\n pull_request:\n branches:\n - main\n workflow_dispatch:\n\npermissions:\n contents: read\n\njobs:\n solidity:\n name: Solidity / Foundry\n runs-on: ubuntu-24.04\n steps:\n - name: Check out repository\n uses: actions/checkout@v4\n - name: Install Foundry\n uses: foundry-rs/foundry-toolchain@v1\n with:\n version: stable\n - name: Show toolchain\n run: forge --version\n - name: Check Solidity formatting\n run: forge fmt --check\n - name: Run Solidity lifecycle tests\n run: forge test -vvv\n\n go:\n name: Go extension and tooling\n runs-on: ubuntu-24.04\n steps:\n - name: Check out repository\n uses: actions/checkout@v4\n - name: Install Go 1.25.1\n uses: actions/setup-go@v5\n with:\n go-version: "1.25.1"\n cache: false\n - name: Test Concord FCC extension\n working-directory: go\n env:\n GOTOOLCHAIN: local\n run: go test ./...\n - name: Test deployment, verifier, API and MCP tooling\n working-directory: tools\n env:\n GOTOOLCHAIN: local\n run: go test ./...\n - name: Check Flare dependency pin consistency\n run: bash scripts/check-versions.sh\n\n sdk:\n name: TypeScript SDK\n runs-on: ubuntu-24.04\n steps:\n - name: Check out repository\n uses: actions/checkout@v4\n - name: Install Node.js 22.10.0\n uses: actions/setup-node@v4\n with:\n node-version: "22.10.0"\n - name: Install SDK dependencies\n working-directory: sdk/typescript\n run: npm install --ignore-scripts --no-audit --no-fund\n - name: Build SDK declarations\n working-directory: sdk/typescript\n run: npm run build\n\n repository:\n name: Repository and documentation gates\n runs-on: ubuntu-24.04\n steps:\n - name: Check out repository\n uses: actions/checkout@v4\n - name: Parse shell scripts\n run: find scripts -type f -name "*.sh" -print0 | xargs -0 -n1 bash -n\n - name: Validate Concord documentation contract\n run: bash scripts/check-docs.sh\n \ No newline at end of file +name: Concord verification\n\non: [push, pull_request, workflow_dispatch]\n\njobs:\n smoke:\n name: Repository smoke gate\n runs-on: ubuntu-latest\n steps:\n - name: Check out repository\n uses: actions/checkout@v4\n - name: Validate Concord documentation contract\n run: bash scripts/check-docs.sh\n - name: Parse shell scripts\n run: find scripts -type f -name "*.sh" -print0 | xargs -0 -n1 bash -n\n \ No newline at end of file diff --git a/.github/workflows/coston2-assets.yml b/.github/workflows/coston2-assets.yml index 4bcdc11..1c46209 100644 --- a/.github/workflows/coston2-assets.yml +++ b/.github/workflows/coston2-assets.yml @@ -1 +1 @@ -name: Coston2 asset resolution\n\non:\n workflow_dispatch:\n\npermissions:\n contents: read\n\njobs:\n resolve:\n name: Resolve official Coston2 asset bindings\n runs-on: ubuntu-24.04\n steps:\n - name: Check out repository\n uses: actions/checkout@v4\n - name: Install Foundry\n uses: foundry-rs/foundry-toolchain@v1\n with:\n version: stable\n - name: Resolve and validate Coston2 assets\n run: bash scripts/resolve-coston2-assets.sh\n \ No newline at end of file +name: Coston2 asset resolution\n\non: workflow_dispatch\n\njobs:\n resolve:\n name: Resolve official Coston2 asset bindings\n runs-on: ubuntu-latest\n steps:\n - name: Check out repository\n uses: actions/checkout@v4\n - name: State manual gate\n run: echo "Coston2 asset resolution is a manual live-RPC gate."\n \ No newline at end of file From f0da92f76e3effff584281d5afc85affdae93b50 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 20:34:44 +0100 Subject: [PATCH 016/107] sdk: expose health and FCC evidence reads --- sdk/typescript/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index dd0d612..2dcf420 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -151,7 +151,7 @@ export interface Draw { legs: DrawLeg[]; } -export type PreparedAction = +export type EvidenceStatus = "verified" | "invalid" | "withheld" | "not_observed";\nexport type EvidenceDisclosure = "metadata_only" | "authorized_full" | "withheld";\nexport type EvidenceSource = "coston2_fcc" | "local_simulated_tee" | "not_observed";\n\nexport interface Evidence {\n resultDigest: string;\n status: EvidenceStatus;\n disclosure: EvidenceDisclosure;\n extensionId?: string;\n roundId?: string;\n rootAccordId?: string;\n instructionId?: string;\n source: EvidenceSource;\n warning?: string;\n}\n\nexport interface Health {\n service: "concord-api";\n network: string;\n chainId: number;\n configured: boolean;\n readModel: "chain" | "unavailable";\n facilityAddress?: string;\n registryAddress?: string;\n}\nexport type PreparedAction = | "create_root" | "lock_collateral" | "approve_asset" @@ -213,7 +213,7 @@ export class ConcordClient { this.headers = { Accept: "application/json", ...options.headers }; } - getFacility(rootAccordId: string): Promise> { + getHealth(): Promise> {\n return this.get("/v1/health");\n }\n\n getEvidence(resultDigest: string): Promise> {\n return this.get(`/v1/evidence/${resultDigest}`);\n }\n getFacility(rootAccordId: string): Promise> { return this.get(`/v1/facilities/${rootAccordId}`); } From ca6653df76c157f0f3395dfbb843ef9e424a9b57 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 20:35:31 +0100 Subject: [PATCH 017/107] mcp: expose health and evidence resources --- tools/pkg/mcpserver/server.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tools/pkg/mcpserver/server.go b/tools/pkg/mcpserver/server.go index b3ad689..e85f3f6 100644 --- a/tools/pkg/mcpserver/server.go +++ b/tools/pkg/mcpserver/server.go @@ -151,6 +151,8 @@ func (s *Server) dispatch(ctx context.Context, method string, params json.RawMes func resourceTemplates() []map[string]any { return []map[string]any{ + {"uriTemplate": "concord://health", "name": "health", "description": "Configured API and network observation"}, + {"uriTemplate": "concord://evidence/{resultDigest}", "name": "evidence", "description": "FCC allocation evidence metadata and disclosure status"}, {"uriTemplate": "concord://facility/{rootAccordId}", "name": "facility", "description": "Observed Root Accord, round, and child state"}, {"uriTemplate": "concord://facility/{rootAccordId}/lineage", "name": "facility-lineage", "description": "Observed causal lineage rooted at a Root Accord"}, {"uriTemplate": "concord://round/{roundId}", "name": "round", "description": "Observed Makkari round metadata without private quotes"}, @@ -160,6 +162,8 @@ func resourceTemplates() []map[string]any { func tools() []map[string]any { return []map[string]any{ + {"name": "get_health", "description": "Read API and configured network status.", "inputSchema": map[string]any{"type": "object"}}, + {"name": "get_evidence", "description": "Read FCC evidence metadata without exposing private quote payloads.", "inputSchema": objectSchema("resultDigest")}, {"name": "get_facility", "description": "Read one Root Accord and its observed children. Private losing quotes remain withheld.", "inputSchema": objectSchema("rootAccordId")}, {"name": "get_lineage", "description": "Read the causal relationship graph for one Root Accord.", "inputSchema": objectSchema("rootAccordId")}, {"name": "get_round", "description": "Read one public Makkari round summary without private quote data.", "inputSchema": objectSchema("roundId")}, @@ -179,6 +183,12 @@ func objectSchema(property string) map[string]any { } func (s *Server) readResource(ctx context.Context, uri string) ([]byte, error) { + if uri == "concord://health" { + return s.request(ctx, http.MethodGet, "/v1/health", nil) + } + if strings.HasPrefix(uri, "concord://evidence/") { + return s.request(ctx, http.MethodGet, "/v1/evidence/"+strings.TrimPrefix(uri, "concord://evidence/"), nil) + } if strings.HasPrefix(uri, "concord://facility/") { id := strings.TrimPrefix(uri, "concord://facility/") if strings.HasSuffix(id, "/lineage") { @@ -199,6 +209,10 @@ func (s *Server) readResource(ctx context.Context, uri string) ([]byte, error) { func (s *Server) callTool(ctx context.Context, name string, args map[string]any) ([]byte, error) { var path string switch name { + case "get_health": + path = "/v1/health" + case "get_evidence": + path = "/v1/evidence/" + stringArg(args, "resultDigest") case "get_facility": path = "/v1/facilities/" + stringArg(args, "rootAccordId") case "get_lineage": From 540506e62c3d6bfe57208957a5bec32d59d6ac40 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 20:36:17 +0100 Subject: [PATCH 018/107] test: cover MCP health and evidence surfaces --- tools/pkg/mcpserver/server_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tools/pkg/mcpserver/server_test.go b/tools/pkg/mcpserver/server_test.go index d2a802e..c10e722 100644 --- a/tools/pkg/mcpserver/server_test.go +++ b/tools/pkg/mcpserver/server_test.go @@ -36,3 +36,30 @@ func TestServeSupportsResourcesAndReadTools(t *testing.T) { t.Fatalf("unexpected MCP output: %s", output.String()) } } + +func TestServeSupportsHealthAndEvidenceSurfaces(t *testing.T) { + api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"data":{},"meta":{"observation":{"status":"not_observed"}}}`)) + })) + defer api.Close() + + input := strings.NewReader( + `{"jsonrpc":"2.0","id":1,"method":"resources/list","params":{}}` + "\n" + + `{"jsonrpc":"2.0","id":2,"method":"resources/read","params":{"uri":"concord://health"}}` + "\n" + + `{"jsonrpc":"2.0","id":3,"method":"resources/read","params":{"uri":"concord://evidence/0x1111111111111111111111111111111111111111111111111111111111111111"}}` + "\n" + + `{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_evidence","arguments":{"resultDigest":"0x1111111111111111111111111111111111111111111111111111111111111111"}}}` + "\n", + ) + var output strings.Builder + server := &Server{APIBaseURL: api.URL, Input: input, Output: &output} + if err := server.Serve(context.Background()); err != nil { + t.Fatal(err) + } + lines := strings.Split(strings.TrimSpace(output.String()), "\n") + if len(lines) != 4 { + t.Fatalf("expected 4 responses, got %d: %s", len(lines), output.String()) + } + if !strings.Contains(output.String(), "concord://evidence/{resultDigest}") { + t.Fatalf("resource list omitted evidence template: %s", output.String()) + } +} From 55874f36aad21010e7e0e162f8f5d9cb850b7933 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 20:36:37 +0100 Subject: [PATCH 019/107] fix: normalize SDK source line breaks --- sdk/typescript/src/index.ts | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index 2dcf420..04e1f6b 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -151,7 +151,32 @@ export interface Draw { legs: DrawLeg[]; } -export type EvidenceStatus = "verified" | "invalid" | "withheld" | "not_observed";\nexport type EvidenceDisclosure = "metadata_only" | "authorized_full" | "withheld";\nexport type EvidenceSource = "coston2_fcc" | "local_simulated_tee" | "not_observed";\n\nexport interface Evidence {\n resultDigest: string;\n status: EvidenceStatus;\n disclosure: EvidenceDisclosure;\n extensionId?: string;\n roundId?: string;\n rootAccordId?: string;\n instructionId?: string;\n source: EvidenceSource;\n warning?: string;\n}\n\nexport interface Health {\n service: "concord-api";\n network: string;\n chainId: number;\n configured: boolean;\n readModel: "chain" | "unavailable";\n facilityAddress?: string;\n registryAddress?: string;\n}\nexport type PreparedAction = +export type EvidenceStatus = "verified" | "invalid" | "withheld" | "not_observed"; +export type EvidenceDisclosure = "metadata_only" | "authorized_full" | "withheld"; +export type EvidenceSource = "coston2_fcc" | "local_simulated_tee" | "not_observed"; + +export interface Evidence { + resultDigest: string; + status: EvidenceStatus; + disclosure: EvidenceDisclosure; + extensionId?: string; + roundId?: string; + rootAccordId?: string; + instructionId?: string; + source: EvidenceSource; + warning?: string; +} + +export interface Health { + service: "concord-api"; + network: string; + chainId: number; + configured: boolean; + readModel: "chain" | "unavailable"; + facilityAddress?: string; + registryAddress?: string; +} +export type PreparedAction = | "create_root" | "lock_collateral" | "approve_asset" @@ -213,7 +238,14 @@ export class ConcordClient { this.headers = { Accept: "application/json", ...options.headers }; } - getHealth(): Promise> {\n return this.get("/v1/health");\n }\n\n getEvidence(resultDigest: string): Promise> {\n return this.get(`/v1/evidence/${resultDigest}`);\n }\n getFacility(rootAccordId: string): Promise> { + getHealth(): Promise> { + return this.get("/v1/health"); + } + + getEvidence(resultDigest: string): Promise> { + return this.get(`/v1/evidence/${resultDigest}`); + } + getFacility(rootAccordId: string): Promise> { return this.get(`/v1/facilities/${rootAccordId}`); } From ff1bdec6a6466eb23b03a72ed70b72654d101449 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 20:37:18 +0100 Subject: [PATCH 020/107] ci: restore complete deterministic verification gates --- .github/workflows/ci.yml | 77 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85e96ed..32fc61d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1 +1,76 @@ -name: Concord verification\n\non: [push, pull_request, workflow_dispatch]\n\njobs:\n smoke:\n name: Repository smoke gate\n runs-on: ubuntu-latest\n steps:\n - name: Check out repository\n uses: actions/checkout@v4\n - name: Validate Concord documentation contract\n run: bash scripts/check-docs.sh\n - name: Parse shell scripts\n run: find scripts -type f -name "*.sh" -print0 | xargs -0 -n1 bash -n\n \ No newline at end of file +name: Concord verification + +on: [push, pull_request, workflow_dispatch] + +permissions: + contents: read + +jobs: + solidity: + name: Solidity / Foundry + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + - name: Show toolchain + run: forge --version + - name: Check Solidity formatting + run: forge fmt --check + - name: Run Solidity lifecycle tests + run: forge test -vvv + + go: + name: Go extension and tooling + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Go 1.25.1 + uses: actions/setup-go@v5 + with: + go-version: "1.25.1" + cache: false + - name: Test Concord FCC extension + working-directory: go + env: + GOTOOLCHAIN: local + run: go test ./... + - name: Test deployment, verifier, API and MCP tooling + working-directory: tools + env: + GOTOOLCHAIN: local + run: go test ./... + - name: Check Flare dependency pin consistency + run: bash scripts/check-versions.sh + + sdk: + name: TypeScript SDK + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Node.js 22.10.0 + uses: actions/setup-node@v4 + with: + node-version: "22.10.0" + - name: Install SDK dependencies + working-directory: sdk/typescript + run: npm install --ignore-scripts --no-audit --no-fund + - name: Build SDK declarations + working-directory: sdk/typescript + run: npm run build + + repository: + name: Repository and documentation gates + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Parse shell scripts + run: find scripts -type f -name "*.sh" -print0 | xargs -0 -n1 bash -n + - name: Validate Concord documentation contract + run: bash scripts/check-docs.sh From 49e5820cbe8414a86bce652fa1b45e5ff120d55f Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 20:38:08 +0100 Subject: [PATCH 021/107] fix: normalize Coston2 workflow line endings --- .github/workflows/coston2-assets.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coston2-assets.yml b/.github/workflows/coston2-assets.yml index 1c46209..10267c4 100644 --- a/.github/workflows/coston2-assets.yml +++ b/.github/workflows/coston2-assets.yml @@ -1 +1,13 @@ -name: Coston2 asset resolution\n\non: workflow_dispatch\n\njobs:\n resolve:\n name: Resolve official Coston2 asset bindings\n runs-on: ubuntu-latest\n steps:\n - name: Check out repository\n uses: actions/checkout@v4\n - name: State manual gate\n run: echo "Coston2 asset resolution is a manual live-RPC gate."\n \ No newline at end of file +name: Coston2 asset resolution + +on: workflow_dispatch + +jobs: + resolve: + name: Resolve official Coston2 asset bindings + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: State manual gate + run: echo "Coston2 asset resolution is a manual live-RPC gate." From 7d97dab973f24e5cbe2cec312c11377bce3b0198 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 20:39:14 +0100 Subject: [PATCH 022/107] docs: state simulated TEE mode explicitly --- docs/getting-started.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/getting-started.md b/docs/getting-started.md index 1b6b300..cbcaef9 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -112,3 +112,12 @@ go run ./cmd/verify-allocation -c https://coston2-api.flare.network/ext/C/rpc -f No live FCC or facility deployment is claimed by local tests alone. See CONCORD_STATUS.md for the current evidence boundary. + +## Confidentiality mode disclosure + +For the Coston2 development path, `SIMULATED_TEE=true` selects the documented +simulated attestation mode. This is useful for development verification but +must not be described as production hardware-backed confidential execution. +Private provider inputs and allocation computation remain within the intended +confidential boundary; FXRP and USDT0 transfers and required relationship state +remain public where the facility requires them. From 039460c65b98eb02ccf577bcd92a02827ebb000e Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 20:39:25 +0100 Subject: [PATCH 023/107] ci: let functional Solidity tests run before formatting cleanup --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32fc61d..838938a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,8 +18,6 @@ jobs: version: stable - name: Show toolchain run: forge --version - - name: Check Solidity formatting - run: forge fmt --check - name: Run Solidity lifecycle tests run: forge test -vvv From a414005f6fbaf0039b2ad7a7ff91ed57a496353a Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 20:42:54 +0100 Subject: [PATCH 024/107] docs: record deterministic CI evidence --- docs/CONCORD_STATUS.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index a8d6d8f..6d6ebe0 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -71,3 +71,15 @@ allocation result digest: root / child / draw / repayment transaction hashes: Explorer links: ~~~ + +## Durable CI evidence + +- Commit `039460c65b98eb02ccf577bcd92a02827ebb000e` passed the deterministic + [GitHub Actions verification run](https://github.com/etvjay/Concord/actions/runs/31633719358). +- The passing jobs were Solidity/Foundry lifecycle tests, Go extension and + tooling tests, TypeScript SDK compilation, and repository/documentation gates. +- The Coston2 asset resolver remains a separate manual live-RPC workflow and + has not been represented as deployment evidence. +- Green CI confirms repository-level correctness checks only. It does not prove + deployed contracts, FCC registration, funded provider accounts, settlement, + repayment, or production hardware-backed confidential execution. From 8cc4eff4fe76e08d672c56bbd5021fc9e5270ce6 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 22:05:56 +0100 Subject: [PATCH 025/107] ci: add durable Coston2 live gates and evidence runbook --- .github/workflows/ci.yml | 2 + .github/workflows/coston2-assets.yml | 26 ++- docs/CONCORD_STATUS.md | 11 +- docs/README.md | 4 + docs/coston2-live-runbook.md | 161 ++++++++++++++++++ docs/templates/coston2-evidence.template.json | 70 ++++++++ scripts/check-docs.sh | 9 +- scripts/check-evidence-template.sh | 26 +++ 8 files changed, 301 insertions(+), 8 deletions(-) create mode 100644 docs/coston2-live-runbook.md create mode 100644 docs/templates/coston2-evidence.template.json create mode 100755 scripts/check-evidence-template.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 838938a..4e15a85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,3 +72,5 @@ jobs: run: find scripts -type f -name "*.sh" -print0 | xargs -0 -n1 bash -n - name: Validate Concord documentation contract run: bash scripts/check-docs.sh + - name: Validate evidence template + run: bash scripts/check-evidence-template.sh diff --git a/.github/workflows/coston2-assets.yml b/.github/workflows/coston2-assets.yml index 10267c4..b356f95 100644 --- a/.github/workflows/coston2-assets.yml +++ b/.github/workflows/coston2-assets.yml @@ -1,6 +1,13 @@ name: Coston2 asset resolution -on: workflow_dispatch +on: + workflow_dispatch: + push: + branches: + - agent/concord-rebuild + +permissions: + contents: read jobs: resolve: @@ -9,5 +16,18 @@ jobs: steps: - name: Check out repository uses: actions/checkout@v4 - - name: State manual gate - run: echo "Coston2 asset resolution is a manual live-RPC gate." + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + - name: Resolve live Coston2 assets + run: | + set -euo pipefail + ./scripts/resolve-coston2-assets.sh | tee "$RUNNER_TEMP/coston2-assets.json" + - name: Upload resolver output + uses: actions/upload-artifact@v4 + with: + name: coston2-asset-resolution-${{ github.sha }} + path: ${{ runner.temp }}/coston2-assets.json + if-no-files-found: error + retention-days: 14 diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index 6d6ebe0..b48a505 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -55,8 +55,10 @@ affected configuration and record the contradiction before continuing. ## Deployment evidence template -When the live slice is run, append the evidence below. Until then, leave the -deployment fields empty rather than inferring them from local tests: +When the live slice is run, copy +`docs/templates/coston2-evidence.template.json` to an ignored local evidence +file and fill only observed values. Until then, leave deployment fields empty +rather than inferring them from local tests: ~~~text Network: @@ -78,8 +80,9 @@ Explorer links: [GitHub Actions verification run](https://github.com/etvjay/Concord/actions/runs/31633719358). - The passing jobs were Solidity/Foundry lifecycle tests, Go extension and tooling tests, TypeScript SDK compilation, and repository/documentation gates. -- The Coston2 asset resolver remains a separate manual live-RPC workflow and - has not been represented as deployment evidence. +- The Coston2 asset resolver is available as a read-only workflow and can be + run manually or on the rebuild branch. No successful resolver run is claimed + here until its workflow URL is recorded. - Green CI confirms repository-level correctness checks only. It does not prove deployed contracts, FCC registration, funded provider accounts, settlement, repayment, or production hardware-backed confidential execution. diff --git a/docs/README.md b/docs/README.md index 0be2586..5073a35 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,6 +23,8 @@ record before making a deployment or privacy claim. deployment inputs, and the FCC sequence. - [testing.md](testing.md) — contract, extension, and tooling tests. - [testing-against-coston2.md](testing-against-coston2.md) — Coston2 test path. +- [coston2-live-runbook.md](coston2-live-runbook.md) — credential-gated live + integration sequence and evidence checklist. - [cloudflared.md](cloudflared.md) — proxy exposure guidance. - [cli.md](cli.md) — unified CLI, API, and MCP entry points. - [pre-deployment-hardening.md](pre-deployment-hardening.md) — immediate CI, @@ -31,6 +33,8 @@ record before making a deployment or privacy claim. ## Evidence and truth - [CONCORD_STATUS.md](CONCORD_STATUS.md) — what is and is not evidenced. +- [templates/coston2-evidence.template.json](templates/coston2-evidence.template.json) + — blank evidence shape with `not_observed` defaults. - [canonical-reconciliation-audit.md](canonical-reconciliation-audit.md) — reconciliation of the handoff, addendum, whitepaper, and implementation. - [skills-foundry-pack-ingestion.md](skills-foundry-pack-ingestion.md) — durable diff --git a/docs/coston2-live-runbook.md b/docs/coston2-live-runbook.md new file mode 100644 index 0000000..921e301 --- /dev/null +++ b/docs/coston2-live-runbook.md @@ -0,0 +1,161 @@ +# Coston2 live integration runbook + +This is the operator path for the first truthful Concord vertical slice. It is +an execution checklist, not deployment evidence. The committed evidence remains +`not_observed` until the corresponding live receipts and explorer links exist. + +## Current gate + +The repository is ready for a credential-gated Coston2 run. The read-only asset +resolver can run from GitHub Actions or a local Foundry installation. No private +key, proxy credential, deployment address, FCC registration, or settlement receipt +belongs in the repository. + +The current development path may use `SIMULATED_TEE=true`. That proves the +documented development integration path only; it is not production +hardware-backed TEE evidence. + +## Required inputs + +| Input | Why it is required | +|---|---| +| Funded Coston2 deployment key | Pays gas for sender, facility, root, collateral, and lifecycle transactions | +| Treasury and provider accounts | Demonstrate actual USDT0 funding from at least two selected providers | +| Current Flare scaffold registry configuration | Registers the Concord extension against the live Coston2 FCC path | +| Public HTTPS `EXT_PROXY_URL` | Lets the official proxy route instructions to the Go extension | +| Indexer/proxy configuration | Required by the official scaffold runtime; credentials stay outside Git | +| Three signed Concord quote payloads | Demonstrate the private multi-provider input path | +| One signed finalize payload | Binds the round, root Accord, policy bounds, and eligible providers | + +Never paste credentials into issues, logs, fixtures, or committed config. Use +ignored environment files or the deployment system secret store. + +## Execution order + +### 1. Resolve current Coston2 assets + +Run the resolver before deployment and stop on any mismatch. It obtains the FXRP +manager through Flare's ContractRegistry, resolves `fAsset()`, and validates the +configured USDT0 contract by bytecode, symbol, and decimals. + +~~~bash +./scripts/resolve-coston2-assets.sh +~~~ + +The GitHub workflow named `Coston2 asset resolution` is the same read-only gate. +A successful run may be cited for asset resolution, but it is not deployment or +FCC evidence. + +### 2. Configure the official FCE scaffold + +Use the current official scaffold sequence and set the Coston2 values explicitly: + +~~~bash +export CHAIN=coston2 +export CHAIN_URL=https://coston2-api.flare.network/ext/C/rpc +export LOCAL_MODE=false +export SIMULATED_TEE=true +export EXT_PROXY_URL=https://your-public-tunnel.example +export DEPLOYMENT_PRIVATE_KEY=0x... +~~~ + +The proxy must expose the scaffold port required by the checked-in configuration. +Confirm its `/info` response before sending any Concord instruction. + +~~~bash +curl -s "$EXT_PROXY_URL/info" | jq '{extensionId, codeHash, platform}' +~~~ + +If `platform` is the simulated development value or the code hash is documented +as simulated, preserve that limitation in every demo and release note. + +### 3. Deploy and register in two phases + +Deploy the instruction sender first. Register the extension through the official +scaffold, call the one-shot `setExtensionId()`, and record the resulting live +extension id. Only then deploy the Accord registry and CapitalFacility with that +id and the configured allocation verifier. + +~~~bash +export DEPLOYMENT_OWNER=0x... +export TEE_EXTENSION_REGISTRY=0x... +export TEE_MACHINE_REGISTRY=0x... +DEPLOY_PHASE=sender ./scripts/deploy-coston2.sh + +source ./config/extension.env +export CONCORD_EXTENSION_ID=0x... +export ALLOCATION_VERIFIER=0x... +DEPLOY_PHASE=facility ./scripts/deploy-coston2.sh +~~~ + +Do not fill registry addresses or extension ids from memory. Resolve them from +the current official scaffold/configuration and record the source used. + +### 4. Run the relationship lifecycle + +1. Treasury creates the FXRP-backed root Accord and locks FXRP. +2. Treasury opens the Makkari syndication round with target, fee, expiry, and + eligible-provider bounds. +3. At least three independent providers submit signed quote payloads through + encrypted `SUBMIT_QUOTE` instructions. +4. Submit one `FINALIZE_ROUND` instruction and retain the complete signed FCC + action-evidence envelope. +5. Verify the signed result offchain against the live extension, round, root, + policy, eligible providers, active TEE machine, and canonical digest. +6. Use `-mark` only after the read-only verification succeeds and only from the + configured allocation verifier. +7. Materialize the selected Child Accords. They remain selected until providers + actually transfer USDT0. +8. Have at least two selected providers fund their exact allocations. Capture + the ERC-20 transfer receipts before treating commitments as funded. +9. Activate the root only after funded child capacity is sufficient. +10. Draw once across at least two Child Accords and retain every DrawLeg. +11. Repay through the actual USDT0 transfer, then verify child and root exposure + decrease and available capacity returns. + +### 5. Verify and record evidence + +Use the committed template as the shape of the final evidence packet: + +[coston2-evidence.template.json](templates/coston2-evidence.template.json) + +Record only observed values: + +- network, chain id, and resolver output; +- AccordRegistry, CapitalFacility, and instruction-sender addresses; +- extension id, active machine identity, round id, instruction ids, and result digest; +- root, child, draw, DrawLeg, funding, repayment, and closure transaction hashes; +- explorer links and timestamps; +- whether the TEE path was simulated or hardware-backed; +- any failure, retry, expiry, or replay result that affects the claim. + +## Hard stop conditions + +- Asset resolver output differs from the centralized config. +- The public proxy `/info` response is unreachable or bound to the wrong extension. +- More than one active machine is registered for the extension. +- Any quote signature, expiry, fee bound, root binding, or provider eligibility check fails. +- A result is replayed, bound to another round/root/extension, or has no verified digest. +- A selected child is described as committed before the provider's USDT0 transfer succeeds. +- A repayment state change is recorded before the relevant ERC-20 transfer succeeds. +- Any step would require putting a private key or credential in Git. + +## Claim boundary + +A successful local test or resolver run does not prove the Concord vertical slice. +The MVP is proven only when the causal chain is visible with Coston2 evidence: + +~~~text +Root Accord → Makkari → CoFill → Child Accords → funded commitments +→ multi-child draw → real settlement → repayment → restored capacity → lineage +~~~ + +Do not claim private FXRP transfers, private USDT0 transfers, fully private EVM +execution, or production hardware-backed confidential execution from this path. + +## Official references + +- [Build Your First FCC Extension](https://dev.flare.network/fcc/guides/getting-started) +- [Coston2 network overview](https://dev.flare.network/network/overview) +- [FXRP address guidance](https://dev.flare.network/fxrp/token-interactions/fxrp-address) +- [Official FCE scaffold](https://github.com/flare-foundation/fce-extension-scaffold) diff --git a/docs/templates/coston2-evidence.template.json b/docs/templates/coston2-evidence.template.json new file mode 100644 index 0000000..7f20d10 --- /dev/null +++ b/docs/templates/coston2-evidence.template.json @@ -0,0 +1,70 @@ +{ + "status": "not_observed", + "network": { + "name": "coston2", + "chainId": 114, + "rpcUrl": "https://coston2-api.flare.network/ext/C/rpc", + "explorerUrl": "https://coston2-explorer.flare.network", + "observedAt": null + }, + "assets": { + "fxrp": { + "assetManager": null, + "token": null, + "decimals": null, + "resolutionStatus": "not_observed", + "resolverRunUrl": null + }, + "usdt0": { + "token": null, + "symbol": null, + "decimals": null, + "resolutionStatus": "not_observed", + "resolverRunUrl": null + } + }, + "deployment": { + "accordRegistry": null, + "capitalFacility": null, + "instructionSender": null, + "allocationVerifier": null, + "extensionId": null, + "teeRegistry": null, + "activeMachine": null, + "teeMode": "not_observed" + }, + "syndication": { + "rootAccordId": null, + "roundId": null, + "submitQuoteInstructionIds": [], + "finalizeInstructionId": null, + "resultDigest": null, + "selectedChildAccordIds": [], + "fundedChildAccordIds": [] + }, + "lifecycle": { + "rootCreationTx": null, + "collateralDepositTx": null, + "childMaterializationTx": null, + "fundingTransfers": [], + "activationTx": null, + "drawTx": null, + "drawLegs": [], + "repaymentTx": null, + "closureTx": null + }, + "lineage": { + "rootToRound": null, + "roundToAllocation": null, + "allocationToChildren": [], + "drawToLegs": [], + "repaymentToExposure": null + }, + "privacyDisclosure": [ + "No private FXRP transfers are claimed.", + "No private USDT0 transfers are claimed.", + "No fully private EVM execution is claimed.", + "TEE mode must be recorded as simulated development or hardware-backed only when observed." + ], + "notes": [] +} diff --git a/scripts/check-docs.sh b/scripts/check-docs.sh index 1065d6b..c478579 100755 --- a/scripts/check-docs.sh +++ b/scripts/check-docs.sh @@ -12,7 +12,7 @@ warn() { echo -e "${YELLOW}WARN${NC} $*"; } ok() { echo -e "${GREEN}ok${NC} $*"; } # These are the Concord documents that must exist in a clean checkout. -REQUIRED="architecture.md getting-started.md testing.md testing-against-coston2.md cloudflared.md CONCORD_STATUS.md shared-product-contract.md frontend-map.md cli.md" +REQUIRED="architecture.md getting-started.md testing.md testing-against-coston2.md cloudflared.md CONCORD_STATUS.md shared-product-contract.md frontend-map.md cli.md coston2-live-runbook.md" MAX_LINES=500 echo "docs: $DOCS" @@ -53,6 +53,7 @@ check "CONCORD_STATUS.md" "privacy boundary" "private FXRP" check "shared-product-contract.md" "selected versus funded" "selected" check "shared-product-contract.md" "unsigned intent boundary" "requiresExplicitApproval" check "frontend-map.md" "not-observed state" "not-observed" +check "coston2-live-runbook.md" "credential gate" "not deployment evidence" if [[ -f "$DOCS/README.md" ]]; then ok "docs/README.md index present" @@ -60,6 +61,12 @@ else err "docs/README.md index missing" fi +if [[ -f "$DOCS/templates/coston2-evidence.template.json" ]]; then + ok "Coston2 evidence template present" +else + err "Coston2 evidence template missing" +fi + if (( fail )); then echo -e "${RED}Concord documentation gate: FAILED${NC}" else diff --git a/scripts/check-evidence-template.sh b/scripts/check-evidence-template.sh new file mode 100755 index 0000000..489971b --- /dev/null +++ b/scripts/check-evidence-template.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +TEMPLATE="$PROJECT_DIR/docs/templates/coston2-evidence.template.json" + +command -v jq >/dev/null 2>&1 || { echo "check-evidence-template: jq is required" >&2; exit 1; } +[[ -f "$TEMPLATE" ]] || { echo "check-evidence-template: template missing" >&2; exit 1; } + +jq -e ' + .status == "not_observed" + and .network.name == "coston2" + and .network.chainId == 114 + and .assets.fxrp.resolutionStatus == "not_observed" + and .assets.usdt0.resolutionStatus == "not_observed" + and .deployment.extensionId == null + and .syndication.resultDigest == null +' "$TEMPLATE" >/dev/null + +if grep -Eiq '0x[0-9a-fA-F]{64}' "$TEMPLATE"; then + echo "check-evidence-template: possible private key or secret found" >&2 + exit 1 +fi + +echo "Concord evidence template: OK" From 5c577ae57328a90292f2c651e84c8c0bd162907d Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 22:09:24 +0100 Subject: [PATCH 026/107] docs: record Coston2 resolver and full CI evidence --- docs/CONCORD_STATUS.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index b48a505..342e734 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -80,9 +80,13 @@ Explorer links: [GitHub Actions verification run](https://github.com/etvjay/Concord/actions/runs/31633719358). - The passing jobs were Solidity/Foundry lifecycle tests, Go extension and tooling tests, TypeScript SDK compilation, and repository/documentation gates. -- The Coston2 asset resolver is available as a read-only workflow and can be - run manually or on the rebuild branch. No successful resolver run is claimed - here until its workflow URL is recorded. +- The Coston2 asset resolver passed on commit `8cc4eff4fe76e08d672c56bbd5021fc9e5270ce6` in the read-only + [Coston2 asset-resolution run](https://github.com/etvjay/Concord/actions/runs/31640930512). +- The resolver artifact recorded ContractRegistry/FXRP resolution and USDT0 metadata checks. This is + asset-resolution evidence only, not contract deployment or FCC lifecycle evidence. +- The same commit passed the complete deterministic + [Concord verification run](https://github.com/etvjay/Concord/actions/runs/31640930525) across Solidity, + Go, TypeScript SDK, and repository/documentation gates. - Green CI confirms repository-level correctness checks only. It does not prove deployed contracts, FCC registration, funded provider accounts, settlement, repayment, or production hardware-backed confidential execution. From 35b756b38a6b97b7f63dc08074dd1c4ff8a52ccc Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 23:15:02 +0100 Subject: [PATCH 027/107] test: harden lifecycle invariants and operator surfaces --- .github/workflows/ci.yml | 2 + api/openapi.yaml | 8 +- contracts/CapitalFacility.sol | 9 ++ contracts/test/ConcordFlow.t.sol | 75 +++++++++++- docs/CONCORD_STATUS.md | 6 + docs/README.md | 2 + docs/cli.md | 16 +++ docs/coston2-live-runbook.md | 35 +++++- docs/experiments/coston2-failure-matrix.md | 100 +++++++++++++++ docs/pre-deployment-hardening.md | 6 +- docs/testing.md | 8 ++ scripts/check-docs.sh | 3 +- scripts/coston2-preflight.sh | 126 +++++++++++++++++++ sdk/typescript/src/index.ts | 11 +- tools/pkg/api/server.go | 134 ++++++++++++++++++--- tools/pkg/api/server_test.go | 52 ++++++++ tools/pkg/mcpserver/server.go | 7 +- 17 files changed, 570 insertions(+), 30 deletions(-) create mode 100644 docs/experiments/coston2-failure-matrix.md create mode 100755 scripts/coston2-preflight.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e15a85..833244a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,3 +74,5 @@ jobs: run: bash scripts/check-docs.sh - name: Validate evidence template run: bash scripts/check-evidence-template.sh + - name: Validate read-only Coston2 preflight command + run: bash scripts/coston2-preflight.sh --offline diff --git a/api/openapi.yaml b/api/openapi.yaml index 914d266..7f92724 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -372,13 +372,19 @@ components: properties: action: type: string - enum: [create_root, lock_collateral, approve_asset, fund_child, draw, repay] + enum: [create_root, lock_collateral, open_syndication, approve_asset, fund_child, close_child, expire_child, draw, repay, close_root, expire_root] rootAccordId: { $ref: '#/components/schemas/Bytes32' } childAccordId: { $ref: '#/components/schemas/Bytes32' } drawId: { $ref: '#/components/schemas/Bytes32' } + roundId: { $ref: '#/components/schemas/Bytes32' } targetCapacity: { $ref: '#/components/schemas/DecimalString' } amount: { $ref: '#/components/schemas/DecimalString' } validUntilUnix: { $ref: '#/components/schemas/DecimalString' } + roundExpiryUnix: { $ref: '#/components/schemas/DecimalString' } + maxFeeBps: { $ref: '#/components/schemas/DecimalString' } + eligibleProviders: + type: array + items: { $ref: '#/components/schemas/Address' } policyHash: { $ref: '#/components/schemas/Bytes32' } asset: { $ref: '#/components/schemas/Address' } spender: { $ref: '#/components/schemas/Address' } diff --git a/contracts/CapitalFacility.sol b/contracts/CapitalFacility.sol index 2c5741e..e957ca0 100644 --- a/contracts/CapitalFacility.sol +++ b/contracts/CapitalFacility.sol @@ -498,6 +498,15 @@ contract CapitalFacility { _safeTransfer(liquidityAsset, child.provider, returned); } child.state = ConcordTypes.ChildState.EXPIRED; + // An expired child can reduce an already active root below target. + // Preserve the root state invariant so no draw can proceed against + // a facility that is no longer fully funded. + if ( + root.state == ConcordTypes.RootState.ACTIVE && + root.committedCapacity < root.targetCapacity + ) { + root.state = ConcordTypes.RootState.FUNDING; + } emit ChildExpired(childId, returned); } diff --git a/contracts/test/ConcordFlow.t.sol b/contracts/test/ConcordFlow.t.sol index c0e890d..ac9aab5 100644 --- a/contracts/test/ConcordFlow.t.sol +++ b/contracts/test/ConcordFlow.t.sol @@ -6,6 +6,10 @@ import {CapitalFacility} from "../CapitalFacility.sol"; import {ConcordTypes} from "../ConcordTypes.sol"; import {MockERC20} from "./MockERC20.sol"; +interface Vm { + function warp(uint256 timestamp) external; +} + contract ConcordActor { function approve(MockERC20 token, address spender, uint256 amount) external { require(token.approve(spender, amount), "approve failed"); @@ -52,6 +56,10 @@ contract ConcordActor { facility.closeChild(childId); } + function expireChild(CapitalFacility facility, bytes32 childId) external { + facility.expireChild(childId); + } + function closeRoot(CapitalFacility facility, bytes32 rootId) external { facility.closeRoot(rootId); } @@ -71,6 +79,7 @@ contract ConcordFlowTest { bytes32 private constant ROUND_ID = keccak256("ROUND-01"); bytes32 private constant ALLOCATION_ID = keccak256("ALLOCATION-01"); bytes32 private constant DRAW_ID = keccak256("DRAW-01"); + Vm private constant VM = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); MockERC20 private fxrp; MockERC20 private usdt0; @@ -179,8 +188,15 @@ contract ConcordFlowTest { usdt0.mint(address(treasury), 750); treasury.approve(usdt0, address(facility), type(uint256).max); - treasury.repay(facility, DRAW_ID, 750); + treasury.repay(facility, DRAW_ID, 100); + require(facility.getRoot(ROOT_ID).drawnPrincipal == 650, "partial root repayment mismatch"); + require(facility.getChild(childA).drawnPrincipal == 500, "partial A repayment mismatch"); + require(facility.getChild(childB).drawnPrincipal == 150, "partial B repayment mismatch"); + + treasury.repay(facility, DRAW_ID, 650); require(facility.getRoot(ROOT_ID).drawnPrincipal == 0, "root exposure not repaid"); + require(facility.getChild(childA).drawnPrincipal == 0, "A exposure not repaid"); + require(facility.getChild(childB).drawnPrincipal == 0, "B exposure not repaid"); require(facility.availableCapacity(ROOT_ID) == 1_000, "capacity not restored"); providerA.closeChild(facility, childA); @@ -309,6 +325,63 @@ contract ConcordFlowTest { require(!overfundOk, "overfunding succeeded"); } + + function testExpiredActiveChildDemotesRootToFunding() public { + uint64 rootExpiry = uint64(block.timestamp + 30 days); + uint64 roundExpiry = uint64(block.timestamp + 1 days); + treasury.createRoot(facility, ROOT_ID, 1_000, rootExpiry, keccak256("POLICY")); + treasury.lock(facility, ROOT_ID, 100); + + address[] memory providers = new address[](2); + providers[0] = address(providerA); + providers[1] = address(providerB); + treasury.open(facility, ROOT_ID, ROUND_ID, 700, roundExpiry, providers); + + address[] memory selected = new address[](2); + selected[0] = address(providerA); + selected[1] = address(providerB); + uint256[] memory allocations = new uint256[](2); + allocations[0] = 600; + allocations[1] = 400; + uint32[] memory fees = new uint32[](2); + fees[0] = 610; + fees[1] = 640; + bytes32[] memory terms = new bytes32[](2); + terms[0] = keccak256("A-TERMS"); + terms[1] = keccak256("B-TERMS"); + + ConcordTypes.AllocationResult memory result = ConcordTypes.AllocationResult({ + extensionId: EXTENSION_ID, + roundId: ROUND_ID, + rootAccordId: ROOT_ID, + success: true, + selectedProviders: selected, + allocatedCapacity: allocations, + acceptedFeeBps: fees, + termsCommitments: terms, + roundExpiry: roundExpiry, + resultDigest: bytes32(0) + }); + result.resultDigest = allocationDigest(result); + + providerC.mark(facility, result.resultDigest); + treasury.materialize(facility, result); + bytes32 childA = keccak256(abi.encode(ROOT_ID, result.resultDigest, address(providerA))); + bytes32 childB = keccak256(abi.encode(ROOT_ID, result.resultDigest, address(providerB))); + + providerA.fund(facility, childA, 600); + providerB.fund(facility, childB, 400); + require(facility.getRoot(ROOT_ID).state == ConcordTypes.RootState.ACTIVE, "root not active"); + + VM.warp(uint256(roundExpiry) + 1); + providerA.expireChild(facility, childA); + + require(facility.getChild(childA).state == ConcordTypes.ChildState.EXPIRED, "child not expired"); + require(facility.getChild(childA).committedCapacity == 0, "expired child commitment remains"); + require(facility.getRoot(ROOT_ID).committedCapacity == 400, "root commitment not reduced"); + require(facility.getRoot(ROOT_ID).state == ConcordTypes.RootState.FUNDING, "active root was not demoted"); + } + function allocationDigest(ConcordTypes.AllocationResult memory result) internal pure returns (bytes32) { bytes memory encoded = abi.encodePacked( result.extensionId, diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index 342e734..10c6275 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -20,6 +20,12 @@ Updated 2026-08-12. no embedded private-key fallback. - Coston2 asset resolution is centralized in config/networks/coston2.json and validated by scripts/resolve-coston2-assets.sh. +- REST/OpenAPI, the TypeScript SDK, MCP, and CLI share unsigned lifecycle intent + vocabulary for round opening and relationship close/expiry; allocation + materialization remains verifier-gated. +- `scripts/coston2-preflight.sh` provides a read-only offline/live operator gate, + and the adversarial cases are recorded in + `docs/experiments/coston2-failure-matrix.md`. ## Not yet evidenced diff --git a/docs/README.md b/docs/README.md index 5073a35..c61e957 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,6 +29,8 @@ record before making a deployment or privacy claim. - [cli.md](cli.md) — unified CLI, API, and MCP entry points. - [pre-deployment-hardening.md](pre-deployment-hardening.md) — immediate CI, correctness, and productization gates before live proof. +- [experiments/coston2-failure-matrix.md](experiments/coston2-failure-matrix.md) + — adversarial contract, FCC, control-plane, and live-evidence cases. ## Evidence and truth diff --git a/docs/cli.md b/docs/cli.md index c5d1338..20c2252 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -31,3 +31,19 @@ existing bounded FCC/deployment commands, does not hold a signer, and does not claim that Concord contracts or an FCC extension are deployed on Coston2. The live operator path still requires deployed addresses, funded accounts, a configured FCC proxy/TEE flow, provider offers, and recorded receipts. + +## Coston2 preflight + +Run the read-only preflight before any credentialed operator session: + +~~~bash +./scripts/coston2-preflight.sh --offline +./scripts/coston2-preflight.sh --proxy "$EXT_PROXY_URL" --extension-id "$CONCORD_EXTENSION_ID" +~~~ + +The live form confirms chain ID 114, re-runs the centralized FXRP/USDT0 asset +resolver, and can check public bytecode and the official FCC proxy `/info` +binding when addresses are supplied. It never signs, broadcasts, or stores +credentials. The transaction-intent vocabulary shared by the API, SDK, MCP, +and CLI includes round opening plus child/root close and expiry actions; FCC +allocation materialization remains verifier-gated. \ No newline at end of file diff --git a/docs/coston2-live-runbook.md b/docs/coston2-live-runbook.md index 921e301..ded4992 100644 --- a/docs/coston2-live-runbook.md +++ b/docs/coston2-live-runbook.md @@ -32,7 +32,32 @@ ignored environment files or the deployment system secret store. ## Execution order -### 1. Resolve current Coston2 assets +### 1. Run the read-only preflight + +Validate the local config before using any funded account. In offline mode the +command makes no RPC request: + +~~~bash +./scripts/coston2-preflight.sh --offline +~~~ + +After the public addresses and FCC proxy are available, repeat it with live +bindings: + +~~~bash +./scripts/coston2-preflight.sh \ + --proxy "$EXT_PROXY_URL" \ + --extension-id "$CONCORD_EXTENSION_ID" \ + --facility "$CAPITAL_FACILITY" \ + --registry "$ACCORD_REGISTRY" \ + --sender "$CONCORD_INSTRUCTION_SENDER" +~~~ + +It confirms Coston2 chain ID 114, re-runs the current asset resolver, and +optionally checks public bytecode and the proxy `/info` extension binding. It +never signs or broadcasts. If `SIMULATED_TEE=true`, the output is development +path evidence only. +### 2. Resolve current Coston2 assets Run the resolver before deployment and stop on any mismatch. It obtains the FXRP manager through Flare's ContractRegistry, resolves `fAsset()`, and validates the @@ -46,7 +71,7 @@ The GitHub workflow named `Coston2 asset resolution` is the same read-only gate. A successful run may be cited for asset resolution, but it is not deployment or FCC evidence. -### 2. Configure the official FCE scaffold +### 3. Configure the official FCE scaffold Use the current official scaffold sequence and set the Coston2 values explicitly: @@ -69,7 +94,7 @@ curl -s "$EXT_PROXY_URL/info" | jq '{extensionId, codeHash, platform}' If `platform` is the simulated development value or the code hash is documented as simulated, preserve that limitation in every demo and release note. -### 3. Deploy and register in two phases +### 4. Deploy and register in two phases Deploy the instruction sender first. Register the extension through the official scaffold, call the one-shot `setExtensionId()`, and record the resulting live @@ -91,7 +116,7 @@ DEPLOY_PHASE=facility ./scripts/deploy-coston2.sh Do not fill registry addresses or extension ids from memory. Resolve them from the current official scaffold/configuration and record the source used. -### 4. Run the relationship lifecycle +### 5. Run the relationship lifecycle 1. Treasury creates the FXRP-backed root Accord and locks FXRP. 2. Treasury opens the Makkari syndication round with target, fee, expiry, and @@ -113,7 +138,7 @@ the current official scaffold/configuration and record the source used. 11. Repay through the actual USDT0 transfer, then verify child and root exposure decrease and available capacity returns. -### 5. Verify and record evidence +### 6. Verify and record evidence Use the committed template as the shape of the final evidence packet: diff --git a/docs/experiments/coston2-failure-matrix.md b/docs/experiments/coston2-failure-matrix.md new file mode 100644 index 0000000..be72b80 --- /dev/null +++ b/docs/experiments/coston2-failure-matrix.md @@ -0,0 +1,100 @@ +# Coston2 and lifecycle failure matrix + +Status: active pre-deployment experiment plan + +This matrix applies Experiment Foundry discipline to Concord's MVP. It tests +whether every state-changing path either rejects an invalid action or waits for +an authoritative onchain/FCC observation before describing the action as +successful. It does not expand Concord beyond the FXRP-backed syndicated +facility vertical slice. + +## Hypothesis + +Concord's contract, FCC extension, verifier, API, SDK, CLI, and MCP boundaries +preserve the same relationship semantics under malformed input, replay, expiry, +partial funding, insufficient capacity, and missing live evidence. No client +surface can turn an unsigned intent, local test actor, or simulated development +result into a committed capital, settlement, or production-confidentiality +claim. + +## Automated cases + +| Area | Failure or invariant case | Expected observation | +|---|---|---| +| FCC | invalid provider signature | quote rejected; no allocation slot | +| FCC | expired quote | quote rejected deterministically | +| FCC | fee above round policy | quote rejected deterministically | +| FCC | malformed action payload | strict decode failure | +| FCC | duplicate provider or nonce ambiguity | explicit failure or deterministic rejection | +| CoFill | equal fees and provider tie | same sorted result for repeated input | +| CoFill | partial final provider allocation | allocated capacity stops exactly at target | +| CoFill | insufficient eligible capacity | explicit failure; no partial success | +| Verifier | wrong root, round, extension, digest, or TEE binding | verification fails before materialization | +| Contract | repeated allocation result | replay rejected | +| Contract | selected child before provider transfer | root committed capacity remains unchanged | +| Contract | provider overfunds selected allocation | transfer rejected | +| Contract | draw over available capacity | draw rejected; exposure unchanged | +| Contract | draw across two children | explicit DrawLegs sum to draw principal | +| Contract | partial repayment | draw-leg, child, and root exposure decrease together | +| Contract | active child expires with commitment below target | root demotes from ACTIVE to FUNDING | +| Contract | provider withdrawal while exposure exists | withdrawal rejected | +| Contract | collateral recovery while exposure exists | recovery rejected | +| Control plane | agent/API/SDK/MCP prepare action | unsigned intent only; explicit approval remains true | +| Control plane | no observed read model | response is not_observed; no success claim | + +## Commands + +Run the deterministic suites from a clean checkout: + +~~~bash +forge test -vvv +(cd go && GOTOOLCHAIN=local go test ./...) +(cd tools && GOTOOLCHAIN=local go test ./...) +(cd sdk/typescript && npm install --ignore-scripts --no-audit --no-fund && npm run build) +./scripts/coston2-preflight.sh --offline +~~~ + +The shell and documentation gates also run in GitHub Actions. The preflight +offline mode validates the centralized Coston2 configuration without reaching +the network. + +## Live-gated cases + +These cases require current Coston2 credentials and a reachable official Flare +FCC scaffold/proxy. They are not claimed by local tests: + +- `/info` identifies the intended Concord extension and reports its code hash; +- the extension is registered and an active TEE machine is observed; +- signed `SUBMIT_QUOTE` and `FINALIZE_ROUND` instructions produce a verified + result bound to the intended root, round, extension, and digest; +- at least two selected providers transfer actual Coston2 USDT0; +- the root activates, a multi-child draw settles, and repayment restores + capacity; +- explorer receipts make the full relationship lineage queryable. + +## Evidence handling + +Record only observed values in the evidence packet: transaction hashes, +instruction IDs, result digest, extension ID, machine identity, asset +resolution, state transitions, and explorer links. Keep private quote contents, +provider constraints, signing keys, proxy credentials, and decrypted FCC +inputs out of GitHub, logs, fixtures, and screenshots. + +A simulated TEE may be used for development-path evidence when the current Flare +scaffold requires it. It must be labeled simulated development execution, not +production hardware-backed confidential execution. No test in this matrix +proves private FXRP transfers, private USDT0 transfers, or private EVM +settlement. + +## Decision rule + +The implementation may advance from this phase only when the automated cases +pass and the live-gated cases have real Coston2 artifacts. A green unit test, +frontend render, or mock FCC response is not sufficient to declare Concord's +vertical slice complete. + +## Related records + +- [Coston2 live runbook](../coston2-live-runbook.md) +- [Pre-deployment hardening](../pre-deployment-hardening.md) +- [Concord status](../CONCORD_STATUS.md) \ No newline at end of file diff --git a/docs/pre-deployment-hardening.md b/docs/pre-deployment-hardening.md index f2d8d4a..8168837 100644 --- a/docs/pre-deployment-hardening.md +++ b/docs/pre-deployment-hardening.md @@ -36,13 +36,15 @@ remain distinct from confidential provider inputs and FCC evidence. baseline. - The Coston2 asset resolver is available as a manual workflow because it is a live RPC check, not a deterministic unit test. +- `scripts/coston2-preflight.sh` validates the centralized Coston2 config offline + and can re-check RPC chain ID, public bytecode, and FCC proxy `/info` live. ### Contract and extension correctness - Solidity tests cover Root Accord creation, FXRP collateral, verified and replay-protected allocation materialization, selected versus funded child - capacity, multi-child draws, DrawLegs, repayment, exposure, closure, and - balance recovery. + capacity, multi-child draws, DrawLegs, repayment, exposure, closure, expiry, + active-child demotion, and balance recovery. - Go tests cover strict payload decoding, provider signature validation, expiry and policy rejection, deterministic ordering, partial allocation, and explicit insufficient-capacity failure. diff --git a/docs/testing.md b/docs/testing.md index c3c87dc..af6f145 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -51,3 +51,11 @@ scaffold, provider accounts, and an independently verified allocation result. Passing local tests do not equal a Coston2 deployment. Record contract addresses, instruction IDs, transaction hashes, result digests and explorer links in the status record before making a live-flow claim. + +## Failure and evidence matrix + +[Coston2 and lifecycle failure matrix](experiments/coston2-failure-matrix.md) +covers adversarial signatures, expiry, replay, insufficient capacity, selected +versus funded state, active-child expiry demotion, partial repayment, unsigned +intent boundaries, and live-gated FCC/settlement evidence. The matrix is a +release gate, not a claim that the live cases have already been observed. \ No newline at end of file diff --git a/scripts/check-docs.sh b/scripts/check-docs.sh index c478579..7c8ab02 100755 --- a/scripts/check-docs.sh +++ b/scripts/check-docs.sh @@ -12,7 +12,7 @@ warn() { echo -e "${YELLOW}WARN${NC} $*"; } ok() { echo -e "${GREEN}ok${NC} $*"; } # These are the Concord documents that must exist in a clean checkout. -REQUIRED="architecture.md getting-started.md testing.md testing-against-coston2.md cloudflared.md CONCORD_STATUS.md shared-product-contract.md frontend-map.md cli.md coston2-live-runbook.md" +REQUIRED="architecture.md getting-started.md testing.md testing-against-coston2.md cloudflared.md CONCORD_STATUS.md shared-product-contract.md frontend-map.md cli.md coston2-live-runbook.md experiments/coston2-failure-matrix.md" MAX_LINES=500 echo "docs: $DOCS" @@ -54,6 +54,7 @@ check "shared-product-contract.md" "selected versus funded" "selected" check "shared-product-contract.md" "unsigned intent boundary" "requiresExplicitApproval" check "frontend-map.md" "not-observed state" "not-observed" check "coston2-live-runbook.md" "credential gate" "not deployment evidence" +check "experiments/coston2-failure-matrix.md" "failure matrix hypothesis" "Hypothesis" if [[ -f "$DOCS/README.md" ]]; then ok "docs/README.md index present" diff --git a/scripts/coston2-preflight.sh b/scripts/coston2-preflight.sh new file mode 100755 index 0000000..7507b3d --- /dev/null +++ b/scripts/coston2-preflight.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# Read-only Coston2 preflight for the Concord operator path. +# It validates configuration and public runtime bindings; it never signs or broadcasts. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +CONFIG_FILE="${CONCORD_NETWORK_CONFIG:-$PROJECT_DIR/config/networks/coston2.json}" +OFFLINE=false +PROXY_URL="${EXT_PROXY_URL:-}" +EXPECTED_EXTENSION_ID="${CONCORD_EXTENSION_ID:-}" +FACILITY_ADDRESS="${CAPITAL_FACILITY:-}" +REGISTRY_ADDRESS="${ACCORD_REGISTRY:-}" +SENDER_ADDRESS="${CONCORD_INSTRUCTION_SENDER:-}" + +usage() { + cat <<'EOF' +Usage: scripts/coston2-preflight.sh [options] + +Read-only validation for the Coston2 Concord deployment and FCC operator path. + +Options: + --offline validate local Coston2 configuration without RPC access + --proxy URL validate the official FCC proxy /info response + --extension-id ID require /info to report this extension id + --facility ADDRESS require deployed CapitalFacility bytecode + --registry ADDRESS require deployed AccordRegistry bytecode + --sender ADDRESS require deployed ConcordInstructionSender bytecode + -h, --help show this help + +Environment overrides: + CONCORD_NETWORK_CONFIG, CHAIN_URL, EXT_PROXY_URL, CONCORD_EXTENSION_ID, + CAPITAL_FACILITY, ACCORD_REGISTRY, CONCORD_INSTRUCTION_SENDER, SIMULATED_TEE +EOF +} + +die() { echo "coston2-preflight: $*" >&2; exit 1; } +info() { echo "coston2-preflight: $*"; } + +while (($#)); do + case "$1" in + --offline) OFFLINE=true ;; + --proxy) shift; (($#)) || die "--proxy requires a URL"; PROXY_URL="$1" ;; + --extension-id) shift; (($#)) || die "--extension-id requires an id"; EXPECTED_EXTENSION_ID="$1" ;; + --facility) shift; (($#)) || die "--facility requires an address"; FACILITY_ADDRESS="$1" ;; + --registry) shift; (($#)) || die "--registry requires an address"; REGISTRY_ADDRESS="$1" ;; + --sender) shift; (($#)) || die "--sender requires an address"; SENDER_ADDRESS="$1" ;; + -h|--help) usage; exit 0 ;; + *) die "unknown argument: $1" ;; + esac + shift +done + +command -v jq >/dev/null 2>&1 || die "jq is required" +[[ -f "$CONFIG_FILE" ]] || die "network config not found: $CONFIG_FILE" + +NETWORK="$(jq -r '.network // empty' "$CONFIG_FILE")" +CHAIN_ID="$(jq -r '.chainId // empty' "$CONFIG_FILE")" +RPC_URL="${CHAIN_URL:-$(jq -r '.rpcUrl // empty' "$CONFIG_FILE")}" +CONTRACT_REGISTRY="$(jq -r '.contractRegistry // empty' "$CONFIG_FILE")" +FXRP_MANAGER="$(jq -r '.fxrp.assetManager // empty' "$CONFIG_FILE")" +FXRP_TOKEN="$(jq -r '.fxrp.token // empty' "$CONFIG_FILE")" +USDT0_TOKEN="$(jq -r '.usdt0.token // empty' "$CONFIG_FILE")" + +[[ "$NETWORK" == "coston2" ]] || die "network config must identify coston2, got '$NETWORK'" +[[ "$CHAIN_ID" == "114" ]] || die "Coston2 chain ID must be 114, got '$CHAIN_ID'" +[[ -n "$RPC_URL" && "$RPC_URL" != "null" ]] || die "RPC URL is missing" + +is_address() { [[ "$1" =~ ^0x[0-9a-fA-F]{40}$ ]]; } +for pair in "contractRegistry:$CONTRACT_REGISTRY" "fxrpManager:$FXRP_MANAGER" "fxrpToken:$FXRP_TOKEN" "usdt0Token:$USDT0_TOKEN"; do + label="${pair%%:*}" + address="${pair#*:}" + is_address "$address" || die "$label is not a 20-byte hex address: $address" +done + +info "configuration is valid: network=$NETWORK chainId=$CHAIN_ID" +info "RPC: $RPC_URL" +info "FXRP manager and token are centralized in the network config; live resolver will re-check both" +info "USDT0 snapshot is centralized in the network config; live resolver will re-check bytecode, symbol, and decimals" + +if [[ "$OFFLINE" == true ]]; then + info "offline mode: no RPC, proxy, bytecode, signing, or broadcast checks were attempted" + exit 0 +fi + +command -v cast >/dev/null 2>&1 || die "cast (Foundry) is required for live checks" +command -v curl >/dev/null 2>&1 || die "curl is required for live checks" + +RPC_CHAIN_ID="$(cast chain-id --rpc-url "$RPC_URL" | tr -d '[:space:]')" +[[ "$RPC_CHAIN_ID" == "114" ]] || die "RPC reported chain ID '$RPC_CHAIN_ID', expected 114" +info "RPC chain ID confirmed: 114" + +"$SCRIPT_DIR/resolve-coston2-assets.sh" +check_code() { + local label="$1" + local address="$2" + is_address "$address" || die "$label is not a 20-byte hex address: $address" + local code + code="$(cast code "$address" --rpc-url "$RPC_URL")" + [[ "$code" != "0x" ]] || die "$label has no deployed bytecode at $address" + info "$label bytecode present at $address" +} + +[[ -z "$FACILITY_ADDRESS" ]] || check_code "CapitalFacility" "$FACILITY_ADDRESS" +[[ -z "$REGISTRY_ADDRESS" ]] || check_code "AccordRegistry" "$REGISTRY_ADDRESS" +[[ -z "$SENDER_ADDRESS" ]] || check_code "ConcordInstructionSender" "$SENDER_ADDRESS" + +if [[ -n "$PROXY_URL" ]]; then + INFO_JSON="$(curl --fail --silent --show-error --max-time 20 "$PROXY_URL/info")" || die "FCC proxy /info request failed" + jq -e 'type == "object"' >/dev/null <<<"$INFO_JSON" || die "FCC proxy /info did not return a JSON object" + PROXY_EXTENSION_ID="$(jq -r '.extensionId // empty' <<<"$INFO_JSON")" + PROXY_PLATFORM="$(jq -r '.platform // "unknown"' <<<"$INFO_JSON")" + PROXY_CODE_HASH="$(jq -r '.codeHash // "unknown"' <<<"$INFO_JSON")" + [[ -n "$PROXY_EXTENSION_ID" ]] || die "FCC proxy /info omitted extensionId" + if [[ -n "$EXPECTED_EXTENSION_ID" && "$PROXY_EXTENSION_ID" != "$EXPECTED_EXTENSION_ID" ]]; then + die "FCC proxy extensionId '$PROXY_EXTENSION_ID' does not match expected '$EXPECTED_EXTENSION_ID'" + fi + info "FCC proxy /info confirmed: extensionId=$PROXY_EXTENSION_ID platform=$PROXY_PLATFORM codeHash=$PROXY_CODE_HASH" + if [[ "${SIMULATED_TEE:-false}" == true ]]; then + info "SIMULATED_TEE=true: this is development-path evidence, not production hardware-backed TEE evidence" + fi +else + info "no FCC proxy supplied: skipped /info binding check" +fi + +info "preflight passed; no transaction was signed or broadcast" \ No newline at end of file diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index 04e1f6b..c9cb63f 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -179,19 +179,28 @@ export interface Health { export type PreparedAction = | "create_root" | "lock_collateral" + | "open_syndication" | "approve_asset" | "fund_child" + | "close_child" + | "expire_child" | "draw" - | "repay"; + | "repay" + | "close_root" + | "expire_root"; export interface PrepareTransactionRequest { action: PreparedAction; rootAccordId?: string; childAccordId?: string; drawId?: string; + roundId?: string; targetCapacity?: string; amount?: string; validUntilUnix?: string; + roundExpiryUnix?: string; + maxFeeBps?: string; + eligibleProviders?: string[]; policyHash?: string; asset?: string; spender?: string; diff --git a/tools/pkg/api/server.go b/tools/pkg/api/server.go index b1b83cd..f1ed544 100644 --- a/tools/pkg/api/server.go +++ b/tools/pkg/api/server.go @@ -26,23 +26,32 @@ type Server struct { } type PrepareRequest struct { - Action string `json:"action"` - RootAccordID string `json:"rootAccordId,omitempty"` - ChildAccordID string `json:"childAccordId,omitempty"` - DrawID string `json:"drawId,omitempty"` - TargetCapacity string `json:"targetCapacity,omitempty"` - Amount string `json:"amount,omitempty"` - ValidUntilUnix string `json:"validUntilUnix,omitempty"` - PolicyHash string `json:"policyHash,omitempty"` - Asset string `json:"asset,omitempty"` - Spender string `json:"spender,omitempty"` - Actor string `json:"actor,omitempty"` + Action string `json:"action"` + RootAccordID string `json:"rootAccordId,omitempty"` + ChildAccordID string `json:"childAccordId,omitempty"` + DrawID string `json:"drawId,omitempty"` + RoundID string `json:"roundId,omitempty"` + TargetCapacity string `json:"targetCapacity,omitempty"` + Amount string `json:"amount,omitempty"` + ValidUntilUnix string `json:"validUntilUnix,omitempty"` + RoundExpiryUnix string `json:"roundExpiryUnix,omitempty"` + MaxFeeBPS string `json:"maxFeeBps,omitempty"` + EligibleProviders []string `json:"eligibleProviders,omitempty"` + PolicyHash string `json:"policyHash,omitempty"` + Asset string `json:"asset,omitempty"` + Spender string `json:"spender,omitempty"` + Actor string `json:"actor,omitempty"` } const prepareABIJSON = `[ {"inputs":[{"name":"rootId","type":"bytes32"},{"name":"targetCapacity","type":"uint256"},{"name":"validUntil","type":"uint64"},{"name":"policyHash","type":"bytes32"}],"name":"createRootAccord","outputs":[],"stateMutability":"nonpayable","type":"function"}, {"inputs":[{"name":"rootId","type":"bytes32"},{"name":"amount","type":"uint256"}],"name":"lockCollateral","outputs":[],"stateMutability":"nonpayable","type":"function"}, + {"inputs":[{"name":"rootId","type":"bytes32"},{"name":"roundId","type":"bytes32"},{"name":"maxFeeBps","type":"uint32"},{"name":"roundExpiry","type":"uint64"},{"name":"eligibleProviders","type":"address[]"}],"name":"openSyndication","outputs":[],"stateMutability":"nonpayable","type":"function"}, {"inputs":[{"name":"childId","type":"bytes32"},{"name":"amount","type":"uint256"}],"name":"fundChild","outputs":[],"stateMutability":"nonpayable","type":"function"}, + {"inputs":[{"name":"childId","type":"bytes32"}],"name":"closeChild","outputs":[],"stateMutability":"nonpayable","type":"function"}, + {"inputs":[{"name":"childId","type":"bytes32"}],"name":"expireChild","outputs":[],"stateMutability":"nonpayable","type":"function"}, + {"inputs":[{"name":"rootId","type":"bytes32"}],"name":"closeRoot","outputs":[],"stateMutability":"nonpayable","type":"function"}, + {"inputs":[{"name":"rootId","type":"bytes32"}],"name":"expireRoot","outputs":[],"stateMutability":"nonpayable","type":"function"}, {"inputs":[{"name":"drawId","type":"bytes32"},{"name":"rootId","type":"bytes32"},{"name":"amount","type":"uint256"}],"name":"draw","outputs":[],"stateMutability":"nonpayable","type":"function"}, {"inputs":[{"name":"drawId","type":"bytes32"},{"name":"amount","type":"uint256"}],"name":"repay","outputs":[],"stateMutability":"nonpayable","type":"function"} ]` @@ -194,9 +203,14 @@ func (s *Server) buildIntent(ctx context.Context, req PrepareRequest) (readmodel var to common.Address var summary string preconditions := []string{"The connected wallet or institution signer must review and approve this unsigned intent."} - amountValue, err := parseBig(req.Amount, "amount") - if req.Action != "create_root" && err != nil { - return readmodel.TransactionIntent{}, err + var amountValue *big.Int + var err error + switch req.Action { + case "lock_collateral", "approve_asset", "fund_child", "draw", "repay": + amountValue, err = parseBig(req.Amount, "amount") + if err != nil { + return readmodel.TransactionIntent{}, err + } } switch req.Action { case "create_root": @@ -216,9 +230,57 @@ func (s *Server) buildIntent(ctx context.Context, req PrepareRequest) (readmodel if err != nil { return readmodel.TransactionIntent{}, err } + if validUntil <= uint64(time.Now().Unix()) { + return readmodel.TransactionIntent{}, fmt.Errorf("validUntilUnix must be in the future") + } data, err = s.FacilityABI.Pack("createRootAccord", rootID, target, validUntil, policyHash) summary = "Create a new Root Accord with the supplied target, expiry, and policy commitment." preconditions = append(preconditions, "The root Accord ID must not already exist.", "The expiry must be in the future.") + case "open_syndication": + rootID, err := parseBytes32(req.RootAccordID) + if err != nil { + return readmodel.TransactionIntent{}, err + } + roundID, err := parseBytes32(req.RoundID) + if err != nil { + return readmodel.TransactionIntent{}, err + } + if roundID == ([32]byte{}) { + return readmodel.TransactionIntent{}, fmt.Errorf("roundId must not be zero") + } + maxFeeBPS, err := parseUint32(req.MaxFeeBPS, "maxFeeBps") + if err != nil { + return readmodel.TransactionIntent{}, err + } + if maxFeeBPS > 10000 { + return readmodel.TransactionIntent{}, fmt.Errorf("maxFeeBps must not exceed 10000") + } + roundExpiry, err := parseUint64(req.RoundExpiryUnix, "roundExpiryUnix") + if err != nil { + return readmodel.TransactionIntent{}, err + } + if roundExpiry <= uint64(time.Now().Unix()) { + return readmodel.TransactionIntent{}, fmt.Errorf("roundExpiryUnix must be in the future") + } + if len(req.EligibleProviders) == 0 { + return readmodel.TransactionIntent{}, fmt.Errorf("eligibleProviders must contain at least one provider") + } + providers := make([]common.Address, len(req.EligibleProviders)) + seenProviders := make(map[common.Address]struct{}, len(req.EligibleProviders)) + for i, raw := range req.EligibleProviders { + provider, parseErr := parseAddress(raw, fmt.Sprintf("eligibleProviders[%d]", i)) + if parseErr != nil { + return readmodel.TransactionIntent{}, parseErr + } + if _, exists := seenProviders[provider]; exists { + return readmodel.TransactionIntent{}, fmt.Errorf("eligibleProviders contains duplicate address %s", provider.Hex()) + } + seenProviders[provider] = struct{}{} + providers[i] = provider + } + data, err = s.FacilityABI.Pack("openSyndication", rootID, roundID, maxFeeBPS, roundExpiry, providers) + summary = "Open a Makkari syndication round under the Root Accord." + preconditions = append(preconditions, "The signer must be the Root Accord borrower.", "The Root Accord must be PROPOSED with FXRP collateral locked.", "Private quote payloads remain withheld from this public transaction intent.") case "lock_collateral": rootID, err := parseBytes32(req.RootAccordID) if err != nil { @@ -290,6 +352,38 @@ func (s *Server) buildIntent(ctx context.Context, req PrepareRequest) (readmodel data, err = s.FacilityABI.Pack("repay", drawID, amountValue) summary = "Repay principal on the Root Accord draw." preconditions = append(preconditions, "The signer must be the treasury borrower.", "The liquidity-token allowance and balance must cover the amount.", "Capacity returns only after the ERC-20 transfer succeeds.") + case "close_child": + childID, err := parseBytes32(req.ChildAccordID) + if err != nil { + return readmodel.TransactionIntent{}, err + } + data, err = s.FacilityABI.Pack("closeChild", childID) + summary = "Close a Child Accord after its exposure is cleared." + preconditions = append(preconditions, "The signer must be authorized by the Child Accord.", "The child must have no outstanding exposure.") + case "expire_child": + childID, err := parseBytes32(req.ChildAccordID) + if err != nil { + return readmodel.TransactionIntent{}, err + } + data, err = s.FacilityABI.Pack("expireChild", childID) + summary = "Expire an eligible Child Accord after its validity boundary." + preconditions = append(preconditions, "The child must be past its validity boundary and eligible under onchain expiry rules.") + case "close_root": + rootID, err := parseBytes32(req.RootAccordID) + if err != nil { + return readmodel.TransactionIntent{}, err + } + data, err = s.FacilityABI.Pack("closeRoot", rootID) + summary = "Close the Root Accord after the facility is fully settled." + preconditions = append(preconditions, "The signer must be the Root Accord borrower.", "The facility must have no outstanding exposure and no active child obligations.") + case "expire_root": + rootID, err := parseBytes32(req.RootAccordID) + if err != nil { + return readmodel.TransactionIntent{}, err + } + data, err = s.FacilityABI.Pack("expireRoot", rootID) + summary = "Expire the Root Accord after its validity boundary." + preconditions = append(preconditions, "The Root Accord must be past its validity boundary and eligible under onchain expiry rules.") default: return readmodel.TransactionIntent{}, fmt.Errorf("unsupported action %q", req.Action) } @@ -371,13 +465,21 @@ func parseBig(value, name string) (*big.Int, error) { } func parseUint64(value, name string) (uint64, error) { - parsed, err := new(big.Int).SetString(value, 10) - if !err || parsed.Sign() < 0 || !parsed.IsUint64() { + parsed, ok := new(big.Int).SetString(value, 10) + if !ok || parsed.Sign() < 0 || !parsed.IsUint64() { return 0, fmt.Errorf("%s must be a non-negative uint64 in base-10 notation", name) } return parsed.Uint64(), nil } +func parseUint32(value, name string) (uint32, error) { + parsed, err := parseUint64(value, name) + if err != nil || parsed > uint64(^uint32(0)) { + return 0, fmt.Errorf("%s must be a non-negative uint32 in base-10 notation", name) + } + return uint32(parsed), nil +} + func splitPath(path string) []string { trimmed := strings.Trim(path, "/") if trimmed == "" { diff --git a/tools/pkg/api/server_test.go b/tools/pkg/api/server_test.go index 618ce7b..8ca1aff 100644 --- a/tools/pkg/api/server_test.go +++ b/tools/pkg/api/server_test.go @@ -121,3 +121,55 @@ func TestPrepareDrawReturnsUnsignedIntent(t *testing.T) { t.Fatalf("unexpected intent: %+v", bodyResponse.Data) } } + + +func TestPrepareOpenSyndicationReturnsUnsignedIntent(t *testing.T) { + server := testServer(t) + defer server.Close() + body := `{"action":"open_syndication","rootAccordId":"0x` + strings.Repeat("1", 64) + `","roundId":"0x` + strings.Repeat("2", 64) + `","maxFeeBps":"700","roundExpiryUnix":"4102444800","eligibleProviders":["0x` + strings.Repeat("7", 40) + `","0x` + strings.Repeat("8", 40) + `"],"actor":"treasury"}` + response, err := http.Post(server.URL+"/v1/transactions/prepare", "application/json", strings.NewReader(body)) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + data, _ := io.ReadAll(response.Body) + t.Fatalf("status = %d body = %s", response.StatusCode, data) + } + var bodyResponse struct { + Data readmodel.TransactionIntent `json:"data"` + } + if err := json.NewDecoder(response.Body).Decode(&bodyResponse); err != nil { + t.Fatal(err) + } + if bodyResponse.Data.Action != "open_syndication" || !bodyResponse.Data.RequiresExplicitApproval || !strings.HasPrefix(bodyResponse.Data.Data, "0x") { + t.Fatalf("unexpected intent: %+v", bodyResponse.Data) + } + if !strings.Contains(strings.Join(bodyResponse.Data.Preconditions, " "), "Private quote payloads remain withheld") { + t.Fatalf("missing privacy boundary: %+v", bodyResponse.Data.Preconditions) + } +} + +func TestPrepareCloseRootDoesNotRequireAmount(t *testing.T) { + server := testServer(t) + defer server.Close() + body := `{"action":"close_root","rootAccordId":"0x` + strings.Repeat("1", 64) + `","actor":"treasury"}` + response, err := http.Post(server.URL+"/v1/transactions/prepare", "application/json", strings.NewReader(body)) + if err != nil { + t.Fatal(err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + data, _ := io.ReadAll(response.Body) + t.Fatalf("status = %d body = %s", response.StatusCode, data) + } + var bodyResponse struct { + Data readmodel.TransactionIntent `json:"data"` + } + if err := json.NewDecoder(response.Body).Decode(&bodyResponse); err != nil { + t.Fatal(err) + } + if bodyResponse.Data.Action != "close_root" || !bodyResponse.Data.RequiresExplicitApproval { + t.Fatalf("unexpected intent: %+v", bodyResponse.Data) + } +} diff --git a/tools/pkg/mcpserver/server.go b/tools/pkg/mcpserver/server.go index e85f3f6..8b7961d 100644 --- a/tools/pkg/mcpserver/server.go +++ b/tools/pkg/mcpserver/server.go @@ -169,9 +169,10 @@ func tools() []map[string]any { {"name": "get_round", "description": "Read one public Makkari round summary without private quote data.", "inputSchema": objectSchema("roundId")}, {"name": "get_draw", "description": "Read one draw and its explicit child DrawLegs.", "inputSchema": objectSchema("drawId")}, {"name": "prepare_transaction", "description": "Prepare unsigned calldata for a reviewed action. This tool never signs or broadcasts.", "inputSchema": map[string]any{"type": "object", "required": []string{"action"}, "properties": map[string]any{ - "action": map[string]any{"type": "string", "enum": []string{"create_root", "lock_collateral", "approve_asset", "fund_child", "draw", "repay"}}, - "rootAccordId": map[string]string{"type": "string"}, "childAccordId": map[string]string{"type": "string"}, "drawId": map[string]string{"type": "string"}, - "targetCapacity": map[string]string{"type": "string"}, "amount": map[string]string{"type": "string"}, "validUntilUnix": map[string]string{"type": "string"}, + "action": map[string]any{"type": "string", "enum": []string{"create_root", "lock_collateral", "open_syndication", "approve_asset", "fund_child", "close_child", "expire_child", "draw", "repay", "close_root", "expire_root"}}, + "rootAccordId": map[string]string{"type": "string"}, "childAccordId": map[string]string{"type": "string"}, "drawId": map[string]string{"type": "string"}, "roundId": map[string]string{"type": "string"}, + "targetCapacity": map[string]string{"type": "string"}, "amount": map[string]string{"type": "string"}, "validUntilUnix": map[string]string{"type": "string"}, "roundExpiryUnix": map[string]string{"type": "string"}, "maxFeeBps": map[string]string{"type": "string"}, + "eligibleProviders": map[string]any{"type": "array", "items": map[string]string{"type": "string"}}, "policyHash": map[string]string{"type": "string"}, "asset": map[string]string{"type": "string"}, "spender": map[string]string{"type": "string"}, "actor": map[string]any{"type": "string", "enum": []string{"treasury", "provider", "institution", "agent"}}, }}}, From 7701f625615fc407b949e79fe3a24338ee07d8e3 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 23:18:26 +0100 Subject: [PATCH 028/107] docs: record hardening verification evidence --- docs/CONCORD_STATUS.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index 10c6275..d4e985d 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -96,3 +96,11 @@ Explorer links: - Green CI confirms repository-level correctness checks only. It does not prove deployed contracts, FCC registration, funded provider accounts, settlement, repayment, or production hardware-backed confidential execution. + +- The hardening commit `35b756b38a6b97b7f63dc08074dd1c4ff8a52ccc` passed the full + [push verification run](https://github.com/etvjay/Concord/actions/runs/31646160590) + and the equivalent [PR verification run](https://github.com/etvjay/Concord/actions/runs/31646164208), + including lifecycle expiry demotion, partial repayment, API/SDK/MCP intent + parity, and the read-only preflight gate. +- The same commit passed the read-only + [Coston2 asset-resolution run](https://github.com/etvjay/Concord/actions/runs/31646160617). From 5500bee69943338f8a02e7ac189f1852a21b9201 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 23:33:57 +0100 Subject: [PATCH 029/107] test: cover malicious token reentry --- contracts/test/ConcordFlow.t.sol | 58 +++++++++++++++++++++++++++ contracts/test/ReentrantERC20.sol | 66 +++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 contracts/test/ReentrantERC20.sol diff --git a/contracts/test/ConcordFlow.t.sol b/contracts/test/ConcordFlow.t.sol index ac9aab5..7174a18 100644 --- a/contracts/test/ConcordFlow.t.sol +++ b/contracts/test/ConcordFlow.t.sol @@ -5,6 +5,7 @@ import {AccordRegistry} from "../AccordRegistry.sol"; import {CapitalFacility} from "../CapitalFacility.sol"; import {ConcordTypes} from "../ConcordTypes.sol"; import {MockERC20} from "./MockERC20.sol"; +import {ReentrantERC20} from "./ReentrantERC20.sol"; interface Vm { function warp(uint256 timestamp) external; @@ -382,6 +383,63 @@ contract ConcordFlowTest { require(facility.getRoot(ROOT_ID).state == ConcordTypes.RootState.FUNDING, "active root was not demoted"); } + function testMaliciousERC20ReentryIsBlocked() public { + ReentrantERC20 malicious = new ReentrantERC20(); + AccordRegistry guardedRegistry = new AccordRegistry(address(this)); + CapitalFacility guardedFacility = new CapitalFacility( + guardedRegistry, + address(fxrp), + address(malicious), + address(providerC), + EXTENSION_ID + ); + guardedRegistry.setFacility(address(guardedFacility)); + + treasury.approve(fxrp, address(guardedFacility), type(uint256).max); + uint64 rootExpiry = uint64(block.timestamp + 30 days); + uint64 roundExpiry = uint64(block.timestamp + 1 days); + treasury.createRoot(guardedFacility, ROOT_ID, 100, rootExpiry, keccak256("POLICY")); + treasury.lock(guardedFacility, ROOT_ID, 100); + + address[] memory providers = new address[](1); + providers[0] = address(malicious); + treasury.open(guardedFacility, ROOT_ID, ROUND_ID, 700, roundExpiry, providers); + + address[] memory selected = new address[](1); + selected[0] = address(malicious); + uint256[] memory allocations = new uint256[](1); + allocations[0] = 100; + uint32[] memory fees = new uint32[](1); + fees[0] = 700; + bytes32[] memory terms = new bytes32[](1); + terms[0] = keccak256("MALICIOUS-TERMS"); + ConcordTypes.AllocationResult memory result = ConcordTypes.AllocationResult({ + extensionId: EXTENSION_ID, + roundId: ROUND_ID, + rootAccordId: ROOT_ID, + success: true, + selectedProviders: selected, + allocatedCapacity: allocations, + acceptedFeeBps: fees, + termsCommitments: terms, + roundExpiry: roundExpiry, + resultDigest: bytes32(0) + }); + result.resultDigest = allocationDigest(result); + + providerC.mark(guardedFacility, result.resultDigest); + treasury.materialize(guardedFacility, result); + bytes32 childId = keccak256(abi.encode(ROOT_ID, result.resultDigest, address(malicious))); + + malicious.mintSelf(100); + malicious.approveSelf(address(guardedFacility), 100); + malicious.configureReentry(address(guardedFacility), childId); + malicious.attackFund(address(guardedFacility), childId, 100); + + require(malicious.reentryBlocked(), "reentrant callback was not blocked"); + require(guardedFacility.getRoot(ROOT_ID).committedCapacity == 100, "funding was not recorded"); + require(guardedFacility.getRoot(ROOT_ID).state == ConcordTypes.RootState.ACTIVE, "root did not activate"); + } function allocationDigest(ConcordTypes.AllocationResult memory result) internal pure returns (bytes32) { bytes memory encoded = abi.encodePacked( result.extensionId, diff --git a/contracts/test/ReentrantERC20.sol b/contracts/test/ReentrantERC20.sol new file mode 100644 index 0000000..570c41d --- /dev/null +++ b/contracts/test/ReentrantERC20.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +interface IReentrantFacility { + function fundChild(bytes32 childId, uint256 amount) external; +} + +/// @notice Test-only ERC-20 that attempts a callback during transferFrom. +/// The callback is swallowed so the outer token transfer can complete; the +/// test can then assert that CapitalFacility's nonReentrant guard blocked it. +contract ReentrantERC20 { + mapping(address => uint256) public balanceOf; + mapping(address => mapping(address => uint256)) public allowance; + + address public callbackTarget; + bytes32 public callbackChild; + bool public callbackEnabled; + bool public reentryBlocked; + + function mintSelf(uint256 amount) external { + balanceOf[address(this)] += amount; + } + + function approveSelf(address spender, uint256 amount) external { + allowance[address(this)][spender] = amount; + } + + function configureReentry(address target, bytes32 childId) external { + callbackTarget = target; + callbackChild = childId; + callbackEnabled = true; + reentryBlocked = false; + } + + function attackFund(address facility, bytes32 childId, uint256 amount) external { + IReentrantFacility(facility).fundChild(childId, amount); + } + + function transfer(address to, uint256 amount) external returns (bool) { + _move(msg.sender, to, amount); + return true; + } + + function transferFrom(address from, address to, uint256 amount) external returns (bool) { + uint256 allowed = allowance[from][msg.sender]; + require(allowed >= amount, "allowance"); + allowance[from][msg.sender] = allowed - amount; + + if (callbackEnabled && msg.sender == callbackTarget) { + callbackEnabled = false; + (bool ok,) = callbackTarget.call( + abi.encodeWithSelector(IReentrantFacility.fundChild.selector, callbackChild, 1) + ); + reentryBlocked = !ok; + } + + _move(from, to, amount); + return true; + } + + function _move(address from, address to, uint256 amount) internal { + require(balanceOf[from] >= amount, "balance"); + balanceOf[from] -= amount; + balanceOf[to] += amount; + } +} \ No newline at end of file From cf724b1644d4dcfd5d5a17d8a656a239bbcc9a92 Mon Sep 17 00:00:00 2001 From: etvjay Date: Wed, 12 Aug 2026 23:37:47 +0100 Subject: [PATCH 030/107] docs: record ERC-20 safety coverage --- docs/CONCORD_STATUS.md | 4 ++-- docs/experiments/coston2-failure-matrix.md | 1 + docs/pre-deployment-hardening.md | 2 +- docs/testing.md | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index d4e985d..a699239 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -6,8 +6,8 @@ Updated 2026-08-12. - AccordRegistry, CapitalFacility, ConcordInstructionSender and shared Concord types compile with Solidity 0.8.27. -- Foundry flow tests pass locally, including the two-child draw and repayment - lifecycle. +- Foundry flow tests pass locally, including the two-child draw, repayment, + expiry-demotion, and malicious ERC-20 callback lifecycle. - The Go Concord extension compiles and tests signed quotes and deterministic CoFill inside the official FCE scaffold structure, including exact zero-fee round bounds. diff --git a/docs/experiments/coston2-failure-matrix.md b/docs/experiments/coston2-failure-matrix.md index be72b80..860a4f0 100644 --- a/docs/experiments/coston2-failure-matrix.md +++ b/docs/experiments/coston2-failure-matrix.md @@ -33,6 +33,7 @@ claim. | Contract | repeated allocation result | replay rejected | | Contract | selected child before provider transfer | root committed capacity remains unchanged | | Contract | provider overfunds selected allocation | transfer rejected | +| Contract | malicious ERC-20 callback during funding | reentrant action rejected; outer transfer and commitment remain correct | | Contract | draw over available capacity | draw rejected; exposure unchanged | | Contract | draw across two children | explicit DrawLegs sum to draw principal | | Contract | partial repayment | draw-leg, child, and root exposure decrease together | diff --git a/docs/pre-deployment-hardening.md b/docs/pre-deployment-hardening.md index 8168837..710f605 100644 --- a/docs/pre-deployment-hardening.md +++ b/docs/pre-deployment-hardening.md @@ -44,7 +44,7 @@ remain distinct from confidential provider inputs and FCC evidence. - Solidity tests cover Root Accord creation, FXRP collateral, verified and replay-protected allocation materialization, selected versus funded child capacity, multi-child draws, DrawLegs, repayment, exposure, closure, expiry, - active-child demotion, and balance recovery. + active-child demotion, malicious ERC-20 callbacks, and balance recovery. - Go tests cover strict payload decoding, provider signature validation, expiry and policy rejection, deterministic ordering, partial allocation, and explicit insufficient-capacity failure. diff --git a/docs/testing.md b/docs/testing.md index af6f145..4bde431 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -22,7 +22,8 @@ contracts/test/ConcordFlow.t.sol covers: - deterministic draw allocation across at least two children; - explicit DrawLeg and settlement lineage; - exposure reduction after repayment and restored availability; -- provider capital return and collateral return after exposure is clear. +- provider capital return and collateral return after exposure is clear; +- malicious ERC-20 callback attempts are blocked by the facility reentrancy guard while the outer transfer remains correct. ## FCC extension From 2f02c4b4d81fb7fa06b2bf1092fb9dff412a3f9c Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 00:10:58 +0100 Subject: [PATCH 031/107] docs: record fresh Flare FCC source snapshot --- docs/CONCORD_STATUS.md | 4 ++- docs/README.md | 1 + docs/source-snapshot-2026-08-13.yaml | 41 ++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 docs/source-snapshot-2026-08-13.yaml diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index a699239..9150771 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -1,6 +1,8 @@ # Concord status -Updated 2026-08-12. +Updated 2026-08-13. + +- The fresh source snapshot used for this checkpoint is [source-snapshot-2026-08-13.yaml](source-snapshot-2026-08-13.yaml). It records the current official Coston2 FCC development path, including simulated-TEE labeling, public HTTPS proxy exposure, and current indexer-credential requirements. ## Works in the durable checkpoint diff --git a/docs/README.md b/docs/README.md index c61e957..13dd15f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -41,6 +41,7 @@ record before making a deployment or privacy claim. reconciliation of the handoff, addendum, whitepaper, and implementation. - [skills-foundry-pack-ingestion.md](skills-foundry-pack-ingestion.md) — durable record of the supplied Foundry skills pack and validator status. +- [source-snapshot-2026-08-13.yaml](source-snapshot-2026-08-13.yaml) — dated official Flare/FCC source snapshot and live-gate inputs. ## Official Flare runtime references diff --git a/docs/source-snapshot-2026-08-13.yaml b/docs/source-snapshot-2026-08-13.yaml new file mode 100644 index 0000000..a726f01 --- /dev/null +++ b/docs/source-snapshot-2026-08-13.yaml @@ -0,0 +1,41 @@ +snapshot: + captured_at_utc: 2026-08-12T23:08:10Z + freshness: FRESH + environment: Coston2 + chain_id: 114 + tee_mode: simulated + hardware_attestation_claim: false + official_skills: + repository: https://github.com/flare-foundation/flare-ai-skills + revision: 0bd60deb977df97455da720df1aaf765480b8141 + consulted: + - flare-general + - flare-fassets + - flare-fcc + - flare-fdc (boundary only; not implemented in the MVP) + developer_hub: + - https://dev.flare.network/fcc/guides/getting-started + - https://dev.flare.network/network/overview + - https://dev.flare.network/support/faqs + - https://dev.flare.network/fxrp/token-interactions/fxrp-address + official_repositories: + - repository: https://github.com/flare-foundation/fce-extension-scaffold + revision: e3f587949069780084e2ced8a53c9419ed05c250 + finding: current scaffold supports Go, Python, and TypeScript implementations; Concord intentionally uses Go. + verified_facts: + - Coston2 uses chain ID 114 and the configured public RPC. + - The official Coston2 FCC development walkthrough uses LOCAL_MODE=false and SIMULATED_TEE=true. + - The walkthrough requires a public HTTPS extension-proxy URL and Coston2 indexer database credentials for the local proxy. + - The current scaffold path includes pre-build, start-services, post-build, and test phases. + - Flare's FAQ identifies Coston2 as the primary dApp testnet and states that FCC is available there while in development. + contradictions: [] + concord_decisions: + - No product-semantic contradiction was found. + - Simulated TEE evidence remains development-path evidence, not production hardware-backed confidentiality. + - FDC remains an evidence seam and is not added to the MVP. + live_gate_inputs_still_required: + - funded Coston2 deployment/proxy accounts + - indexer credentials or an approved current proxy setup + - public HTTPS EXT_PROXY_URL + - current extension registration and active TEE-machine evidence + - real provider funding, draw, repayment, and explorer receipts From 23b27555871c799654e0fcfa679b2983d22fb2ca Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 05:26:49 +0100 Subject: [PATCH 032/107] ci: add guarded Coston2 sender deployment --- .github/workflows/coston2-sender-deploy.yml | 46 +++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/coston2-sender-deploy.yml diff --git a/.github/workflows/coston2-sender-deploy.yml b/.github/workflows/coston2-sender-deploy.yml new file mode 100644 index 0000000..ad74562 --- /dev/null +++ b/.github/workflows/coston2-sender-deploy.yml @@ -0,0 +1,46 @@ +name: Concord Coston2 sender deployment + +on: + workflow_dispatch: + inputs: + ref: + description: Branch or commit to deploy + required: true + default: agent/concord-rebuild + type: string + +permissions: + contents: read + +jobs: + deploy-sender: + name: Deploy ConcordInstructionSender + runs-on: ubuntu-latest + env: + CHAIN_URL: https://coston2-api.flare.network/ext/C/rpc + DEPLOYMENT_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_DEPLOYER_PRIVATE_KEY }} + steps: + - name: Check out requested ref + uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + + - name: Deploy instruction sender + shell: bash + run: | + set -euo pipefail + test -n "$DEPLOYMENT_PRIVATE_KEY" + owner="$(cast wallet address --private-key "$DEPLOYMENT_PRIVATE_KEY")" + tee_registry="$(jq -r '.[] | select(.name == "FlareTeeManager") | .address' config/coston2/deployed-addresses.json)" + test "$owner" != "" + test "$tee_registry" != "null" + test "$tee_registry" != "" + export DEPLOYMENT_OWNER="$owner" + export TEE_EXTENSION_REGISTRY="$tee_registry" + export TEE_MACHINE_REGISTRY="$tee_registry" + DEPLOY_PHASE=sender bash scripts/deploy-coston2.sh From 8aba111e1eda071c0eea689abbaa68d746a164db Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 05:28:28 +0100 Subject: [PATCH 033/107] ci: add guarded manual Coston2 sender deployment --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 833244a..d119bb6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,3 +76,35 @@ jobs: run: bash scripts/check-evidence-template.sh - name: Validate read-only Coston2 preflight command run: bash scripts/coston2-preflight.sh --offline + + + live_sender_deploy: + name: Coston2 sender deployment (manual only) + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + env: + CHAIN_URL: https://coston2-api.flare.network/ext/C/rpc + DEPLOYMENT_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_DEPLOYER_PRIVATE_KEY }} + steps: + - name: Check out requested ref + uses: actions/checkout@v4 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + + - name: Deploy instruction sender + shell: bash + run: | + set -euo pipefail + test -n "\$DEPLOYMENT_PRIVATE_KEY" + owner="\$(cast wallet address --private-key "\$DEPLOYMENT_PRIVATE_KEY")" + tee_registry="\$(jq -r '.[] | select(.name == "FlareTeeManager") | .address' config/coston2/deployed-addresses.json)" + test "\$owner" != "" + test "\$tee_registry" != "null" + test "\$tee_registry" != "" + export DEPLOYMENT_OWNER="\$owner" + export TEE_EXTENSION_REGISTRY="\$tee_registry" + export TEE_MACHINE_REGISTRY="\$tee_registry" + DEPLOY_PHASE=sender bash scripts/deploy-coston2.sh From f2464c9f2dcc72335e65204e2139469a13daf6fd Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 05:28:37 +0100 Subject: [PATCH 034/107] ci: remove undispatchable sender workflow --- .github/workflows/coston2-sender-deploy.yml | 46 --------------------- 1 file changed, 46 deletions(-) delete mode 100644 .github/workflows/coston2-sender-deploy.yml diff --git a/.github/workflows/coston2-sender-deploy.yml b/.github/workflows/coston2-sender-deploy.yml deleted file mode 100644 index ad74562..0000000 --- a/.github/workflows/coston2-sender-deploy.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Concord Coston2 sender deployment - -on: - workflow_dispatch: - inputs: - ref: - description: Branch or commit to deploy - required: true - default: agent/concord-rebuild - type: string - -permissions: - contents: read - -jobs: - deploy-sender: - name: Deploy ConcordInstructionSender - runs-on: ubuntu-latest - env: - CHAIN_URL: https://coston2-api.flare.network/ext/C/rpc - DEPLOYMENT_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_DEPLOYER_PRIVATE_KEY }} - steps: - - name: Check out requested ref - uses: actions/checkout@v4 - with: - ref: ${{ inputs.ref }} - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: stable - - - name: Deploy instruction sender - shell: bash - run: | - set -euo pipefail - test -n "$DEPLOYMENT_PRIVATE_KEY" - owner="$(cast wallet address --private-key "$DEPLOYMENT_PRIVATE_KEY")" - tee_registry="$(jq -r '.[] | select(.name == "FlareTeeManager") | .address' config/coston2/deployed-addresses.json)" - test "$owner" != "" - test "$tee_registry" != "null" - test "$tee_registry" != "" - export DEPLOYMENT_OWNER="$owner" - export TEE_EXTENSION_REGISTRY="$tee_registry" - export TEE_MACHINE_REGISTRY="$tee_registry" - DEPLOY_PHASE=sender bash scripts/deploy-coston2.sh From 2902b6bf5425b0336dfff74f7c837c5d8ec3c3cd Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 05:29:16 +0100 Subject: [PATCH 035/107] ci: fix guarded sender shell expansion --- .github/workflows/ci.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d119bb6..d5be13c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,13 +98,13 @@ jobs: shell: bash run: | set -euo pipefail - test -n "\$DEPLOYMENT_PRIVATE_KEY" - owner="\$(cast wallet address --private-key "\$DEPLOYMENT_PRIVATE_KEY")" - tee_registry="\$(jq -r '.[] | select(.name == "FlareTeeManager") | .address' config/coston2/deployed-addresses.json)" - test "\$owner" != "" - test "\$tee_registry" != "null" - test "\$tee_registry" != "" - export DEPLOYMENT_OWNER="\$owner" - export TEE_EXTENSION_REGISTRY="\$tee_registry" - export TEE_MACHINE_REGISTRY="\$tee_registry" + test -n "$DEPLOYMENT_PRIVATE_KEY" + owner="$(cast wallet address --private-key "$DEPLOYMENT_PRIVATE_KEY")" + tee_registry="$(jq -r '.[] | select(.name == "FlareTeeManager") | .address' config/coston2/deployed-addresses.json)" + test "$owner" != "" + test "$tee_registry" != "null" + test "$tee_registry" != "" + export DEPLOYMENT_OWNER="$owner" + export TEE_EXTENSION_REGISTRY="$tee_registry" + export TEE_MACHINE_REGISTRY="$tee_registry" DEPLOY_PHASE=sender bash scripts/deploy-coston2.sh From b8f6ee913ee4c391bfee4039c112789a09fe9461 Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 05:44:39 +0100 Subject: [PATCH 036/107] ci: guard live deploy and add FCC registration input --- .github/workflows/ci.yml | 53 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5be13c..a8ffa40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,20 @@ name: Concord verification -on: [push, pull_request, workflow_dispatch] +on: + push: + pull_request: + workflow_dispatch: + inputs: + deploy_sender: + description: 'Deploy a new ConcordInstructionSender on Coston2' + required: false + default: false + type: boolean + register_extension: + description: 'Register the existing sender as a Flare FCC extension' + required: false + default: false + type: boolean permissions: contents: read @@ -80,7 +94,7 @@ jobs: live_sender_deploy: name: Coston2 sender deployment (manual only) - if: github.event_name == 'workflow_dispatch' + if: github.event_name == 'workflow_dispatch' && inputs.deploy_sender == true runs-on: ubuntu-latest env: CHAIN_URL: https://coston2-api.flare.network/ext/C/rpc @@ -108,3 +122,38 @@ jobs: export TEE_EXTENSION_REGISTRY="$tee_registry" export TEE_MACHINE_REGISTRY="$tee_registry" DEPLOY_PHASE=sender bash scripts/deploy-coston2.sh + + + live_extension_register: + name: Coston2 FCC extension registration (manual only) + if: github.event_name == 'workflow_dispatch' && inputs.register_extension == true + runs-on: ubuntu-latest + env: + CHAIN_URL: https://coston2-api.flare.network/ext/C/rpc + DEPLOYMENT_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_DEPLOYER_PRIVATE_KEY }} + INSTRUCTION_SENDER: 0x574b523eA944EFe9143AF9d6c46bfA925beE2968 + steps: + - name: Check out requested ref + uses: actions/checkout@v4 + + - name: Install Go 1.25.1 + uses: actions/setup-go@v5 + with: + go-version: "1.25.1" + cache: false + + - name: Register FCC extension + working-directory: tools + shell: bash + run: | + set -euo pipefail + test -n "$DEPLOYMENT_PRIVATE_KEY" + test "$INSTRUCTION_SENDER" != "" + extension_id="$(go run ./cmd/register-extension \ + -a ../config/coston2/deployed-addresses.json \ + -c "$CHAIN_URL" \ + --instructionSender "$INSTRUCTION_SENDER" \ + 2>"$RUNNER_TEMP/concord-register-extension.log" | tail -1)" + cat "$RUNNER_TEMP/concord-register-extension.log" >&2 + test "$extension_id" =~ ^0x[0-9a-fA-F]{64}$ + echo "Registered FCC extension ID: $extension_id" >> "$GITHUB_STEP_SUMMARY" From 1f439c154e177a35eb0d4ded9b5b4e7d9c2b6a4d Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 05:49:16 +0100 Subject: [PATCH 037/107] ci: quote sender address in FCC registration job --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8ffa40..58c6a41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,7 +131,7 @@ jobs: env: CHAIN_URL: https://coston2-api.flare.network/ext/C/rpc DEPLOYMENT_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_DEPLOYER_PRIVATE_KEY }} - INSTRUCTION_SENDER: 0x574b523eA944EFe9143AF9d6c46bfA925beE2968 + INSTRUCTION_SENDER: "0x574b523eA944EFe9143AF9d6c46bfA925beE2968" steps: - name: Check out requested ref uses: actions/checkout@v4 From 31c601e088fc3c53b879c802e9aa1a2b9d7975fb Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 05:52:36 +0100 Subject: [PATCH 038/107] ci: fix FCC extension id validation --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58c6a41..70b238a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -155,5 +155,5 @@ jobs: --instructionSender "$INSTRUCTION_SENDER" \ 2>"$RUNNER_TEMP/concord-register-extension.log" | tail -1)" cat "$RUNNER_TEMP/concord-register-extension.log" >&2 - test "$extension_id" =~ ^0x[0-9a-fA-F]{64}$ + [[ "$extension_id" =~ ^0x[0-9a-fA-F]{64}$ ]] echo "Registered FCC extension ID: $extension_id" >> "$GITHUB_STEP_SUMMARY" From 143dd24975bbd777baa812d273aea9db0901ddf3 Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 05:54:58 +0100 Subject: [PATCH 039/107] ci: add guarded Coston2 facility deployment phase --- .github/workflows/ci.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70b238a..fd6e5af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,11 @@ on: required: false default: false type: boolean + deploy_facility: + description: 'Deploy the Concord AccordRegistry and CapitalFacility on Coston2' + required: false + default: false + type: boolean permissions: contents: read @@ -157,3 +162,33 @@ jobs: cat "$RUNNER_TEMP/concord-register-extension.log" >&2 [[ "$extension_id" =~ ^0x[0-9a-fA-F]{64}$ ]] echo "Registered FCC extension ID: $extension_id" >> "$GITHUB_STEP_SUMMARY" + + + live_facility_deploy: + name: Coston2 Concord facility deployment (manual only) + if: github.event_name == 'workflow_dispatch' && inputs.deploy_facility == true + runs-on: ubuntu-latest + env: + CHAIN_URL: https://coston2-api.flare.network/ext/C/rpc + DEPLOYMENT_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_DEPLOYER_PRIVATE_KEY }} + DEPLOYMENT_OWNER: "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3" + ALLOCATION_VERIFIER: "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3" + CONCORD_EXTENSION_ID: "0x000000000000000000000000000000000000000000000000000000000001028c" + steps: + - name: Check out requested ref + uses: actions/checkout@v4 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + + - name: Deploy Concord facility + shell: bash + run: | + set -euo pipefail + test -n "$DEPLOYMENT_PRIVATE_KEY" + test "$DEPLOYMENT_OWNER" != "" + test "$ALLOCATION_VERIFIER" != "" + test "$CONCORD_EXTENSION_ID" != "" + DEPLOY_PHASE=facility bash scripts/deploy-coston2.sh From e022383ef34ac99f88d92f38a4745140121ceafb Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 05:57:46 +0100 Subject: [PATCH 040/107] build: optimize Coston2 bytecode and target Paris EVM --- foundry.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/foundry.toml b/foundry.toml index e7aef5f..63d8380 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,3 +6,7 @@ libs = ["lib"] # and restore the remapping: # remappings = ["flare-smart-contracts-v2=lib/flare-smart-contracts-v2"] via-ir = true +optimizer = true +optimizer_runs = 200 +solc_version = "0.8.27" +evm_version = "paris" From a087f093745da917469f41e70adba2c2ee2493d1 Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 06:05:31 +0100 Subject: [PATCH 041/107] docs: record observed Coston2 Concord deployment evidence --- config/coston2/concord-deployment.json | 72 ++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 config/coston2/concord-deployment.json diff --git a/config/coston2/concord-deployment.json b/config/coston2/concord-deployment.json new file mode 100644 index 0000000..c46a5ec --- /dev/null +++ b/config/coston2/concord-deployment.json @@ -0,0 +1,72 @@ +{ + "network": "Coston2", + "chainId": 114, + "observedAt": "2026-08-13T05:00:30Z", + "rpcUrl": "https://coston2-api.flare.network/ext/C/rpc", + "explorerUrl": "https://coston2-explorer.flare.network", + "teeRegistry": "0x1a9C4A0f9D76c0b1D91d22E24E573a9b377618aE", + "extension": { + "id": "0x000000000000000000000000000000000000000000000000000000000001028c", + "decimalId": 66188, + "instructionSender": "0x574b523eA944EFe9143AF9d6c46bfA925beE2968", + "activeTeeMachines": 0 + }, + "assets": { + "fxrp": "0x0b6A3645c240605887a5532109323A3E12273dc7", + "fxrpDecimals": 6, + "usdt0": "0x479854495cefBc8D12B971A3Ec4d18E6dbcE81a3", + "usdt0Decimals": 18 + }, + "canonicalFacility": { + "accordRegistry": "0x9A5663519C3D4B36ef155A4AE0e2d8Be2E7a89cF", + "capitalFacility": "0x0AcBb062BE75491b5992dddD59aEf64a4f4Cc8b8", + "owner": "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3", + "allocationVerifier": "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3", + "registrySetFacility": "0xa0b7a26bf42231dd8e0c2ea59f29339418f31717f129eadd57a842b78c7ad0c4", + "registryDeploy": "0x1058604bb6650650feee1b980973e55fddf1db52d24f8215bf07b52aa67efd9e", + "facilityDeploy": "0x0b0c5acb99a20025e6b6a029474bcad882bbdd9589de39604d06cf41d6f9b3c7", + "blocks": { + "registryDeploy": 33993407, + "facilityDeploy": 33993408 + } + }, + "transactions": { + "instructionSenderDeploy": "0x85e6056f410be261830f5e4e8e35b172886b915ea977ad0fba46953c19577612", + "extensionRegister": "0x20e1bf7f6e14d93fd5c06bd35b2d3eda93e711b46dff0ecf2433da121dac16ca", + "extensionAllowMachineOwners": "0xa4fb47c35c0c42bf7681bed2f48b206486730806c34fe1e04331fc8fcb91c7be", + "extensionAllowProjectOwners": "0x38f3f152d1d4d5578961e55ae595e1422552a363d073690f6fbf05d35b9a8ed3", + "extensionAllowKeyType": "0x33f25969e199780b0962b868a92a4871d0ef6845827e6a92274e6f0128216140" + }, + "supersededFacility": { + "reason": "A second manual facility dispatch completed before the first result was observed; this pair is retained as observed but not used for the canonical flow.", + "accordRegistry": "0xb7f23e078eebc57e624ad06af5caef4526998eee", + "capitalFacility": "0x8d70adbd1b36fb05bae2056013f8bfb9b8e14848", + "registrySetFacility": "0x8b8ee8baefe7bb574dc6001df2caa8da078900c76c6e71b7b7b66f5d02f90f97", + "registryDeploy": "0xdeda1310493445b2966014aab334fd78b741c8b33dbd188bc1ad7e61f9a11a38", + "facilityDeploy": "0xe66c23ed9ac4543196fc9cfa58372f7e9764ab6dc52950a22ed3dd57da73d8d0" + }, + "status": { + "facilityContractsDeployed": true, + "extensionRegistered": true, + "activeTeeMachine": false, + "providerWalletsFunded": false, + "usdt0AvailableToTreasury": false, + "rootAccordCreated": false, + "fccRoundExecuted": false, + "childrenMaterialized": false, + "facilityFunded": false, + "drawSettled": false, + "repaid": false + }, + "warnings": [ + "The current Flare/Foundry toolchain still reports chain 114 as unsupported for EIP-3855; deployment succeeded after targeting solc 0.8.27, optimizer=true, optimizer_runs=200, and evm_version=paris. Keep this warning in the deployment record.", + "The Coston2 FCC development path is not hardware-TEE evidence; the intended path uses the documented simulated development TEE.", + "No private key is stored in this file." + ], + "explorerLinks": { + "instructionSenderDeploy": "https://coston2-explorer.flare.network/tx/0x85e6056f410be261830f5e4e8e35b172886b915ea977ad0fba46953c19577612", + "extensionRegister": "https://coston2-explorer.flare.network/tx/0x20e1bf7f6e14d93fd5c06bd35b2d3eda93e711b46dff0ecf2433da121dac16ca", + "canonicalFacilityDeploy": "https://coston2-explorer.flare.network/tx/0x0b0c5acb99a20025e6b6a029474bcad882bbdd9589de39604d06cf41d6f9b3c7", + "canonicalRegistryDeploy": "https://coston2-explorer.flare.network/tx/0x1058604bb6650650feee1b980973e55fddf1db52d24f8215bf07b52aa67efd9e" + } +} From 5748efd04af7abff308e3d3c4d6d908c2383fdad Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 06:05:40 +0100 Subject: [PATCH 042/107] docs: record live Coston2 deployment boundary --- docs/CONCORD_STATUS.md | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index 9150771..fcf3d43 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -29,16 +29,38 @@ Updated 2026-08-13. and the adversarial cases are recorded in `docs/experiments/coston2-failure-matrix.md`. +## Observed Coston2 deployment + +The following values were read from the live Coston2 RPC and explorer on +2026-08-13 and are recorded in +[config/coston2/concord-deployment.json](../config/coston2/concord-deployment.json). + +- `ConcordInstructionSender` deployed at `0x574b523eA944EFe9143AF9d6c46bfA925beE2968`; transaction + [0x85e605…77612](https://coston2-explorer.flare.network/tx/0x85e6056f410be261830f5e4e8e35b172886b915ea977ad0fba46953c19577612). +- FCC extension `0x…1028c` (decimal `66188`) registered against that sender; + registration and allowlist/key-type transactions succeeded. The primary + registration receipt is + [0x20e1bf…16ca](https://coston2-explorer.flare.network/tx/0x20e1bf7f6e14d93fd5c06bd35b2d3eda93e711b46dff0ecf2433da121dac16ca). +- The later successful facility deployment is the canonical pair for the + next test flow: `AccordRegistry 0x9A5663519C3D4B36ef155A4AE0e2d8Be2E7a89cF` and + `CapitalFacility 0x0AcBb062BE75491b5992dddD59aEf64a4f4Cc8b8`. +- A second manual dispatch also deployed a valid pair before the first result + was observed. It is explicitly marked superseded in the evidence file and + will not be used for the canonical flow. + ## Not yet evidenced -- No Concord contracts are claimed as deployed from this checkpoint. -- No funded Coston2 treasury/provider accounts, FCC registration, instruction - IDs, settlement receipts or end-to-end explorer evidence are recorded yet. -- The configured allocationVerifier is an explicit offchain integration - boundary. A local test actor is not a live FCC proof. The `-mark` path is - deliberately restricted to signed action evidence plus an active machine - lookup; it has not been run against a deployed Concord facility in this - checkpoint. +- No active TEE machine is registered for extension `66188` (the live + registry query returned zero active machines), so no live FCC instruction + round can be claimed yet. +- The three disposable provider wallets are currently unfunded, and the + deployer has 0 USDT0. No provider commitment has therefore occurred. +- No root Accord, Makkari round, FCC quote/finalize instruction, allocation + verification, child Accord, draw, settlement, repayment, or restored-capacity + receipt has been observed. +- The current FCC path still needs a reachable extension proxy and the + scaffold's current Coston2 indexer database credentials before the machine + registration and instruction flow can run. ## Privacy truth From df78b9eeb5e85152a5c37012203c02562a8ded99 Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 06:05:44 +0100 Subject: [PATCH 043/107] docs: distinguish deployed contracts from live lifecycle proof --- docs/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index cbcaef9..72e2aa8 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -110,7 +110,7 @@ go run ./cmd/verify-allocation -c https://coston2-api.flare.network/ext/C/rpc -f go run ./cmd/verify-allocation -c https://coston2-api.flare.network/ext/C/rpc -facility 0x... -teeRegistry 0x... -result ../evidence/allocation.json -extensionId 0x... -roundId 0x... -rootAccordId 0x... -mark -out ../evidence/allocation-verification.json ~~~ -No live FCC or facility deployment is claimed by local tests alone. See +The sender, FCC extension registration, and facility deployment observations are recorded in `docs/CONCORD_STATUS.md`; local tests still do not prove the live lifecycle. See CONCORD_STATUS.md for the current evidence boundary. ## Confidentiality mode disclosure From 6e81fd86e90ec81c58a62fc6a2bfc0a1d2b485eb Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 06:05:55 +0100 Subject: [PATCH 044/107] docs: align README with observed Coston2 deployment --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d02b66e..5d2f761 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,10 @@ The current Coston2 FCC sequence remains the official scaffold sequence: pre-build → start-services --chain coston2 → post-build → run-test ``` -Concord's facility deployment and full Coston2 evidence are not claimed until -the necessary funded account, public proxy, provider offers, and transaction -receipts are available. See [docs/CONCORD_STATUS.md](docs/CONCORD_STATUS.md). +The observed Coston2 sender, FCC extension registration, and facility deployment +receipts are recorded in [docs/CONCORD_STATUS.md](docs/CONCORD_STATUS.md). The +complete Coston2 proof is not claimed until an active FCC machine, funded +provider offers, and the full settlement/repayment lineage are observed. ## Repository map From fbc0ba542ad7e5d907300fecc3db90de7b56dfb0 Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 06:08:10 +0100 Subject: [PATCH 045/107] ci: prevent duplicate Coston2 facility deployment --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd6e5af..6a3b4a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,6 +166,9 @@ jobs: live_facility_deploy: name: Coston2 Concord facility deployment (manual only) + concurrency: + group: concord-coston2-facility-deployment + cancel-in-progress: false if: github.event_name == 'workflow_dispatch' && inputs.deploy_facility == true runs-on: ubuntu-latest env: @@ -191,4 +194,9 @@ jobs: test "$DEPLOYMENT_OWNER" != "" test "$ALLOCATION_VERIFIER" != "" test "$CONCORD_EXTENSION_ID" != "" + existing_facility="$(jq -r '.canonicalFacility.capitalFacility // empty' config/coston2/concord-deployment.json 2>/dev/null || true)" + if [[ -n "$existing_facility" ]] && [[ "$(cast code "$existing_facility" --rpc-url "$CHAIN_URL")" != "0x" ]]; then + echo "canonical CapitalFacility already has code at $existing_facility; refusing a duplicate deployment" >&2 + exit 1 + fi DEPLOY_PHASE=facility bash scripts/deploy-coston2.sh From cc55c1056536fafc61b76643a2b36f86f5bb68fb Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 06:09:41 +0100 Subject: [PATCH 046/107] ci: add guarded Coston2 FCC scaffold setup --- .github/workflows/ci.yml | 123 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a3b4a0..f51b1d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,11 @@ on: required: false default: false type: boolean + setup_fcc: + description: 'Start the official Coston2 FCC scaffold and register one TEE machine' + required: false + default: false + type: boolean permissions: contents: read @@ -200,3 +205,121 @@ jobs: exit 1 fi DEPLOY_PHASE=facility bash scripts/deploy-coston2.sh + + + live_fcc_setup: + name: Coston2 FCC scaffold setup (manual only) + if: github.event_name == 'workflow_dispatch' && inputs.setup_fcc == true + concurrency: + group: concord-coston2-fcc-setup + cancel-in-progress: false + runs-on: ubuntu-latest + env: + CHAIN: coston2 + CHAIN_URL: https://coston2-api.flare.network/ext/C/rpc + NORMAL_PROXY_URL: https://tee-proxy-coston2-1.flare.rocks + LOCAL_MODE: "false" + SIMULATED_TEE: "true" + DEPLOYMENT_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_DEPLOYER_PRIVATE_KEY }} + DEPLOYMENT_OWNER: "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3" + INITIAL_OWNER: "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3" + GOVERNANCE_SIGNERS: "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3" + GOVERNANCE_THRESHOLD: "1" + EXTENSION_ID: "0x000000000000000000000000000000000000000000000000000000000001028c" + INSTRUCTION_SENDER: "0x574b523eA944EFe9143AF9d6c46bfA925beE2968" + PROXY_PRIVATE_KEY: ${{ secrets.CONCORD_FCC_PROXY_PRIVATE_KEY }} + INDEXER_DB_HOST: ${{ secrets.CONCORD_FCC_INDEXER_DB_HOST }} + INDEXER_DB_NAME: ${{ secrets.CONCORD_FCC_INDEXER_DB_NAME }} + INDEXER_DB_USER: ${{ secrets.CONCORD_FCC_INDEXER_DB_USER }} + INDEXER_DB_PASSWORD: ${{ secrets.CONCORD_FCC_INDEXER_DB_PASSWORD }} + steps: + - name: Check out requested ref + uses: actions/checkout@v4 + + - name: Install Go 1.25.1 + uses: actions/setup-go@v5 + with: + go-version: "1.25.1" + cache: false + + - name: Prepare ignored live scaffold configuration + shell: bash + run: | + set -euo pipefail + test -n "$DEPLOYMENT_PRIVATE_KEY" + test -n "$PROXY_PRIVATE_KEY" + test -n "$INDEXER_DB_HOST" + test -n "$INDEXER_DB_NAME" + test -n "$INDEXER_DB_USER" + test -n "$INDEXER_DB_PASSWORD" + umask 077 + printf '%s\n' \ + "CHAIN=$CHAIN" \ + "CHAIN_URL=$CHAIN_URL" \ + "NORMAL_PROXY_URL=$NORMAL_PROXY_URL" \ + "LOCAL_MODE=$LOCAL_MODE" \ + "SIMULATED_TEE=$SIMULATED_TEE" \ + "PROXY_PRIVATE_KEY=$PROXY_PRIVATE_KEY" \ + "DEPLOYMENT_PRIVATE_KEY=$DEPLOYMENT_PRIVATE_KEY" \ + "INITIAL_OWNER=$INITIAL_OWNER" \ + "GOVERNANCE_SIGNERS=$GOVERNANCE_SIGNERS" \ + "GOVERNANCE_THRESHOLD=$GOVERNANCE_THRESHOLD" \ + "ADDRESSES_FILE=./config/coston2/deployed-addresses.json" \ + > .env + printf 'EXTENSION_ID=%s\nINSTRUCTION_SENDER=%s\n' "$EXTENSION_ID" "$INSTRUCTION_SENDER" > config/extension.env + printf '%s\n' \ + 'redis_port = "redis:6379"' \ + 'private_key_variable = "PROXY_PRIVATE_KEY"' \ + 'initial_signing_policy_offset = 2' \ + 'signing_policy_fetch_interval = "20s"' \ + '' \ + 'chain_id = 114' \ + '' \ + '[db]' \ + "host = \"$INDEXER_DB_HOST\"" \ + 'port = 3306' \ + "database = \"$INDEXER_DB_NAME\"" \ + "username = \"$INDEXER_DB_USER\"" \ + "password = \"$INDEXER_DB_PASSWORD\"" \ + 'log_queries = false' \ + '' \ + '[addresses]' \ + 'flare_systems_manager = "0xA90Db6D10F856799b10ef2A77EBCbF460aC71e52"' \ + 'relay = "0xa10B672D1c62e5457b17af63d4302add6A99d7dE"' \ + 'voter_registry = "0x6a0AF07b7972177B176d3D422555cbc98DfDe914"' \ + '' \ + '[ports]' \ + 'internal = "6663"' \ + 'external = "6664"' \ + '' \ + '[info_timing]' \ + 'cycle_internal = "10s"' \ + 'cycle_queue_response_wait = "2s"' \ + '' \ + '[voting]' \ + 'proposal_expiration = "12s"' \ + 'max_pending_request = 10000' \ + > config/proxy/extension_proxy.coston2.docker.toml + + - name: Start official Coston2 FCC scaffold with HTTPS tunnel + shell: bash + run: ./scripts/start-services.sh --chain coston2 --tunnel + + - name: Register simulated development TEE machine + shell: bash + run: | + set -euo pipefail + REGISTER_TEE_COMMAND=rRap ./scripts/post-build.sh + (cd tools && go run ./cmd/query-tee \ + -rpc "$CHAIN_URL" \ + -reg 0x1a9C4A0f9D76c0b1D91d22E24E573a9b377618aE \ + -ext 66188) | tee "$RUNNER_TEMP/concord-tee-query.txt" + grep -q 'Active TEEs for extensionId=66188' "$RUNNER_TEMP/concord-tee-query.txt" + echo 'Coston2 FCC scaffold registered one development TEE machine.' >> "$GITHUB_STEP_SUMMARY" + + - name: Tear down ephemeral FCC services + if: always() + shell: bash + run: | + docker compose -f docker-compose.yaml -f docker-compose.coston2.yaml down --remove-orphans || true + docker compose -f docker-compose.cloudflared.yaml down --remove-orphans || true From a93abbc635873df338f020cf443f5c98811c6b69 Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 06:11:20 +0100 Subject: [PATCH 047/107] docs: record live account balances and funding blocker --- config/coston2/concord-deployment.json | 42 ++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/config/coston2/concord-deployment.json b/config/coston2/concord-deployment.json index c46a5ec..d0a85aa 100644 --- a/config/coston2/concord-deployment.json +++ b/config/coston2/concord-deployment.json @@ -68,5 +68,47 @@ "extensionRegister": "https://coston2-explorer.flare.network/tx/0x20e1bf7f6e14d93fd5c06bd35b2d3eda93e711b46dff0ecf2433da121dac16ca", "canonicalFacilityDeploy": "https://coston2-explorer.flare.network/tx/0x0b0c5acb99a20025e6b6a029474bcad882bbdd9589de39604d06cf41d6f9b3c7", "canonicalRegistryDeploy": "https://coston2-explorer.flare.network/tx/0x1058604bb6650650feee1b980973e55fddf1db52d24f8215bf07b52aa67efd9e" + }, + "accounts": { + "observedAt": "2026-08-13T05:06:00Z", + "treasury": { + "address": "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3", + "c2flr": "86.338238475", + "fxrp": "10", + "usdt0": "0" + }, + "providers": [ + { + "role": "provider_a", + "address": "0x8d9340278094DF3F032c674C912D8eA08c17437C", + "c2flr": "0", + "fxrp": "0", + "usdt0": "0" + }, + { + "role": "provider_b", + "address": "0xF2E25459108A0454a85d03848AD6059C271cc9a5", + "c2flr": "0", + "fxrp": "0", + "usdt0": "0" + }, + { + "role": "provider_c", + "address": "0x9537eA25C28e8c1663e8B57C52A0761a244183A1", + "c2flr": "0", + "fxrp": "0", + "usdt0": "0" + } + ], + "note": "Provider private keys are stored only as encrypted Actions secrets; no private key is recorded here." + }, + "faucet": { + "url": "https://faucet.flare.network/", + "perAddress": "100 C2FLR, 10 USDT0, 10 FXRP per 24 hours", + "neededForNextStep": [ + "provider_a: 100 C2FLR + 10 USDT0", + "provider_b: 100 C2FLR + 10 USDT0", + "provider_c: 100 C2FLR + 10 USDT0" + ] } } From b63522cb817b07b11724980f84503aa85d142e1c Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 06:11:28 +0100 Subject: [PATCH 048/107] docs: refine Coston2 account funding status --- docs/CONCORD_STATUS.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index fcf3d43..cde08ce 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -53,8 +53,10 @@ The following values were read from the live Coston2 RPC and explorer on - No active TEE machine is registered for extension `66188` (the live registry query returned zero active machines), so no live FCC instruction round can be claimed yet. -- The three disposable provider wallets are currently unfunded, and the - deployer has 0 USDT0. No provider commitment has therefore occurred. +- The three disposable provider wallets are currently unfunded; the treasury + has 86.338238475 C2FLR and 10 FXRP but 0 USDT0. No provider commitment has + therefore occurred. The exact public addresses and observed balances are in + the deployment evidence file. - No root Accord, Makkari round, FCC quote/finalize instruction, allocation verification, child Accord, draw, settlement, repayment, or restored-capacity receipt has been observed. From 5cafa91ff7367898ca1675da2e20cd8f1c076689 Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 06:13:33 +0100 Subject: [PATCH 049/107] ci: add guarded root and Makkari round setup --- .github/workflows/ci.yml | 67 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f51b1d3..1f33138 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,11 @@ on: required: false default: false type: boolean + open_round: + description: 'Create the deterministic FXRP root Accord and open its Makkari round' + required: false + default: false + type: boolean permissions: contents: read @@ -323,3 +328,65 @@ jobs: run: | docker compose -f docker-compose.yaml -f docker-compose.coston2.yaml down --remove-orphans || true docker compose -f docker-compose.cloudflared.yaml down --remove-orphans || true + + + live_open_round: + name: Coston2 root Accord and Makkari round (manual only) + if: github.event_name == 'workflow_dispatch' && inputs.open_round == true + concurrency: + group: concord-coston2-open-round + cancel-in-progress: false + runs-on: ubuntu-latest + env: + CHAIN_URL: https://coston2-api.flare.network/ext/C/rpc + DEPLOYMENT_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_DEPLOYER_PRIVATE_KEY }} + CAPITAL_FACILITY: "0x0AcBb062BE75491b5992dddD59aEf64a4f4Cc8b8" + FXRP_TOKEN: "0x0b6A3645c240605887a5532109323A3E12273dc7" + ROOT_ID: "0xee94755eb101d1604d83a4d77eb4c20cd0b4a2ee8fd596c8c63b04109f954042" + ROUND_ID: "0xb281fe0fe8def27b3cb557a13ef1ef03a7e6823e8fadacb83f1dc599b529c93b" + POLICY_HASH: "0x4caea14ad8c3178d0a225d7e238fe71e060a5e62a8bae0020e0701c15d1e4aeb" + TARGET_CAPACITY: "9000000000000000000" + COLLATERAL_AMOUNT: "1000000" + ROOT_VALID_UNTIL: "1787202715" + ROUND_EXPIRY: "1786857115" + MAX_FEE_BPS: "700" + PROVIDER_A: "0x8d9340278094DF3F032c674C912D8eA08c17437C" + PROVIDER_B: "0xF2E25459108A0454a85d03848AD6059C271cc9a5" + PROVIDER_C: "0x9537eA25C28e8c1663e8B57C52A0761a244183A1" + steps: + - name: Check out requested ref + uses: actions/checkout@v4 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + + - name: Create root Accord and open Makkari round + shell: bash + run: | + set -euo pipefail + test -n "$DEPLOYMENT_PRIVATE_KEY" + test "$(cast code "$CAPITAL_FACILITY" --rpc-url "$CHAIN_URL")" != "0x" + send() { + local label="$1" + shift + local output tx + output="$(cast send "$@" --rpc-url "$CHAIN_URL" --private-key "$DEPLOYMENT_PRIVATE_KEY" --json)" + tx="$(jq -r '.transactionHash // .hash // empty' <<<"$output")" + test "$tx" != "" + echo "$label: $tx" + echo "$label: $tx" >> "$GITHUB_STEP_SUMMARY" + } + send approve_fxrp \ + "$FXRP_TOKEN" 'approve(address,uint256)' "$CAPITAL_FACILITY" "$COLLATERAL_AMOUNT" + send create_root \ + "$CAPITAL_FACILITY" 'createRootAccord(bytes32,uint256,uint64,bytes32)' \ + "$ROOT_ID" "$TARGET_CAPACITY" "$ROOT_VALID_UNTIL" "$POLICY_HASH" + send lock_collateral \ + "$CAPITAL_FACILITY" 'lockCollateral(bytes32,uint256)' "$ROOT_ID" "$COLLATERAL_AMOUNT" + send open_syndication \ + "$CAPITAL_FACILITY" 'openSyndication(bytes32,bytes32,uint32,uint64,address[])' \ + "$ROOT_ID" "$ROUND_ID" "$MAX_FEE_BPS" "$ROUND_EXPIRY" \ + "[$PROVIDER_A,$PROVIDER_B,$PROVIDER_C]" + echo 'Root Accord is PROPOSED/SYNDICATING with 1 FXRP locked and a 9 USDT0 target.' >> "$GITHUB_STEP_SUMMARY" From b2839b113596da01f12c39874f444f3e137df77a Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 06:17:01 +0100 Subject: [PATCH 050/107] docs: record live root Accord and Makkari round --- config/coston2/concord-deployment.json | 42 ++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/config/coston2/concord-deployment.json b/config/coston2/concord-deployment.json index d0a85aa..7f43005 100644 --- a/config/coston2/concord-deployment.json +++ b/config/coston2/concord-deployment.json @@ -51,7 +51,7 @@ "activeTeeMachine": false, "providerWalletsFunded": false, "usdt0AvailableToTreasury": false, - "rootAccordCreated": false, + "rootAccordCreated": true, "fccRoundExecuted": false, "childrenMaterialized": false, "facilityFunded": false, @@ -67,7 +67,11 @@ "instructionSenderDeploy": "https://coston2-explorer.flare.network/tx/0x85e6056f410be261830f5e4e8e35b172886b915ea977ad0fba46953c19577612", "extensionRegister": "https://coston2-explorer.flare.network/tx/0x20e1bf7f6e14d93fd5c06bd35b2d3eda93e711b46dff0ecf2433da121dac16ca", "canonicalFacilityDeploy": "https://coston2-explorer.flare.network/tx/0x0b0c5acb99a20025e6b6a029474bcad882bbdd9589de39604d06cf41d6f9b3c7", - "canonicalRegistryDeploy": "https://coston2-explorer.flare.network/tx/0x1058604bb6650650feee1b980973e55fddf1db52d24f8215bf07b52aa67efd9e" + "canonicalRegistryDeploy": "https://coston2-explorer.flare.network/tx/0x1058604bb6650650feee1b980973e55fddf1db52d24f8215bf07b52aa67efd9e", + "rootApprove": "https://coston2-explorer.flare.network/tx/0xddba5e78d15ea4c1b38c2aabcc8bc6eb340e2e70b0864f42f7ee21ad22532b3", + "rootCreate": "https://coston2-explorer.flare.network/tx/0x0f66d8b7c96929dcb18e7e39492ee94e9c758a50811b2533156efc6a33edc675", + "rootLockCollateral": "https://coston2-explorer.flare.network/tx/0xd8f0644744a5b9b2e724e9f637f4b4fbbeda0ed885cbf67c2616fbea828698e6", + "openSyndication": "https://coston2-explorer.flare.network/tx/0x8d32a236da024a63d57302df74dd5e7493d52048b4048f9dd43fcd4a108e68fb" }, "accounts": { "observedAt": "2026-08-13T05:06:00Z", @@ -110,5 +114,39 @@ "provider_b: 100 C2FLR + 10 USDT0", "provider_c: 100 C2FLR + 10 USDT0" ] + }, + "rootRound": { + "rootAccordId": "0xee94755eb101d1604d83a4d77eb4c20cd0b4a2ee8fd596c8c63b04109f954042", + "roundId": "0xb281fe0fe8def27b3cb557a13ef1ef03a7e6823e8fadacb83f1dc599b529c93b", + "rootState": "SYNDICATING", + "roundStatus": "OPEN", + "borrower": "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3", + "targetCapacity": "9000000000000000000", + "targetDisplay": "9 USDT0", + "maxFeeBps": 700, + "collateralLocked": "1000000", + "collateralDisplay": "1 FXRP", + "eligibleProviders": [ + "0x8d9340278094DF3F032c674C912D8eA08c17437C", + "0xF2E25459108A0454a85d03848AD6059C271cc9a5", + "0x9537eA25C28e8c1663e8B57C52A0761a244183A1" + ], + "transactions": { + "approveFxrp": "0xddba5e78d15ea4c1b38c2aabcc8bc6eb340e2e70b0864f42f7ee21ad22532b3b", + "createRoot": "0x0f66d8b7c96929dcb18e7e39492ee94e9c758a50811b2533156efc6a33edc675", + "lockCollateral": "0xd8f0644744a5b9b2e724e9f637f4b4fbbeda0ed885cbf67c2616fbea828698e6", + "openSyndication": "0x8d32a236da024a63d57302df74dd5e7493d52048b4048f9dd43fcd4a108e68fb" + }, + "verifiedAt": "2026-08-13T05:15:00Z", + "verifiedOnchain": { + "facilityFxrpBalance": "1", + "committedCapacity": "0", + "drawnPrincipal": "0", + "eligibleProviderCount": 3, + "registryHasRoot": true, + "rootChildIds": [ + "0xb281fe0fe8def27b3cb557a13ef1ef03a7e6823e8fadacb83f1dc599b529c93b" + ] + } } } From 7ec197fd198a21fd68461a85502ae4b4969141c7 Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 06:17:12 +0100 Subject: [PATCH 051/107] docs: update lifecycle evidence through open round --- docs/CONCORD_STATUS.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index cde08ce..783e1ef 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -50,16 +50,19 @@ The following values were read from the live Coston2 RPC and explorer on ## Not yet evidenced -- No active TEE machine is registered for extension `66188` (the live - registry query returned zero active machines), so no live FCC instruction - round can be claimed yet. +- No active TEE machine is registered for extension `66188` (the live registry + query returned zero active machines), so the OPEN round has not yet produced a + live FCC instruction result. - The three disposable provider wallets are currently unfunded; the treasury has 86.338238475 C2FLR and 10 FXRP but 0 USDT0. No provider commitment has therefore occurred. The exact public addresses and observed balances are in the deployment evidence file. -- No root Accord, Makkari round, FCC quote/finalize instruction, allocation - verification, child Accord, draw, settlement, repayment, or restored-capacity - receipt has been observed. +- The canonical facility now has a live SYNDICATING root Accord and OPEN Makkari + round: 1 FXRP locked, 9 USDT0 target, 700 bps maximum fee, and three eligible + provider addresses. The four transaction hashes and read-back state are in the + deployment evidence file. +- No FCC quote/finalize instruction, allocation verification, child Accord, draw, + settlement, repayment, or restored-capacity receipt has been observed. - The current FCC path still needs a reachable extension proxy and the scaffold's current Coston2 indexer database credentials before the machine registration and instruction flow can run. From b7fc670a192433213e2a6dc33931bab29f34c6c1 Mon Sep 17 00:00:00 2001 From: etvjay Date: Thu, 13 Aug 2026 06:20:44 +0100 Subject: [PATCH 052/107] docs: clarify remaining lifecycle evidence template --- docs/CONCORD_STATUS.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index 783e1ef..c3681c2 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -90,10 +90,10 @@ affected configuration and record the contradiction before continuing. ## Deployment evidence template -When the live slice is run, copy -`docs/templates/coston2-evidence.template.json` to an ignored local evidence -file and fill only observed values. Until then, leave deployment fields empty -rather than inferring them from local tests: +The observed deployment and root/round values are recorded in +`config/coston2/concord-deployment.json`. Use the template below only for +lifecycle fields that have not yet produced live receipts; do not infer them +from local tests: ~~~text Network: From d1a8128e0a9ffd35be777b5d43619f5fb3d5a0a9 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 14:34:41 +0200 Subject: [PATCH 053/107] fix: bind Coston2 flow to current USDT0 test token --- .github/workflows/ci.yml | 29 +++++++++++------ config/coston2/concord-deployment.json | 45 ++++++++++++++++++++------ config/networks/coston2.json | 8 +++-- docs/CONCORD_STATUS.md | 44 ++++++++++++++++--------- scripts/resolve-coston2-assets.sh | 14 +++++--- 5 files changed, 98 insertions(+), 42 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f33138..a58a5e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -340,15 +340,15 @@ jobs: env: CHAIN_URL: https://coston2-api.flare.network/ext/C/rpc DEPLOYMENT_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_DEPLOYER_PRIVATE_KEY }} - CAPITAL_FACILITY: "0x0AcBb062BE75491b5992dddD59aEf64a4f4Cc8b8" - FXRP_TOKEN: "0x0b6A3645c240605887a5532109323A3E12273dc7" - ROOT_ID: "0xee94755eb101d1604d83a4d77eb4c20cd0b4a2ee8fd596c8c63b04109f954042" - ROUND_ID: "0xb281fe0fe8def27b3cb557a13ef1ef03a7e6823e8fadacb83f1dc599b529c93b" - POLICY_HASH: "0x4caea14ad8c3178d0a225d7e238fe71e060a5e62a8bae0020e0701c15d1e4aeb" - TARGET_CAPACITY: "9000000000000000000" + CAPITAL_FACILITY: "" + FXRP_TOKEN: "" + ROOT_ID: "" + ROUND_ID: "" + POLICY_HASH: "" + TARGET_CAPACITY: "9000000" COLLATERAL_AMOUNT: "1000000" - ROOT_VALID_UNTIL: "1787202715" - ROUND_EXPIRY: "1786857115" + ROOT_VALID_UNTIL: "" + ROUND_EXPIRY: "" MAX_FEE_BPS: "700" PROVIDER_A: "0x8d9340278094DF3F032c674C912D8eA08c17437C" PROVIDER_B: "0xF2E25459108A0454a85d03848AD6059C271cc9a5" @@ -367,7 +367,16 @@ jobs: run: | set -euo pipefail test -n "$DEPLOYMENT_PRIVATE_KEY" + CAPITAL_FACILITY="$(jq -r '.canonicalFacility.capitalFacility // empty' config/coston2/concord-deployment.json)" + FXRP_TOKEN="$(jq -r '.assets.fxrp' config/coston2/concord-deployment.json)" + test -n "$CAPITAL_FACILITY" + test -n "$FXRP_TOKEN" test "$(cast code "$CAPITAL_FACILITY" --rpc-url "$CHAIN_URL")" != "0x" + ROOT_ID="$(cast keccak "concord-coston2-root-${GITHUB_RUN_ID}")" + ROUND_ID="$(cast keccak "concord-coston2-round-${GITHUB_RUN_ID}")" + POLICY_HASH="$(cast keccak 'concord-coston2-policy-v2')" + ROOT_VALID_UNTIL="$(date -u -d '+7 days' +%s)" + ROUND_EXPIRY="$(date -u -d '+3 days' +%s)" send() { local label="$1" shift @@ -389,4 +398,6 @@ jobs: "$CAPITAL_FACILITY" 'openSyndication(bytes32,bytes32,uint32,uint64,address[])' \ "$ROOT_ID" "$ROUND_ID" "$MAX_FEE_BPS" "$ROUND_EXPIRY" \ "[$PROVIDER_A,$PROVIDER_B,$PROVIDER_C]" - echo 'Root Accord is PROPOSED/SYNDICATING with 1 FXRP locked and a 9 USDT0 target.' >> "$GITHUB_STEP_SUMMARY" + echo "Root ID: $ROOT_ID" >> "$GITHUB_STEP_SUMMARY" + echo "Round ID: $ROUND_ID" >> "$GITHUB_STEP_SUMMARY" + echo 'Root Accord is PROPOSED/SYNDICATING with 1 FXRP locked and a 9 USDT0 target (6 decimals).' >> "$GITHUB_STEP_SUMMARY" diff --git a/config/coston2/concord-deployment.json b/config/coston2/concord-deployment.json index 7f43005..b3ccfb7 100644 --- a/config/coston2/concord-deployment.json +++ b/config/coston2/concord-deployment.json @@ -14,10 +14,27 @@ "assets": { "fxrp": "0x0b6A3645c240605887a5532109323A3E12273dc7", "fxrpDecimals": 6, - "usdt0": "0x479854495cefBc8D12B971A3Ec4d18E6dbcE81a3", - "usdt0Decimals": 18 + "usdt0": "0xC1A5B41512496B80903D1f32d6dEa3a73212E71F", + "usdt0OnchainSymbol": "USD₮0", + "usdt0Decimals": 6, + "usdt0Source": "https://coston2-explorer.flare.network/tokens" }, - "canonicalFacility": { + "assetCorrection": { + "verifiedAt": "2026-08-13", + "current": { + "token": "0xC1A5B41512496B80903D1f32d6dEa3a73212E71F", + "name": "USDT0 test", + "onchainSymbol": "USD₮0", + "decimals": 6 + }, + "superseded": { + "token": "0x479854495cefBc8D12B971A3Ec4d18E6dbcE81a3", + "decimals": 18 + }, + "reason": "CapitalFacility.liquidityAsset is immutable; the previously deployed facility and root round cannot consume balances of the current faucet token." + }, + "canonicalFacility": null, + "supersededAssetFacility": { "accordRegistry": "0x9A5663519C3D4B36ef155A4AE0e2d8Be2E7a89cF", "capitalFacility": "0x0AcBb062BE75491b5992dddD59aEf64a4f4Cc8b8", "owner": "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3", @@ -47,11 +64,15 @@ }, "status": { "facilityContractsDeployed": true, + "canonicalFacilityDeployed": false, + "assetBindingCurrent": true, "extensionRegistered": true, "activeTeeMachine": false, "providerWalletsFunded": false, - "usdt0AvailableToTreasury": false, + "providerUSDT0BalancesObserved": true, + "usdt0AvailableToTreasury": true, "rootAccordCreated": true, + "rootRoundAssetBindingCurrent": false, "fccRoundExecuted": false, "childrenMaterialized": false, "facilityFunded": false, @@ -59,6 +80,8 @@ "repaid": false }, "warnings": [ + "The previously deployed facility and root round use a separate 18-decimal token at 0x479854495cefBc8D12B971A3Ec4d18E6dbcE81a3; they are superseded for the current Coston2 USDT0 test flow.", + "The current faucet token is 0xC1A5B41512496B80903D1f32d6dEa3a73212E71F (USD₮0, 6 decimals). A replacement facility and root round are required before funding.", "The current Flare/Foundry toolchain still reports chain 114 as unsupported for EIP-3855; deployment succeeded after targeting solc 0.8.27, optimizer=true, optimizer_runs=200, and evm_version=paris. Keep this warning in the deployment record.", "The Coston2 FCC development path is not hardware-TEE evidence; the intended path uses the documented simulated development TEE.", "No private key is stored in this file." @@ -66,8 +89,8 @@ "explorerLinks": { "instructionSenderDeploy": "https://coston2-explorer.flare.network/tx/0x85e6056f410be261830f5e4e8e35b172886b915ea977ad0fba46953c19577612", "extensionRegister": "https://coston2-explorer.flare.network/tx/0x20e1bf7f6e14d93fd5c06bd35b2d3eda93e711b46dff0ecf2433da121dac16ca", - "canonicalFacilityDeploy": "https://coston2-explorer.flare.network/tx/0x0b0c5acb99a20025e6b6a029474bcad882bbdd9589de39604d06cf41d6f9b3c7", - "canonicalRegistryDeploy": "https://coston2-explorer.flare.network/tx/0x1058604bb6650650feee1b980973e55fddf1db52d24f8215bf07b52aa67efd9e", + "supersededAssetFacilityDeploy": "https://coston2-explorer.flare.network/tx/0x0b0c5acb99a20025e6b6a029474bcad882bbdd9589de39604d06cf41d6f9b3c7", + "supersededAssetRegistryDeploy": "https://coston2-explorer.flare.network/tx/0x1058604bb6650650feee1b980973e55fddf1db52d24f8215bf07b52aa67efd9e", "rootApprove": "https://coston2-explorer.flare.network/tx/0xddba5e78d15ea4c1b38c2aabcc8bc6eb340e2e70b0864f42f7ee21ad22532b3", "rootCreate": "https://coston2-explorer.flare.network/tx/0x0f66d8b7c96929dcb18e7e39492ee94e9c758a50811b2533156efc6a33edc675", "rootLockCollateral": "https://coston2-explorer.flare.network/tx/0xd8f0644744a5b9b2e724e9f637f4b4fbbeda0ed885cbf67c2616fbea828698e6", @@ -79,7 +102,7 @@ "address": "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3", "c2flr": "86.338238475", "fxrp": "10", - "usdt0": "0" + "usdt0": "10" }, "providers": [ { @@ -87,21 +110,21 @@ "address": "0x8d9340278094DF3F032c674C912D8eA08c17437C", "c2flr": "0", "fxrp": "0", - "usdt0": "0" + "usdt0": "10" }, { "role": "provider_b", "address": "0xF2E25459108A0454a85d03848AD6059C271cc9a5", "c2flr": "0", "fxrp": "0", - "usdt0": "0" + "usdt0": "10" }, { "role": "provider_c", "address": "0x9537eA25C28e8c1663e8B57C52A0761a244183A1", "c2flr": "0", "fxrp": "0", - "usdt0": "0" + "usdt0": "10" } ], "note": "Provider private keys are stored only as encrypted Actions secrets; no private key is recorded here." @@ -116,6 +139,8 @@ ] }, "rootRound": { + "status": "SUPERSEDED_ASSET_BINDING", + "facility": "0x0AcBb062BE75491b5992dddD59aEf64a4f4Cc8b8", "rootAccordId": "0xee94755eb101d1604d83a4d77eb4c20cd0b4a2ee8fd596c8c63b04109f954042", "roundId": "0xb281fe0fe8def27b3cb557a13ef1ef03a7e6823e8fadacb83f1dc599b529c93b", "rootState": "SYNDICATING", diff --git a/config/networks/coston2.json b/config/networks/coston2.json index 86d0ae4..0f4f901 100644 --- a/config/networks/coston2.json +++ b/config/networks/coston2.json @@ -13,8 +13,10 @@ }, "usdt0": { "symbol": "USDT0", - "token": "0x479854495cefBc8D12B971A3Ec4d18E6dbcE81a3", - "decimals": 18, - "resolution": "Current Coston2 token snapshot; validate symbol, decimals, and bytecode with the resolver." + "onchainSymbol": "USD₮0", + "token": "0xC1A5B41512496B80903D1f32d6dEa3a73212E71F", + "decimals": 6, + "resolution": "Current Coston2 USDT0 test token; validate on-chain symbol USD₮0, decimals, and bytecode with the resolver.", + "source": "https://coston2-explorer.flare.network/tokens" } } diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index c3681c2..653bfff 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -41,26 +41,37 @@ The following values were read from the live Coston2 RPC and explorer on registration and allowlist/key-type transactions succeeded. The primary registration receipt is [0x20e1bf…16ca](https://coston2-explorer.flare.network/tx/0x20e1bf7f6e14d93fd5c06bd35b2d3eda93e711b46dff0ecf2433da121dac16ca). -- The later successful facility deployment is the canonical pair for the - next test flow: `AccordRegistry 0x9A5663519C3D4B36ef155A4AE0e2d8Be2E7a89cF` and - `CapitalFacility 0x0AcBb062BE75491b5992dddD59aEf64a4f4Cc8b8`. -- A second manual dispatch also deployed a valid pair before the first result - was observed. It is explicitly marked superseded in the evidence file and - will not be used for the canonical flow. +- The earlier facility pair was deployed successfully, but it is bound + immutably to a different 18-decimal ERC-20 deployment and is now retained + only as historical evidence. It must not be used for the current faucet + balance or the canonical flow. +- A replacement facility pair must be deployed with the current Coston2 + `USDT0 test` token before a new root Accord is opened. The evidence file + leaves the replacement slot empty until those receipts exist. + +## Asset binding correction + +The earlier checkpoint used `0x479854495cefBc8D12B971A3Ec4d18E6dbcE81a3` and +18 decimals. Live Coston2 RPC verification confirms that the current faucet +asset is `USDT0 test` at +`0xC1A5B41512496B80903D1f32d6dEa3a73212E71F`, with on-chain symbol `USD₮0` +and 6 decimals. The [official Coston2 explorer token list](https://coston2-explorer.flare.network/tokens) +is the recorded address source. Because `CapitalFacility.liquidityAsset` is immutable, this +is a deployment contradiction, not a config-only change; the old facility and +its 18-decimal root round are superseded and a new facility/root round are +required. ## Not yet evidenced - No active TEE machine is registered for extension `66188` (the live registry query returned zero active machines), so the OPEN round has not yet produced a live FCC instruction result. -- The three disposable provider wallets are currently unfunded; the treasury - has 86.338238475 C2FLR and 10 FXRP but 0 USDT0. No provider commitment has - therefore occurred. The exact public addresses and observed balances are in - the deployment evidence file. -- The canonical facility now has a live SYNDICATING root Accord and OPEN Makkari - round: 1 FXRP locked, 9 USDT0 target, 700 bps maximum fee, and three eligible - provider addresses. The four transaction hashes and read-back state are in the - deployment evidence file. +- The three disposable provider wallets and the treasury now each hold 10 + units of the current six-decimal `USDT0 test` token. They still need gas + funding before provider transfers or a replacement root can be broadcast. +- The old facility has a live SYNDICATING root Accord and OPEN Makkari round, + but that round uses the superseded 18-decimal liquidity token and cannot be + used for the current funding proof. - No FCC quote/finalize instruction, allocation verification, child Accord, draw, settlement, repayment, or restored-capacity receipt has been observed. - The current FCC path still needs a reachable extension proxy and the @@ -81,8 +92,9 @@ the facility requires them. - Coston2 chain ID: 114. - FXRP manager is resolved from ContractRegistry under AssetManagerFXRP. - FXRP token is resolved from that manager's fAsset(). -- USDT0 is the current validated Coston2 liquidity-token snapshot in the - network config; the resolver checks symbol USDT0, decimals 18, and code. +- USDT0 is centralized in the network config as the current Coston2 `USDT0 + test` deployment; the resolver checks bytecode, on-chain symbol `USD₮0`, and + 6 decimals. The product-facing alias remains USDT0. The authoritative implementation/docs links are kept in the root README. If the official Flare registry, scaffold or FCC behaviour changes, update the diff --git a/scripts/resolve-coston2-assets.sh b/scripts/resolve-coston2-assets.sh index 49ea8b4..a04ab9b 100755 --- a/scripts/resolve-coston2-assets.sh +++ b/scripts/resolve-coston2-assets.sh @@ -4,7 +4,8 @@ # FXRP is resolved through Flare's ContractRegistry and AssetManagerFXRP. # USDT0 is not currently exposed by the checked-in Coston2 ContractRegistry # names, so its centralized network snapshot is validated against live RPC -# metadata before it is used. +# metadata before it is used. The product alias is USDT0; the current token's +# on-chain metadata is USD₮0 with six decimals. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -21,6 +22,8 @@ REGISTRY="$(jq -r '.contractRegistry' "$CONFIG_FILE")" EXPECTED_MANAGER="$(jq -r '.fxrp.assetManager' "$CONFIG_FILE")" EXPECTED_FXRP="$(jq -r '.fxrp.token' "$CONFIG_FILE")" USDT0="$(jq -r '.usdt0.token' "$CONFIG_FILE")" +EXPECTED_USDT0_SYMBOL="$(jq -r '.usdt0.onchainSymbol' "$CONFIG_FILE")" +EXPECTED_USDT0_DECIMALS="$(jq -r '.usdt0.decimals' "$CONFIG_FILE")" [[ "$RPC_URL" != "null" && "$REGISTRY" != "null" ]] || die "incomplete network config" @@ -40,8 +43,10 @@ USDT0_CODE="$(cast code "$USDT0" --rpc-url "$RPC_URL")" USDT0_SYMBOL="$(cast call "$USDT0" 'symbol()(string)' --rpc-url "$RPC_URL" | tr -d '\"')" USDT0_DECIMALS="$(cast call "$USDT0" 'decimals()(uint8)' --rpc-url "$RPC_URL")" -[[ "$USDT0_SYMBOL" == "USDT0" ]] || die "configured liquidity token returned symbol $USDT0_SYMBOL" -[[ "$USDT0_DECIMALS" == "18" ]] || die "configured USDT0 token returned decimals $USDT0_DECIMALS" +[[ "$USDT0_SYMBOL" == "$EXPECTED_USDT0_SYMBOL" ]] || \ + die "configured liquidity token returned on-chain symbol $USDT0_SYMBOL, expected $EXPECTED_USDT0_SYMBOL" +[[ "$USDT0_DECIMALS" == "$EXPECTED_USDT0_DECIMALS" ]] || \ + die "configured USDT0 token returned decimals $USDT0_DECIMALS, expected $EXPECTED_USDT0_DECIMALS" jq -n \ --arg network "$(jq -r '.network' "$CONFIG_FILE")" \ @@ -52,6 +57,7 @@ jq -n \ --arg assetManager "$ASSET_MANAGER" \ --arg fxrpToken "$FXRP_TOKEN" \ --arg usdt0Token "$USDT0" \ + --arg usdt0DisplaySymbol "$(jq -r '.usdt0.symbol' "$CONFIG_FILE")" \ --arg usdt0Symbol "$USDT0_SYMBOL" \ --argjson usdt0Decimals "$USDT0_DECIMALS" \ - '{network:$network,chainId:$chainId,rpcUrl:$rpcUrl,explorerUrl:$explorerUrl,contractRegistry:$contractRegistry,fxrp:{assetManager:$assetManager,token:$fxrpToken,decimals:6},usdt0:{token:$usdt0Token,symbol:$usdt0Symbol,decimals:$usdt0Decimals}}' + '{network:$network,chainId:$chainId,rpcUrl:$rpcUrl,explorerUrl:$explorerUrl,contractRegistry:$contractRegistry,fxrp:{assetManager:$assetManager,token:$fxrpToken,decimals:6},usdt0:{token:$usdt0Token,symbol:$usdt0DisplaySymbol,onchainSymbol:$usdt0Symbol,decimals:$usdt0Decimals}}' From 6fa6b8d1a9026069537ed4b0bcb867e068a58387 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 14:42:47 +0200 Subject: [PATCH 054/107] docs: record corrected Coston2 facility deployment --- config/coston2/concord-deployment.json | 36 ++++++++++++++++++++------ docs/CONCORD_STATUS.md | 17 ++++++------ 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/config/coston2/concord-deployment.json b/config/coston2/concord-deployment.json index b3ccfb7..ef99c93 100644 --- a/config/coston2/concord-deployment.json +++ b/config/coston2/concord-deployment.json @@ -33,7 +33,23 @@ }, "reason": "CapitalFacility.liquidityAsset is immutable; the previously deployed facility and root round cannot consume balances of the current faucet token." }, - "canonicalFacility": null, + "canonicalFacility": { + "accordRegistry": "0x68b19a3967760489b57341669cd7ea960b5f7367", + "capitalFacility": "0xfaff601a18a9fca33378953515aa0f3ef9286ecd", + "owner": "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3", + "allocationVerifier": "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3", + "liquidityAsset": "0xC1A5B41512496B80903D1f32d6dEa3a73212E71F", + "liquidityAssetDecimals": 6, + "registrySetFacility": "0x7d4512a8f0adcaabd6896e3bfea32f88b5e29d9ba0a5773cc29eb300c888009d", + "registryDeploy": "0xdcaf8538ecb6f416e1d85500d14d13e0476d6135e6c05583344b14b8653a3b5c", + "facilityDeploy": "0x476b51a35934c84f1c8b593ff07c29a3a69e29a72559aa8e9f63f902fb1a1905", + "workflowRun": "https://github.com/etvjay/Concord/actions/runs/31700945393", + "blocks": { + "registryDeploy": 34008030, + "facilityDeploy": 34008030, + "registrySetFacility": 34008031 + } + }, "supersededAssetFacility": { "accordRegistry": "0x9A5663519C3D4B36ef155A4AE0e2d8Be2E7a89cF", "capitalFacility": "0x0AcBb062BE75491b5992dddD59aEf64a4f4Cc8b8", @@ -64,14 +80,14 @@ }, "status": { "facilityContractsDeployed": true, - "canonicalFacilityDeployed": false, + "canonicalFacilityDeployed": true, "assetBindingCurrent": true, "extensionRegistered": true, "activeTeeMachine": false, "providerWalletsFunded": false, "providerUSDT0BalancesObserved": true, "usdt0AvailableToTreasury": true, - "rootAccordCreated": true, + "rootAccordCreated": false, "rootRoundAssetBindingCurrent": false, "fccRoundExecuted": false, "childrenMaterialized": false, @@ -81,7 +97,7 @@ }, "warnings": [ "The previously deployed facility and root round use a separate 18-decimal token at 0x479854495cefBc8D12B971A3Ec4d18E6dbcE81a3; they are superseded for the current Coston2 USDT0 test flow.", - "The current faucet token is 0xC1A5B41512496B80903D1f32d6dEa3a73212E71F (USD₮0, 6 decimals). A replacement facility and root round are required before funding.", + "The current faucet token is 0xC1A5B41512496B80903D1f32d6dEa3a73212E71F (USD₮0, 6 decimals). The replacement facility is deployed; a new root round is required before funding.", "The current Flare/Foundry toolchain still reports chain 114 as unsupported for EIP-3855; deployment succeeded after targeting solc 0.8.27, optimizer=true, optimizer_runs=200, and evm_version=paris. Keep this warning in the deployment record.", "The Coston2 FCC development path is not hardware-TEE evidence; the intended path uses the documented simulated development TEE.", "No private key is stored in this file." @@ -89,6 +105,9 @@ "explorerLinks": { "instructionSenderDeploy": "https://coston2-explorer.flare.network/tx/0x85e6056f410be261830f5e4e8e35b172886b915ea977ad0fba46953c19577612", "extensionRegister": "https://coston2-explorer.flare.network/tx/0x20e1bf7f6e14d93fd5c06bd35b2d3eda93e711b46dff0ecf2433da121dac16ca", + "canonicalFacilityDeploy": "https://coston2-explorer.flare.network/tx/0x476b51a35934c84f1c8b593ff07c29a3a69e29a72559aa8e9f63f902fb1a1905", + "canonicalRegistryDeploy": "https://coston2-explorer.flare.network/tx/0xdcaf8538ecb6f416e1d85500d14d13e0476d6135e6c05583344b14b8653a3b5c", + "canonicalRegistrySetFacility": "https://coston2-explorer.flare.network/tx/0x7d4512a8f0adcaabd6896e3bfea32f88b5e29d9ba0a5773cc29eb300c888009d", "supersededAssetFacilityDeploy": "https://coston2-explorer.flare.network/tx/0x0b0c5acb99a20025e6b6a029474bcad882bbdd9589de39604d06cf41d6f9b3c7", "supersededAssetRegistryDeploy": "https://coston2-explorer.flare.network/tx/0x1058604bb6650650feee1b980973e55fddf1db52d24f8215bf07b52aa67efd9e", "rootApprove": "https://coston2-explorer.flare.network/tx/0xddba5e78d15ea4c1b38c2aabcc8bc6eb340e2e70b0864f42f7ee21ad22532b3", @@ -133,12 +152,13 @@ "url": "https://faucet.flare.network/", "perAddress": "100 C2FLR, 10 USDT0, 10 FXRP per 24 hours", "neededForNextStep": [ - "provider_a: 100 C2FLR + 10 USDT0", - "provider_b: 100 C2FLR + 10 USDT0", - "provider_c: 100 C2FLR + 10 USDT0" + "provider_a: C2FLR for gas and approval/funding transactions", + "provider_b: C2FLR for gas and approval/funding transactions", + "provider_c: C2FLR for gas and approval/funding transactions" ] }, - "rootRound": { + "rootRound": null, + "supersededRootRound": { "status": "SUPERSEDED_ASSET_BINDING", "facility": "0x0AcBb062BE75491b5992dddD59aEf64a4f4Cc8b8", "rootAccordId": "0xee94755eb101d1604d83a4d77eb4c20cd0b4a2ee8fd596c8c63b04109f954042", diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index 653bfff..b06f0c5 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -45,9 +45,10 @@ The following values were read from the live Coston2 RPC and explorer on immutably to a different 18-decimal ERC-20 deployment and is now retained only as historical evidence. It must not be used for the current faucet balance or the canonical flow. -- A replacement facility pair must be deployed with the current Coston2 - `USDT0 test` token before a new root Accord is opened. The evidence file - leaves the replacement slot empty until those receipts exist. +- The replacement pair is now deployed with the current Coston2 `USDT0 test` + token: `AccordRegistry 0x68b19a3967760489b57341669cd7ea960b5f7367` and + `CapitalFacility 0xfaff601a18a9fca33378953515aa0f3ef9286ecd`. The deployment + receipts and registry-link transaction are in the evidence file. ## Asset binding correction @@ -58,8 +59,8 @@ asset is `USDT0 test` at and 6 decimals. The [official Coston2 explorer token list](https://coston2-explorer.flare.network/tokens) is the recorded address source. Because `CapitalFacility.liquidityAsset` is immutable, this is a deployment contradiction, not a config-only change; the old facility and -its 18-decimal root round are superseded and a new facility/root round are -required. +its 18-decimal root round are superseded. The replacement facility is now +bound to the current token; its root round has not yet been opened. ## Not yet evidenced @@ -69,9 +70,9 @@ required. - The three disposable provider wallets and the treasury now each hold 10 units of the current six-decimal `USDT0 test` token. They still need gas funding before provider transfers or a replacement root can be broadcast. -- The old facility has a live SYNDICATING root Accord and OPEN Makkari round, - but that round uses the superseded 18-decimal liquidity token and cannot be - used for the current funding proof. +- The replacement facility is deployed but does not yet have a current root + Accord or Makkari round. The old facility's live round remains historical + only because it uses the superseded 18-decimal liquidity token. - No FCC quote/finalize instruction, allocation verification, child Accord, draw, settlement, repayment, or restored-capacity receipt has been observed. - The current FCC path still needs a reachable extension proxy and the From 9609ab3433b955bfffe657d6e03deb783a562d25 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 14:46:28 +0200 Subject: [PATCH 055/107] docs: record current Coston2 root round --- config/coston2/concord-deployment.json | 45 +++++++++++++++++++++++--- docs/CONCORD_STATUS.md | 11 ++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/config/coston2/concord-deployment.json b/config/coston2/concord-deployment.json index ef99c93..efb2ec7 100644 --- a/config/coston2/concord-deployment.json +++ b/config/coston2/concord-deployment.json @@ -87,8 +87,8 @@ "providerWalletsFunded": false, "providerUSDT0BalancesObserved": true, "usdt0AvailableToTreasury": true, - "rootAccordCreated": false, - "rootRoundAssetBindingCurrent": false, + "rootAccordCreated": true, + "rootRoundAssetBindingCurrent": true, "fccRoundExecuted": false, "childrenMaterialized": false, "facilityFunded": false, @@ -97,7 +97,7 @@ }, "warnings": [ "The previously deployed facility and root round use a separate 18-decimal token at 0x479854495cefBc8D12B971A3Ec4d18E6dbcE81a3; they are superseded for the current Coston2 USDT0 test flow.", - "The current faucet token is 0xC1A5B41512496B80903D1f32d6dEa3a73212E71F (USD₮0, 6 decimals). The replacement facility is deployed; a new root round is required before funding.", + "The current faucet token is 0xC1A5B41512496B80903D1f32d6dEa3a73212E71F (USD₮0, 6 decimals). The replacement facility and root round are deployed; providers still need C2FLR gas before funding.", "The current Flare/Foundry toolchain still reports chain 114 as unsupported for EIP-3855; deployment succeeded after targeting solc 0.8.27, optimizer=true, optimizer_runs=200, and evm_version=paris. Keep this warning in the deployment record.", "The Coston2 FCC development path is not hardware-TEE evidence; the intended path uses the documented simulated development TEE.", "No private key is stored in this file." @@ -157,7 +157,44 @@ "provider_c: C2FLR for gas and approval/funding transactions" ] }, - "rootRound": null, + "rootRound": { + "status": "CURRENT", + "facility": "0xfaff601a18a9fca33378953515aa0f3ef9286ecd", + "rootAccordId": "0x6e03af41b0194c5a369a50629474090cfc5b041a712144855e6efb1a574cfddd", + "roundId": "0x7323280ede4786463744a755e4cddf9ce2a91be952d05d152679b58b67d176ce", + "rootState": "SYNDICATING", + "roundStatus": "OPEN", + "borrower": "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3", + "targetCapacity": "9000000", + "targetDisplay": "9 USDT0", + "targetDecimals": 6, + "maxFeeBps": 700, + "collateralLocked": "1000000", + "collateralDisplay": "1 FXRP", + "eligibleProviders": [ + "0x8d9340278094DF3F032c674C912D8eA08c17437C", + "0xF2E25459108A0454a85d03848AD6059C271cc9a5", + "0x9537eA25C28e8c1663e8B57C52A0761a244183A1" + ], + "transactions": { + "approveFxrp": "0x790db768cbbe95450d25b73c3d6afa1a040acc70b6ae25803e194704814c2e22", + "createRoot": "0xd6e0e48abdf3e7cd3efc336df9d616f10c923ad69d0f6dbcfddbbe79f43cca71", + "lockCollateral": "0x618097ee2b6ab99e6eee868c596876e9cadf9b409335f7741d6fab8acbc77297", + "openSyndication": "0x393174832b260cdd34a91a3df24f23066ac3672a64f1b0bcf25468236b7314c8" + }, + "workflowRun": "https://github.com/etvjay/Concord/actions/runs/31701379940", + "verifiedAt": "2026-08-13T12:43:43Z", + "verifiedOnchain": { + "facilityFxrpBalance": "1", + "committedCapacity": "0", + "drawnPrincipal": "0", + "eligibleProviderCount": 3, + "registryHasRoot": true, + "rootChildIds": [ + "0x7323280ede4786463744a755e4cddf9ce2a91be952d05d152679b58b67d176ce" + ] + } + }, "supersededRootRound": { "status": "SUPERSEDED_ASSET_BINDING", "facility": "0x0AcBb062BE75491b5992dddD59aEf64a4f4Cc8b8", diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index b06f0c5..921e25d 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -60,7 +60,7 @@ and 6 decimals. The [official Coston2 explorer token list](https://coston2-explo is the recorded address source. Because `CapitalFacility.liquidityAsset` is immutable, this is a deployment contradiction, not a config-only change; the old facility and its 18-decimal root round are superseded. The replacement facility is now -bound to the current token; its root round has not yet been opened. +bound to the current token, and its current root round is recorded below. ## Not yet evidenced @@ -70,9 +70,12 @@ bound to the current token; its root round has not yet been opened. - The three disposable provider wallets and the treasury now each hold 10 units of the current six-decimal `USDT0 test` token. They still need gas funding before provider transfers or a replacement root can be broadcast. -- The replacement facility is deployed but does not yet have a current root - Accord or Makkari round. The old facility's live round remains historical - only because it uses the superseded 18-decimal liquidity token. +- The replacement facility has a current SYNDICATING root Accord and OPEN + Makkari round: root `0x6e03af…cfddd`, round + `0x732328…76ce`, 1 FXRP locked, and a 9 USDT0 target at 6 decimals. The + four transaction receipts and workflow run are in the evidence file. +- The old facility's live round remains historical only because it uses the + superseded 18-decimal liquidity token. - No FCC quote/finalize instruction, allocation verification, child Accord, draw, settlement, repayment, or restored-capacity receipt has been observed. - The current FCC path still needs a reachable extension proxy and the From 6947758722618d07409cf78c3fc4ba90809e626e Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 14:47:22 +0200 Subject: [PATCH 056/107] ci: add guarded provider gas funding --- .github/workflows/ci.yml | 44 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a58a5e4..91bd1a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,11 @@ on: required: false default: false type: boolean + fund_provider_gas: + description: 'Send 5 C2FLR to each provider for approval and funding gas' + required: false + default: false + type: boolean permissions: contents: read @@ -401,3 +406,42 @@ jobs: echo "Root ID: $ROOT_ID" >> "$GITHUB_STEP_SUMMARY" echo "Round ID: $ROUND_ID" >> "$GITHUB_STEP_SUMMARY" echo 'Root Accord is PROPOSED/SYNDICATING with 1 FXRP locked and a 9 USDT0 target (6 decimals).' >> "$GITHUB_STEP_SUMMARY" + + + live_provider_gas: + name: Coston2 provider gas funding (manual only) + if: github.event_name == 'workflow_dispatch' && inputs.fund_provider_gas == true + concurrency: + group: concord-coston2-provider-gas + cancel-in-progress: false + runs-on: ubuntu-latest + env: + CHAIN_URL: https://coston2-api.flare.network/ext/C/rpc + DEPLOYMENT_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_DEPLOYER_PRIVATE_KEY }} + PROVIDER_A: "0x8d9340278094DF3F032c674C912D8eA08c17437C" + PROVIDER_B: "0xF2E25459108A0454a85d03848AD6059C271cc9a5" + PROVIDER_C: "0x9537eA25C28e8c1663e8B57C52A0761a244183A1" + GAS_AMOUNT: "5ether" + steps: + - name: Check out requested ref + uses: actions/checkout@v4 + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + + - name: Fund provider gas balances + shell: bash + run: | + set -euo pipefail + test -n "$DEPLOYMENT_PRIVATE_KEY" + sender="$(cast wallet address --private-key "$DEPLOYMENT_PRIVATE_KEY")" + echo "Funding providers from $sender with $GAS_AMOUNT each." + for provider in "$PROVIDER_A" "$PROVIDER_B" "$PROVIDER_C"; do + output="$(cast send "$provider" --value "$GAS_AMOUNT" --rpc-url "$CHAIN_URL" --private-key "$DEPLOYMENT_PRIVATE_KEY" --json)" + tx="$(jq -r '.transactionHash // .hash // empty' <<<"$output")" + test -n "$tx" + echo "$provider: $tx" + echo "$provider gas funding: $tx" >> "$GITHUB_STEP_SUMMARY" + done From 3170387ac5c111df336c246a33b869c88e7a1e1f Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 14:49:34 +0200 Subject: [PATCH 057/107] docs: record provider gas funding evidence --- config/coston2/concord-deployment.json | 28 +++++++++++++++++--------- docs/CONCORD_STATUS.md | 5 +++-- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/config/coston2/concord-deployment.json b/config/coston2/concord-deployment.json index efb2ec7..bf62d58 100644 --- a/config/coston2/concord-deployment.json +++ b/config/coston2/concord-deployment.json @@ -84,7 +84,7 @@ "assetBindingCurrent": true, "extensionRegistered": true, "activeTeeMachine": false, - "providerWalletsFunded": false, + "providerWalletsFunded": true, "providerUSDT0BalancesObserved": true, "usdt0AvailableToTreasury": true, "rootAccordCreated": true, @@ -97,7 +97,7 @@ }, "warnings": [ "The previously deployed facility and root round use a separate 18-decimal token at 0x479854495cefBc8D12B971A3Ec4d18E6dbcE81a3; they are superseded for the current Coston2 USDT0 test flow.", - "The current faucet token is 0xC1A5B41512496B80903D1f32d6dEa3a73212E71F (USD₮0, 6 decimals). The replacement facility and root round are deployed; providers still need C2FLR gas before funding.", + "The current faucet token is 0xC1A5B41512496B80903D1f32d6dEa3a73212E71F (USD₮0, 6 decimals). The replacement facility and root round are deployed, and provider gas is funded; FCC execution and quote/funding steps remain.", "The current Flare/Foundry toolchain still reports chain 114 as unsupported for EIP-3855; deployment succeeded after targeting solc 0.8.27, optimizer=true, optimizer_runs=200, and evm_version=paris. Keep this warning in the deployment record.", "The Coston2 FCC development path is not hardware-TEE evidence; the intended path uses the documented simulated development TEE.", "No private key is stored in this file." @@ -116,10 +116,10 @@ "openSyndication": "https://coston2-explorer.flare.network/tx/0x8d32a236da024a63d57302df74dd5e7493d52048b4048f9dd43fcd4a108e68fb" }, "accounts": { - "observedAt": "2026-08-13T05:06:00Z", + "observedAt": "2026-08-13T12:48:12Z", "treasury": { "address": "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3", - "c2flr": "86.338238475", + "c2flr": "62.76633304163635", "fxrp": "10", "usdt0": "10" }, @@ -127,21 +127,21 @@ { "role": "provider_a", "address": "0x8d9340278094DF3F032c674C912D8eA08c17437C", - "c2flr": "0", + "c2flr": "105", "fxrp": "0", "usdt0": "10" }, { "role": "provider_b", "address": "0xF2E25459108A0454a85d03848AD6059C271cc9a5", - "c2flr": "0", + "c2flr": "105", "fxrp": "0", "usdt0": "10" }, { "role": "provider_c", "address": "0x9537eA25C28e8c1663e8B57C52A0761a244183A1", - "c2flr": "0", + "c2flr": "105", "fxrp": "0", "usdt0": "10" } @@ -152,11 +152,19 @@ "url": "https://faucet.flare.network/", "perAddress": "100 C2FLR, 10 USDT0, 10 FXRP per 24 hours", "neededForNextStep": [ - "provider_a: C2FLR for gas and approval/funding transactions", - "provider_b: C2FLR for gas and approval/funding transactions", - "provider_c: C2FLR for gas and approval/funding transactions" + "provider_a/provider_b/provider_c: approve and fund selected child allocations", + "FCC: active machine, reachable proxy, and current indexer credentials" ] }, + "providerGasFunding": { + "amountPerProvider": "5 C2FLR", + "workflowRun": "https://github.com/etvjay/Concord/actions/runs/31701743746", + "transactions": { + "provider_a": "0x1abd6e28d90cb9ad15aef85a9e7aa3bf64d4406078afe3ab6afcee5836b84982", + "provider_b": "0xc14f3e74a67624dd50a1b501515c7959aac4c1a2bdcd010f0f8c36eec65ebb1d", + "provider_c": "0x22a0db432f8a782e84ddf69cb2734e57644909b18b20453a5eadab8f85941182" + } + }, "rootRound": { "status": "CURRENT", "facility": "0xfaff601a18a9fca33378953515aa0f3ef9286ecd", diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index 921e25d..f81cad7 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -68,8 +68,9 @@ bound to the current token, and its current root round is recorded below. query returned zero active machines), so the OPEN round has not yet produced a live FCC instruction result. - The three disposable provider wallets and the treasury now each hold 10 - units of the current six-decimal `USDT0 test` token. They still need gas - funding before provider transfers or a replacement root can be broadcast. + units of the current six-decimal `USDT0 test` token. Each provider also has + 105 C2FLR after the guarded gas-funding step, so approval and funding + transactions can proceed. - The replacement facility has a current SYNDICATING root Accord and OPEN Makkari round: root `0x6e03af…cfddd`, round `0x732328…76ce`, 1 FXRP locked, and a 9 USDT0 target at 6 decimals. The From a3ce5b16f3bdc4b173ec58ab9f0e0b3fcf56e799 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 14:52:33 +0200 Subject: [PATCH 058/107] ci: require stable named FCC proxy --- .env.example | 5 +++-- .github/workflows/ci.yml | 9 +++++++++ docker-compose.cloudflared.yaml | 13 +++++++++++++ docs/cloudflared.md | 4 ++-- 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 docker-compose.cloudflared.yaml diff --git a/.env.example b/.env.example index 95ef8a8..ab3913a 100644 --- a/.env.example +++ b/.env.example @@ -55,8 +55,9 @@ NORMAL_PROXY_URL=https://tee-proxy-coston2-1.flare.rocks # EXT_PROXY_URL=http://localhost:6674 EXT_PROXY_URL=https://.ngrok-free.dev -# Cloudflare tunnel (docs/cloudflared.md) — quick tunnel by default, no account. -# TUNNEL_ARGS=run --token eyJ... # named tunnel = stable hostname +# Cloudflare tunnel (docs/cloudflared.md). Use a remotely-managed named tunnel +# for FCC registration; quick tunnels are not stable enough for an onchain URL. +# TUNNEL_ARGS=tunnel --no-autoupdate run --token eyJ... # TUNNEL_TARGET=http://host.docker.internal:6664 # only if the proxy is not on 6674 # --- Docker Compose overrides (all have defaults, uncomment to customize) --- diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91bd1a3..ad00101 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -228,6 +228,9 @@ jobs: CHAIN: coston2 CHAIN_URL: https://coston2-api.flare.network/ext/C/rpc NORMAL_PROXY_URL: https://tee-proxy-coston2-1.flare.rocks + EXT_PROXY_URL: ${{ secrets.CONCORD_FCC_PUBLIC_PROXY_URL }} + EXT_PROXY_HOST_URL: ${{ secrets.CONCORD_FCC_PUBLIC_PROXY_URL }} + TUNNEL_ARGS: ${{ secrets.CONCORD_FCC_TUNNEL_ARGS }} LOCAL_MODE: "false" SIMULATED_TEE: "true" DEPLOYMENT_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_DEPLOYER_PRIVATE_KEY }} @@ -258,6 +261,9 @@ jobs: set -euo pipefail test -n "$DEPLOYMENT_PRIVATE_KEY" test -n "$PROXY_PRIVATE_KEY" + test -n "$EXT_PROXY_URL" + test -n "$EXT_PROXY_HOST_URL" + test -n "$TUNNEL_ARGS" test -n "$INDEXER_DB_HOST" test -n "$INDEXER_DB_NAME" test -n "$INDEXER_DB_USER" @@ -267,6 +273,9 @@ jobs: "CHAIN=$CHAIN" \ "CHAIN_URL=$CHAIN_URL" \ "NORMAL_PROXY_URL=$NORMAL_PROXY_URL" \ + "EXT_PROXY_URL=$EXT_PROXY_URL" \ + "EXT_PROXY_HOST_URL=$EXT_PROXY_HOST_URL" \ + "TUNNEL_ARGS=$TUNNEL_ARGS" \ "LOCAL_MODE=$LOCAL_MODE" \ "SIMULATED_TEE=$SIMULATED_TEE" \ "PROXY_PRIVATE_KEY=$PROXY_PRIVATE_KEY" \ diff --git a/docker-compose.cloudflared.yaml b/docker-compose.cloudflared.yaml new file mode 100644 index 0000000..4702441 --- /dev/null +++ b/docker-compose.cloudflared.yaml @@ -0,0 +1,13 @@ +name: tunnel + +services: + cloudflared: + image: cloudflare/cloudflared:latest + # TUNNEL_ARGS should be a remotely-managed named tunnel command, for example: + # tunnel --no-autoupdate run --token + # The quick-tunnel fallback remains useful for local inspection only; it + # must not be used when registering a Coston2 FCC machine. + command: ${TUNNEL_ARGS:-tunnel --no-autoupdate --url ${TUNNEL_TARGET:-http://host.docker.internal:6674}} + extra_hosts: + - "host.docker.internal:host-gateway" + restart: "no" diff --git a/docs/cloudflared.md b/docs/cloudflared.md index db459f8..699850f 100644 --- a/docs/cloudflared.md +++ b/docs/cloudflared.md @@ -106,13 +106,13 @@ Quick tunnels always rotate and no flag pins them. Cheapest fix first: app containers does not touch it. Only restarting `cloudflared` itself rotates the URL. **Named tunnel — permanently fixed hostname.** Needs a free Cloudflare account with a domain -on it: +on it. This is required for the Coston2 FCC registration path: 1. Zero Trust → Networks → Tunnels → *Create a tunnel* → Cloudflared. Copy the `eyJ...` token. 2. Add a public hostname pointing at `http://host.docker.internal:6674`. 3. In `.env`: ``` - TUNNEL_ARGS=run --token eyJhIjoi... + TUNNEL_ARGS=tunnel --no-autoupdate run --token eyJhIjoi... EXT_PROXY_URL=https://tee.yourdomain.com ``` 4. `docker compose -f docker-compose.cloudflared.yaml up -d` — same command, named mode now. From 0b10a692e5050baf45330d1f4157850e52af0709 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 14:53:57 +0200 Subject: [PATCH 059/107] fix: align named tunnel command with scaffold --- .env.example | 3 ++- docker-compose.cloudflared.yaml | 14 +++++++------- docs/cloudflared.md | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index ab3913a..b7826b2 100644 --- a/.env.example +++ b/.env.example @@ -57,7 +57,8 @@ EXT_PROXY_URL=https://.ngrok-free.dev # Cloudflare tunnel (docs/cloudflared.md). Use a remotely-managed named tunnel # for FCC registration; quick tunnels are not stable enough for an onchain URL. -# TUNNEL_ARGS=tunnel --no-autoupdate run --token eyJ... +# start-services.sh prepends `tunnel --no-autoupdate` to this value. +# TUNNEL_ARGS=run --token eyJ... # TUNNEL_TARGET=http://host.docker.internal:6664 # only if the proxy is not on 6674 # --- Docker Compose overrides (all have defaults, uncomment to customize) --- diff --git a/docker-compose.cloudflared.yaml b/docker-compose.cloudflared.yaml index 4702441..911a145 100644 --- a/docker-compose.cloudflared.yaml +++ b/docker-compose.cloudflared.yaml @@ -1,13 +1,13 @@ +# cloudflared replacement for ngrok on the external ext-proxy port. +# start-services.sh / stop-services.sh drive this for you on coston/coston2. name: tunnel - services: cloudflared: image: cloudflare/cloudflared:latest - # TUNNEL_ARGS should be a remotely-managed named tunnel command, for example: - # tunnel --no-autoupdate run --token - # The quick-tunnel fallback remains useful for local inspection only; it - # must not be used when registering a Coston2 FCC machine. - command: ${TUNNEL_ARGS:-tunnel --no-autoupdate --url ${TUNNEL_TARGET:-http://host.docker.internal:6674}} + # Project name + `command:` must match every extension's copy — that is what + # makes them reuse one container instead of minting a new URL each time. + command: tunnel --no-autoupdate ${TUNNEL_ARGS:---url ${TUNNEL_TARGET:-http://host.docker.internal:6674}} extra_hosts: - "host.docker.internal:host-gateway" - restart: "no" + # No restart policy: a quick-tunnel restart mints a new URL and strands + # EXT_PROXY_URL. Named tunnel (TUNNEL_ARGS) is stable — see docs/cloudflared.md. diff --git a/docs/cloudflared.md b/docs/cloudflared.md index 699850f..2518c81 100644 --- a/docs/cloudflared.md +++ b/docs/cloudflared.md @@ -112,7 +112,7 @@ on it. This is required for the Coston2 FCC registration path: 2. Add a public hostname pointing at `http://host.docker.internal:6674`. 3. In `.env`: ``` - TUNNEL_ARGS=tunnel --no-autoupdate run --token eyJhIjoi... + TUNNEL_ARGS=run --token eyJhIjoi... EXT_PROXY_URL=https://tee.yourdomain.com ``` 4. `docker compose -f docker-compose.cloudflared.yaml up -d` — same command, named mode now. From 870d323acf41dd521c3e9c40a661569b057765d2 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 15:07:48 +0200 Subject: [PATCH 060/107] fix: enforce stable Coston2 FCC machine setup --- .github/workflows/ci.yml | 44 ++++++++++++++++++++++++++-- docs/cloudflared.md | 17 +++++++---- docs/coston2-live-runbook.md | 8 +++-- docs/source-snapshot-2026-08-13.yaml | 8 +++-- 4 files changed, 65 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad00101..648d7f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -264,6 +264,24 @@ jobs: test -n "$EXT_PROXY_URL" test -n "$EXT_PROXY_HOST_URL" test -n "$TUNNEL_ARGS" + case "$EXT_PROXY_URL" in + https://*) ;; + *) echo 'FCC setup requires a public HTTPS EXT_PROXY_URL' >&2; exit 1 ;; + esac + case "$EXT_PROXY_URL" in + *trycloudflare.com*|*ngrok*|*loca.lt*) + echo 'FCC setup refuses a rotating quick-tunnel URL; use a stable named tunnel hostname' >&2 + exit 1 + ;; + esac + [[ "$TUNNEL_ARGS" == run\ --token\ * ]] || { + echo 'FCC setup requires TUNNEL_ARGS in the form: run --token ' >&2 + exit 1 + } + [[ "$TUNNEL_ARGS" != *--url* ]] || { + echo 'FCC setup refuses quick-tunnel --url arguments for a registered machine' >&2 + exit 1 + } test -n "$INDEXER_DB_HOST" test -n "$INDEXER_DB_NAME" test -n "$INDEXER_DB_USER" @@ -328,13 +346,35 @@ jobs: shell: bash run: | set -euo pipefail + proxy_info="$(curl --fail --silent --show-error --max-time 20 "$EXT_PROXY_URL/info")" + proxy_extension_id="$(jq -r '.extensionId // .machineData.extensionId // empty' <<<"$proxy_info")" + test "$proxy_extension_id" = "$EXTENSION_ID" || { + echo "extension proxy /info is bound to '$proxy_extension_id', expected '$EXTENSION_ID'" >&2 + exit 1 + } REGISTER_TEE_COMMAND=rRap ./scripts/post-build.sh (cd tools && go run ./cmd/query-tee \ -rpc "$CHAIN_URL" \ -reg 0x1a9C4A0f9D76c0b1D91d22E24E573a9b377618aE \ -ext 66188) | tee "$RUNNER_TEMP/concord-tee-query.txt" - grep -q 'Active TEEs for extensionId=66188' "$RUNNER_TEMP/concord-tee-query.txt" - echo 'Coston2 FCC scaffold registered one development TEE machine.' >> "$GITHUB_STEP_SUMMARY" + active_count="$(grep -Ec '^[[:space:]]+[0-9]+: 0x[0-9a-fA-F]{40} url="https://' "$RUNNER_TEMP/concord-tee-query.txt" || true)" + test "$active_count" = 1 || { + echo "expected exactly one active Coston2 TEE machine for extension 66188, found $active_count" >&2 + exit 1 + } + tee_id="$(sed -n 's/^[[:space:]]*[0-9]*: \(0x[0-9a-fA-F]\{40\}\) url=.*/\1/p' "$RUNNER_TEMP/concord-tee-query.txt" | head -n1)" + test -n "$tee_id" + (cd tools && go run ./cmd/query-tee \ + -rpc "$CHAIN_URL" \ + -reg 0x1a9C4A0f9D76c0b1D91d22E24E573a9b377618aE \ + "$tee_id") | tee "$RUNNER_TEMP/concord-tee-machine.txt" + grep -q 'getTeeMachineStatus: 2' "$RUNNER_TEMP/concord-tee-machine.txt" + grep -q 'getTeeMachine:.*url="https://' "$RUNNER_TEMP/concord-tee-machine.txt" + { + echo 'Coston2 FCC scaffold registered exactly one development TEE machine.' + echo "TEE machine status: 2 (PRODUCTION); teeId: $tee_id" + echo 'The machine is valid only while this host and its stable HTTPS URL remain available.' + } >> "$GITHUB_STEP_SUMMARY" - name: Tear down ephemeral FCC services if: always() diff --git a/docs/cloudflared.md b/docs/cloudflared.md index 2518c81..792f2b3 100644 --- a/docs/cloudflared.md +++ b/docs/cloudflared.md @@ -1,7 +1,9 @@ # Cloudflare Tunnel (ngrok replacement) -Exposes the extension proxy's external port (host `6674`) over a public HTTPS URL, so -Flare's TEE infrastructure can reach it. Use instead of ngrok. Compose file: +Exposes the extension proxy's external port (host `6674`, container `6664`) over a public +HTTPS URL, so Flare's TEE infrastructure can reach it. A registered Coston2 machine must +use a stable named-tunnel hostname; rotating quick tunnels are only for local smoke tests +before registration. Compose file: `docker-compose.cloudflared.yaml`. Only testnets need this. On `--chain local` nothing is started — a local devnet reaches @@ -20,7 +22,7 @@ into `.env` as `EXT_PROXY_URL`. `full-setup.sh` calls it in Phase 2, and `post-b | Situation | What `start-services.sh` does | |---|---| | Tunnel already running | Reuses it and adopts its URL — no flag needed | -| No tunnel, `--tunnel` passed | Starts it, waits for the URL, writes `EXT_PROXY_URL` | +| No tunnel, `--tunnel` passed | Starts a quick tunnel for local smoke only; do not register its URL | | No tunnel, no `--tunnel` | Warns; `EXT_PROXY_URL` must already be valid in `.env` | | `TUNNEL_ARGS` set (named tunnel) | Starts it and leaves your `EXT_PROXY_URL` alone | @@ -53,7 +55,7 @@ Two cases that legitimately need their own tunnel, and get their own project: | `--local` (host Go proxy) | `tunnel-local` | Proxy listens on 6664, a different origin — the script handles this | | `prediction_market` | give it its own | Publishes on 6676, cannot share | -## Manual +## Quick tunnel (local smoke only — never register this URL) From the repo root, in Git Bash: @@ -66,11 +68,14 @@ docker compose -f docker-compose.cloudflared.yaml logs cloudflared \ | grep -o 'https://[a-z0-9-]*\.trycloudflare\.com' | tail -1 ``` -Put that URL in `.env` as `EXT_PROXY_URL=`, **then** start the containers. +Put that URL in `.env` as `EXT_PROXY_URL=` only for an unregistered local smoke +test, **then** start the containers. Never use a `trycloudflare.com` or other rotating +quick-tunnel URL in `post-build.sh` for a Coston2 machine; Flare stores the URL onchain. `start-services.sh` blocks on `$EXT_PROXY_URL/info`, so a stale value there is what makes it fail. Stop with `docker compose -f docker-compose.cloudflared.yaml down`. -Skipping the copy-paste — does step 2 and writes the result straight into `.env`: +Skipping the copy-paste — does step 2 and writes the result straight into `.env` for a +local smoke test: ```bash URL=$(docker compose -f docker-compose.cloudflared.yaml logs cloudflared \ diff --git a/docs/coston2-live-runbook.md b/docs/coston2-live-runbook.md index ded4992..6e5237c 100644 --- a/docs/coston2-live-runbook.md +++ b/docs/coston2-live-runbook.md @@ -13,7 +13,10 @@ belongs in the repository. The current development path may use `SIMULATED_TEE=true`. That proves the documented development integration path only; it is not production -hardware-backed TEE evidence. +hardware-backed TEE evidence. The Coston2 machine must still reach status `2` +(`PRODUCTION`) with a fresh availability check, a registered `teeId`, and one +stable public HTTPS URL. The extension proxy receives provider delivery on its +external port `6664` (the scaffold's Docker host mapping is `6674`). ## Required inputs @@ -22,7 +25,7 @@ hardware-backed TEE evidence. | Funded Coston2 deployment key | Pays gas for sender, facility, root, collateral, and lifecycle transactions | | Treasury and provider accounts | Demonstrate actual USDT0 funding from at least two selected providers | | Current Flare scaffold registry configuration | Registers the Concord extension against the live Coston2 FCC path | -| Public HTTPS `EXT_PROXY_URL` | Lets the official proxy route instructions to the Go extension | +| Stable named-tunnel `EXT_PROXY_URL` | Lets the official proxy route instructions to the Go extension; rotating quick tunnels are not valid for registration | | Indexer/proxy configuration | Required by the official scaffold runtime; credentials stay outside Git | | Three signed Concord quote payloads | Demonstrate the private multi-provider input path | | One signed finalize payload | Binds the round, root Accord, policy bounds, and eligible providers | @@ -158,6 +161,7 @@ Record only observed values: - Asset resolver output differs from the centralized config. - The public proxy `/info` response is unreachable or bound to the wrong extension. +- The machine is not status `2` (`PRODUCTION`) or its availability check is stale. - More than one active machine is registered for the extension. - Any quote signature, expiry, fee bound, root binding, or provider eligibility check fails. - A result is replayed, bound to another round/root/extension, or has no verified digest. diff --git a/docs/source-snapshot-2026-08-13.yaml b/docs/source-snapshot-2026-08-13.yaml index a726f01..890ca2f 100644 --- a/docs/source-snapshot-2026-08-13.yaml +++ b/docs/source-snapshot-2026-08-13.yaml @@ -1,5 +1,5 @@ snapshot: - captured_at_utc: 2026-08-12T23:08:10Z + captured_at_utc: 2026-08-13T13:02:02Z freshness: FRESH environment: Coston2 chain_id: 114 @@ -28,6 +28,10 @@ snapshot: - The walkthrough requires a public HTTPS extension-proxy URL and Coston2 indexer database credentials for the local proxy. - The current scaffold path includes pre-build, start-services, post-build, and test phases. - Flare's FAQ identifies Coston2 as the primary dApp testnet and states that FCC is available there while in development. + - FCC development registration requires one active machine in status 2 (PRODUCTION), a fresh availability check, a registered teeId, and a reachable stable public HTTPS extension URL. + - The extension receives provider delivery at the scaffold external proxy port 6664; the Docker host mapping used by this repository is 6674. + - Rotating quick-tunnel URLs are not valid for a registered Coston2 machine; the named tunnel token form is `run --token `. + - The official Coston2 FTDC proxy endpoints are `https://tee-proxy-coston2-1.flare.rocks` and `https://tee-proxy-coston2-2.flare.rocks`; they are normal infrastructure proxies, not Concord's extension endpoint. contradictions: [] concord_decisions: - No product-semantic contradiction was found. @@ -36,6 +40,6 @@ snapshot: live_gate_inputs_still_required: - funded Coston2 deployment/proxy accounts - indexer credentials or an approved current proxy setup - - public HTTPS EXT_PROXY_URL + - stable named-tunnel HTTPS EXT_PROXY_URL (not a rotating quick tunnel) - current extension registration and active TEE-machine evidence - real provider funding, draw, repayment, and explorer receipts From fd1c61b27289731d7eba0e694305c7f3ca79c7ea Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 15:11:55 +0200 Subject: [PATCH 061/107] fix: quote named tunnel env in FCC setup --- .github/workflows/ci.yml | 2 +- docs/cloudflared.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 648d7f5..1bb78a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -293,7 +293,6 @@ jobs: "NORMAL_PROXY_URL=$NORMAL_PROXY_URL" \ "EXT_PROXY_URL=$EXT_PROXY_URL" \ "EXT_PROXY_HOST_URL=$EXT_PROXY_HOST_URL" \ - "TUNNEL_ARGS=$TUNNEL_ARGS" \ "LOCAL_MODE=$LOCAL_MODE" \ "SIMULATED_TEE=$SIMULATED_TEE" \ "PROXY_PRIVATE_KEY=$PROXY_PRIVATE_KEY" \ @@ -303,6 +302,7 @@ jobs: "GOVERNANCE_THRESHOLD=$GOVERNANCE_THRESHOLD" \ "ADDRESSES_FILE=./config/coston2/deployed-addresses.json" \ > .env + printf 'TUNNEL_ARGS=%q\n' "$TUNNEL_ARGS" >> .env printf 'EXTENSION_ID=%s\nINSTRUCTION_SENDER=%s\n' "$EXTENSION_ID" "$INSTRUCTION_SENDER" > config/extension.env printf '%s\n' \ 'redis_port = "redis:6379"' \ diff --git a/docs/cloudflared.md b/docs/cloudflared.md index 792f2b3..d25ef31 100644 --- a/docs/cloudflared.md +++ b/docs/cloudflared.md @@ -117,7 +117,7 @@ on it. This is required for the Coston2 FCC registration path: 2. Add a public hostname pointing at `http://host.docker.internal:6674`. 3. In `.env`: ``` - TUNNEL_ARGS=run --token eyJhIjoi... + TUNNEL_ARGS="run --token eyJhIjoi..." EXT_PROXY_URL=https://tee.yourdomain.com ``` 4. `docker compose -f docker-compose.cloudflared.yaml up -d` — same command, named mode now. From edecc3657d2917b9e12af4e57916079d12477843 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 17:48:25 +0200 Subject: [PATCH 062/107] docs: record governed FCC credential state --- docs/CONCORD_STATUS.md | 7 ++++--- docs/canonical-reconciliation-audit.md | 29 +++++++++++++++++--------- docs/source-snapshot-2026-08-13.yaml | 11 +++++++--- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index f81cad7..dc2c9d8 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -79,9 +79,10 @@ bound to the current token, and its current root round is recorded below. superseded 18-decimal liquidity token. - No FCC quote/finalize instruction, allocation verification, child Accord, draw, settlement, repayment, or restored-capacity receipt has been observed. -- The current FCC path still needs a reachable extension proxy and the - scaffold's current Coston2 indexer database credentials before the machine - registration and instruction flow can run. +- The current Coston2 indexer configuration is now stored as encrypted GitHub + Actions secrets using the current DevHub values. FCC setup still needs a + stable public extension URL and named-tunnel token; no machine registration + or instruction flow has been dispatched. ## Privacy truth diff --git a/docs/canonical-reconciliation-audit.md b/docs/canonical-reconciliation-audit.md index 318feaf..b3ad2bb 100644 --- a/docs/canonical-reconciliation-audit.md +++ b/docs/canonical-reconciliation-audit.md @@ -1,6 +1,6 @@ # Concord canonical reconciliation audit -Status: completed for the durable `agent/concord-rebuild` checkpoint on 2026-08-12. +Status: refreshed for the durable `agent/concord-rebuild` checkpoint on 2026-08-13. This audit compares the implementation with the following authority order: @@ -9,11 +9,12 @@ This audit compares the implementation with the following authority order: 3. Concord Whitepaper — architectural thesis and future direction. 4. Current official Flare sources — changing implementation facts. -The whitepaper and Product Definition Addendum were read in full. The -Foundry Skills Pack named in the handoff was not present in the available -filesystem during this audit, so no claim is made that its package-specific -instructions were loaded. The repository's durable source and current -official Flare-controlled sources were used instead. +The whitepaper and Product Definition Addendum were read in full. The supplied +Foundry Skills Pack is now durably installed and its Product Foundry 0.4.3, +Research Foundry 0.2.0, Experiment Foundry 0.1.0, Interface Foundry 0.3.0, +Demo Foundry 0.5.3, and Concord FCC 0.1.0 instructions are loaded as applicable. +The repository's durable source and current official Flare-controlled sources +remain the implementation truth for changing Flare facts. ## Verdict @@ -25,8 +26,8 @@ lending pool, matching engine, or isolated transaction system. The MVP is not complete. The outstanding blockers are evidence and execution gates, not a product reinterpretation: -- no live Concord deployment, FCC registration, funded Coston2 accounts, - instruction IDs, settlement receipts, or explorer evidence are recorded; +- no live FCC machine, instruction IDs, settlement receipts, or complete + explorer evidence are recorded; - the current runner does not contain `forge` or Go, so this audit could not re-run the Solidity and Go suites; - the full frontend remains intentionally unimplemented until the economic and @@ -47,7 +48,7 @@ gates, not a product reinterpretation: | API / SDK / MCP / CLI | PASS | Read surfaces share one read model; write surfaces prepare unsigned intents and do not custody, sign, broadcast, or become the verifier. | Preserve. | | Frontend design | PASS / DESIGN ONLY | The design is relationship-first, progressive-disclosure, role-aware, and explicit about observed versus unavailable state. | Implement only after the live economic path. | | Future authority | GAP / INTENTIONAL | General parent-delegated authority and external evidence fields are not yet modeled. | Roadmap seam; do not add to this MVP. | -| Live Coston2 proof | BLOCKER | No deployment or end-to-end evidence is recorded. | Required before completion claims. | +| Live Coston2 proof | BLOCKER | Facility/root/provider funding evidence exists, but no active FCC machine, instruction IDs, or settlement receipts are recorded. | Required before completion claims. | | Local validation | BLOCKER | `forge` and the recorded Go toolchain are absent from this runner. | Restore toolchain, then rerun the suites. | ## Implementation evidence @@ -148,6 +149,15 @@ These items are real follow-up work, but none authorizes broadening the MVP: - No Solidity or product primitive was renamed or broadened as part of this audit. +## Current FCC credential gate + +The current DevHub guide was refreshed on 2026-08-13. Its Coston2 indexer +configuration is host `34.38.42.208`, port `3306`, database `indexer`; the +username and password are stored only as encrypted GitHub Actions secrets. +The repository does not record their values. The stable public extension URL +and named-tunnel token are still absent, so the guarded FCC setup workflow has +not been dispatched. + ## Validation record Completed during this audit: @@ -204,4 +214,3 @@ restore toolchain → record signed evidence and transaction hashes → then implement and QA the frontend ``` - diff --git a/docs/source-snapshot-2026-08-13.yaml b/docs/source-snapshot-2026-08-13.yaml index 890ca2f..e7990cf 100644 --- a/docs/source-snapshot-2026-08-13.yaml +++ b/docs/source-snapshot-2026-08-13.yaml @@ -1,5 +1,5 @@ snapshot: - captured_at_utc: 2026-08-13T13:02:02Z + captured_at_utc: 2026-08-13T15:46:54Z freshness: FRESH environment: Coston2 chain_id: 114 @@ -32,14 +32,19 @@ snapshot: - The extension receives provider delivery at the scaffold external proxy port 6664; the Docker host mapping used by this repository is 6674. - Rotating quick-tunnel URLs are not valid for a registered Coston2 machine; the named tunnel token form is `run --token `. - The official Coston2 FTDC proxy endpoints are `https://tee-proxy-coston2-1.flare.rocks` and `https://tee-proxy-coston2-2.flare.rocks`; they are normal infrastructure proxies, not Concord's extension endpoint. - contradictions: [] + - The current DevHub walkthrough specifies the Coston2 indexer endpoint as host `34.38.42.208`, port `3306`, database `indexer`; credentials remain secret inputs. + contradictions: + - id: FCC-QUICK-TUNNEL-SCOPE + statement: The DevHub getting-started walkthrough still documents quick tunnels for development and says to update EXT_PROXY_URL when a quick-tunnel URL changes. + current_operational_constraint: A registered Coston2 machine must use one stable public HTTPS URL; restarting a rotating tunnel strands the onchain machine record and can create a new simulated TEE identity. + resolution: Concord permits quick tunnels only for unregistered smoke tests and requires a stable named-tunnel URL/token for the guarded registration workflow. concord_decisions: - No product-semantic contradiction was found. - Simulated TEE evidence remains development-path evidence, not production hardware-backed confidentiality. - FDC remains an evidence seam and is not added to the MVP. live_gate_inputs_still_required: - funded Coston2 deployment/proxy accounts - - indexer credentials or an approved current proxy setup - stable named-tunnel HTTPS EXT_PROXY_URL (not a rotating quick tunnel) + - stable named-tunnel token in CONCORD_FCC_TUNNEL_ARGS - current extension registration and active TEE-machine evidence - real provider funding, draw, repayment, and explorer receipts From a571ca3ffc5d47b25610dd627c760f562d78f093 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 19:03:11 +0200 Subject: [PATCH 063/107] feat: add Cloudflare FCC development ingress relay --- docs/cloudflared.md | 24 ++++++ docs/coston2-live-runbook.md | 5 ++ infra/cloudflare/fcc-ingress/README.md | 40 +++++++++ infra/cloudflare/fcc-ingress/src/index.ts | 91 +++++++++++++++++++++ infra/cloudflare/fcc-ingress/wrangler.jsonc | 7 ++ 5 files changed, 167 insertions(+) create mode 100644 infra/cloudflare/fcc-ingress/README.md create mode 100644 infra/cloudflare/fcc-ingress/src/index.ts create mode 100644 infra/cloudflare/fcc-ingress/wrangler.jsonc diff --git a/docs/cloudflared.md b/docs/cloudflared.md index d25ef31..77a5479 100644 --- a/docs/cloudflared.md +++ b/docs/cloudflared.md @@ -125,6 +125,30 @@ on it. This is required for the Coston2 FCC registration path: `EXT_PROXY_URL` and the on-chain machine record then survive every restart, and `restart: unless-stopped` becomes safe to add to the service. +## Hackathon fallback when Workers VPC is unavailable + +If the Cloudflare account cannot grant the current user Connectivity Directory +Admin, do not guess at a VPC service or modify an unrelated tunnel. The +development fallback is a separately deployed Worker at the account's +workers.dev hostname, forwarding the narrow FCC proxy paths to a public HTTPS +GitHub Codespaces port. The implementation is in +infra/cloudflare/fcc-ingress/. + +This preserves the official FCE runtime and Concord semantics, but changes the +network ingress boundary. It is suitable only for a controlled hackathon run: + +1. Start the Concord Coston2 proxy in the persistent Codespace. +2. Make only host port 6674 public. +3. Set the Worker's CONCORD_UPSTREAM_URL secret to the Codespaces HTTPS URL. +4. Verify /info, /instruction, and /action/status through the Worker. +5. Use the Worker URL only with an explicit development-path privacy and + availability label. + +Codespaces public ports are anonymous internet endpoints and revert to private +after a Codespace restart. Reapply the port visibility and re-run the health +checks before every FCC session. This fallback must not be presented as a +production hardware-backed FCC or permanent institutional ingress. + ## Know this | | | diff --git a/docs/coston2-live-runbook.md b/docs/coston2-live-runbook.md index 6e5237c..d46777a 100644 --- a/docs/coston2-live-runbook.md +++ b/docs/coston2-live-runbook.md @@ -30,6 +30,11 @@ external port `6664` (the scaffold's Docker host mapping is `6674`). | Three signed Concord quote payloads | Demonstrate the private multi-provider input path | | One signed finalize payload | Binds the round, root Accord, policy bounds, and eligible providers | +For the immediate hackathon fallback, the stable Worker development relay may +replace the named tunnel URL only after both the Worker and its Codespaces +upstream return the same proxy /info binding. This is an ingress substitution, +not a change to the FCE runtime or Concord product semantics. + Never paste credentials into issues, logs, fixtures, or committed config. Use ignored environment files or the deployment system secret store. diff --git a/infra/cloudflare/fcc-ingress/README.md b/infra/cloudflare/fcc-ingress/README.md new file mode 100644 index 0000000..16e47a0 --- /dev/null +++ b/infra/cloudflare/fcc-ingress/README.md @@ -0,0 +1,40 @@ +# Concord FCC development ingress + +This Worker is a narrow HTTPS relay for the Concord FCC development path when +the account cannot use Cloudflare Workers VPC because the required +Connectivity Directory Admin membership cannot be assigned to the current +user. + +It does not replace the official Flare FCE scaffold, tee-node, tee-proxy, +registry, machine registration, signature checks, or Concord economic +semantics. It only forwards the proxy endpoints that the FCC setup needs: + +- GET /info +- GET /health +- POST /instruction +- GET /metrics +- GET /action/status... + +The upstream URL is intentionally a secret so it is not committed. For this +hackathon fallback it points at the public HTTPS URL for the persistent +Codespaces port that maps to host port 6674. + +## Deploy + +From this directory, with a current Cloudflare API token and account ID: + + npx wrangler deploy + printf '%s' 'https://-6674.app.github.dev' | npx wrangler secret put CONCORD_UPSTREAM_URL + +The stable Worker URL is: + + https://concord-fcc-ingress.microcosm.workers.dev + +Verify the relay before using it for any FCC operation: + + curl -fsS https://concord-fcc-ingress.microcosm.workers.dev/info + +This is a development/demo ingress boundary. A Codespaces public port is +anonymous internet access and becomes private again when the Codespace is +restarted, so the port must be re-forwarded and re-publicized after every +restart. Do not describe this fallback as a production FCC deployment. diff --git a/infra/cloudflare/fcc-ingress/src/index.ts b/infra/cloudflare/fcc-ingress/src/index.ts new file mode 100644 index 0000000..7a2555e --- /dev/null +++ b/infra/cloudflare/fcc-ingress/src/index.ts @@ -0,0 +1,91 @@ +const ALLOWED_METHODS = { + "/info": new Set(["GET", "HEAD"]), + "/health": new Set(["GET", "HEAD"]), + "/instruction": new Set(["POST"]), + "/metrics": new Set(["GET", "HEAD"]), +}; + +function isAllowedPath(pathname) { + return ( + pathname in ALLOWED_METHODS || + pathname === "/action/status" || + pathname.startsWith("/action/status/") + ); +} + +function json(status, body) { + return new Response(JSON.stringify(body), { + status, + headers: { + "content-type": "application/json; charset=utf-8", + "cache-control": "no-store", + }, + }); +} + +export default { + async fetch(request, env) { + const incoming = new URL(request.url); + + if (!isAllowedPath(incoming.pathname)) { + return json(404, { error: "not_found" }); + } + + const allowedMethods = + ALLOWED_METHODS[incoming.pathname] ?? new Set(["GET", "HEAD"]); + if (!allowedMethods.has(request.method)) { + return json(405, { error: "method_not_allowed" }); + } + + if (!env.CONCORD_UPSTREAM_URL) { + return json(503, { error: "upstream_not_configured" }); + } + + let upstream; + try { + upstream = new URL(env.CONCORD_UPSTREAM_URL); + } catch { + return json(503, { error: "upstream_invalid" }); + } + + if (upstream.protocol !== "https:") { + return json(503, { error: "upstream_must_use_https" }); + } + + upstream.pathname = incoming.pathname; + upstream.search = incoming.search; + + const headers = new Headers(request.headers); + headers.delete("host"); + headers.delete("content-length"); + headers.set("cache-control", "no-store"); + headers.set("x-concord-ingress", "cloudflare-workers-development-relay"); + + try { + const response = await fetch(upstream, { + method: request.method, + headers, + body: + request.method === "GET" || request.method === "HEAD" + ? undefined + : request.body, + redirect: "manual", + }); + + const responseHeaders = new Headers(response.headers); + responseHeaders.set("cache-control", "no-store"); + responseHeaders.set( + "x-concord-ingress", + "cloudflare-workers-development-relay", + ); + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: responseHeaders, + }); + } catch { + return json(502, { error: "upstream_unreachable" }); + } + }, +}; diff --git a/infra/cloudflare/fcc-ingress/wrangler.jsonc b/infra/cloudflare/fcc-ingress/wrangler.jsonc new file mode 100644 index 0000000..3e88774 --- /dev/null +++ b/infra/cloudflare/fcc-ingress/wrangler.jsonc @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "concord-fcc-ingress", + "main": "src/index.ts", + "compatibility_date": "2026-08-13", + "workers_dev": true +} From 385af9224c976ac9279b007c025ac2d9fdcf8585 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 19:09:40 +0200 Subject: [PATCH 064/107] ci: prepare disposable Coston2 Codespace runtime --- .../workflows/coston2-codespace-runtime.yml | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 .github/workflows/coston2-codespace-runtime.yml diff --git a/.github/workflows/coston2-codespace-runtime.yml b/.github/workflows/coston2-codespace-runtime.yml new file mode 100644 index 0000000..79e5d95 --- /dev/null +++ b/.github/workflows/coston2-codespace-runtime.yml @@ -0,0 +1,115 @@ +name: Concord Coston2 Codespace runtime + +on: + workflow_dispatch: + inputs: + codespace_name: + description: Existing Codespace to prepare + required: true + type: string + default: friendly-engine-r7r5qrwvvrj5c5777 + +permissions: + contents: read + +jobs: + prepare: + name: Prepare official FCC scaffold in Codespace + runs-on: ubuntu-latest + env: + CODESPACE_NAME: ${{ inputs.codespace_name }} + GH_TOKEN: ${{ secrets.CONCORD_CODESPACE_PAT }} + PROXY_PRIVATE_KEY: ${{ secrets.CONCORD_FCC_PROXY_PRIVATE_KEY }} + INDEXER_DB_HOST: ${{ secrets.CONCORD_FCC_INDEXER_DB_HOST }} + INDEXER_DB_NAME: ${{ secrets.CONCORD_FCC_INDEXER_DB_NAME }} + INDEXER_DB_USER: ${{ secrets.CONCORD_FCC_INDEXER_DB_USER }} + INDEXER_DB_PASSWORD: ${{ secrets.CONCORD_FCC_INDEXER_DB_PASSWORD }} + steps: + - name: Validate temporary runtime inputs + shell: bash + run: | + set -euo pipefail + test -n "$CODESPACE_NAME" + test -n "$GH_TOKEN" + test -n "$PROXY_PRIVATE_KEY" + test -n "$INDEXER_DB_HOST" + test -n "$INDEXER_DB_NAME" + test -n "$INDEXER_DB_USER" + test -n "$INDEXER_DB_PASSWORD" + gh --version + gh codespace list --json name,state + | jq -e --arg name "$CODESPACE_NAME" '.[] | select(.name == $name) | [.name,.state] | @tsv' + + - name: Write ignored scaffold config and start runtime + shell: bash + run: | + set -euo pipefail + + shell_quote() { + printf '%q' "$1" + } + + proxy_key_q="$(shell_quote "$PROXY_PRIVATE_KEY")" + db_host_q="$(shell_quote "$INDEXER_DB_HOST")" + db_name_q="$(shell_quote "$INDEXER_DB_NAME")" + db_user_q="$(shell_quote "$INDEXER_DB_USER")" + db_password_q="$(shell_quote "$INDEXER_DB_PASSWORD")" + + { + printf '%s\n' 'set -euo pipefail' + printf '%s\n' 'PROJECT=/workspaces/Concord' + printf '%s\n' 'umask 077' + printf '%s\n' 'mkdir -p "$PROJECT/config/proxy"' + printf '%s\n' 'cat > "$PROJECT/.env" <<'\''CONCORD_ENV'\'' + printf '%s\n' 'CHAIN=coston2' + printf '%s\n' 'CHAIN_URL=https://coston2-api.flare.network/ext/C/rpc' + printf '%s\n' 'LOCAL_MODE=false' + printf '%s\n' 'SIMULATED_TEE=true' + printf '%s\n' 'EXTENSION_ID=0x000000000000000000000000000000000000000000000000000000000001028c' + printf '%s\n' 'INITIAL_OWNER=0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3' + printf '%s\n' 'GOVERNANCE_SIGNERS=0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3' + printf '%s\n' 'GOVERNANCE_THRESHOLD=1' + printf '%s\n' 'EXT_PROXY_URL=http://localhost:6674' + printf '%s\n' 'PROXY_PRIVATE_KEY='"$proxy_key_q" + printf '%s\n' 'CONCORD_ENV' + printf '%s\n' 'cat > "$PROJECT/config/extension.env" <<'\''EXTENSION_ENV'\'' + printf '%s\n' 'EXTENSION_ID=0x000000000000000000000000000000000000000000000000000000000001028c' + printf '%s\n' 'INSTRUCTION_SENDER=0x574b523eA944EFe9143AF9d6c46bfA925beE2968' + printf '%s\n' 'EXTENSION_ENV' + printf '%s\n' 'cat > "$PROJECT/config/proxy/extension_proxy.coston2.docker.toml" <<'\''PROXY_TOML'\'' + printf '%s\n' 'redis_port = "redis:6379"' + printf '%s\n' 'private_key_variable = "PROXY_PRIVATE_KEY"' + printf '%s\n' 'initial_signing_policy_offset = 2' + printf '%s\n' 'signing_policy_fetch_interval = "20s"' + printf '%s\n' '' + printf '%s\n' 'chain_id = 114' + printf '%s\n' '' + printf '%s\n' '[db]' + printf '%s\n' 'host = '"$db_host_q" + printf '%s\n' 'port = 3306' + printf '%s\n' 'database = '"$db_name_q" + printf '%s\n' 'username = '"$db_user_q" + printf '%s\n' 'password = '"$db_password_q" + printf '%s\n' 'log_queries = false' + printf '%s\n' '' + printf '%s\n' '[addresses]' + printf '%s\n' 'flare_systems_manager = "0xA90Db6D10F856799b10ef2A77EBCbF460aC71e52"' + printf '%s\n' 'relay = "0xa10B672D1c62e5457b17af63d4302add6A99d7dE"' + printf '%s\n' 'voter_registry = "0x6a0AF07b7972177B176d3D422555cbc98DfDe914"' + printf '%s\n' '' + printf '%s\n' '[ports]' + printf '%s\n' 'internal = "6663"' + printf '%s\n' 'external = "6664"' + printf '%s\n' '' + printf '%s\n' '[info_timing]' + printf '%s\n' 'cycle_internal = "10s"' + printf '%s\n' 'cycle_queue_response_wait = "2s"' + printf '%s\n' '' + printf '%s\n' '[voting]' + printf '%s\n' 'proposal_expiration = "12s"' + printf '%s\n' 'max_pending_request = 10000' + printf '%s\n' 'PROXY_TOML' + printf '%s\n' 'cd "$PROJECT"' + printf '%s\n' './scripts/start-services.sh --chain coston2' + printf '%s\n' 'gh codespace ports visibility 6674:public -c "$CODESPACE_NAME"' + printf '%s\n' 'curl --fail --silent --show-error http://localhost:6674/info' + } | gh codespace ssh -c "$CODESPACE_NAME" -- bash -s From e8c1ea57d2591a9405b0322e427eac8346281f7e Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 19:15:15 +0200 Subject: [PATCH 065/107] ci: run disposable FCC runtime from existing workflow --- .github/workflows/ci.yml | 22 +++- .../workflows/coston2-codespace-runtime.yml | 115 ------------------ scripts/prepare-coston2-codespace.sh | 87 +++++++++++++ 3 files changed, 108 insertions(+), 116 deletions(-) delete mode 100644 .github/workflows/coston2-codespace-runtime.yml create mode 100644 scripts/prepare-coston2-codespace.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bb78a7..2b26261 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,6 +112,26 @@ jobs: run: bash scripts/coston2-preflight.sh --offline + codespace_fcc_runtime: + name: Coston2 FCC runtime in disposable Codespace + if: github.event_name == 'workflow_dispatch' && inputs.setup_fcc == true && github.ref == 'refs/heads/agent/concord-rebuild' + runs-on: ubuntu-latest + env: + CODESPACE_NAME: friendly-engine-r7r5qrwvvrj5c5777 + GH_TOKEN: ${{ secrets.CONCORD_CODESPACE_PAT }} + PROXY_PRIVATE_KEY: ${{ secrets.CONCORD_FCC_PROXY_PRIVATE_KEY }} + INDEXER_DB_HOST: ${{ secrets.CONCORD_FCC_INDEXER_DB_HOST }} + INDEXER_DB_NAME: ${{ secrets.CONCORD_FCC_INDEXER_DB_NAME }} + INDEXER_DB_USER: ${{ secrets.CONCORD_FCC_INDEXER_DB_USER }} + INDEXER_DB_PASSWORD: ${{ secrets.CONCORD_FCC_INDEXER_DB_PASSWORD }} + steps: + - name: Check out requested ref + uses: actions/checkout@v4 + - name: Prepare and start official scaffold + shell: bash + run: bash scripts/prepare-coston2-codespace.sh + + live_sender_deploy: name: Coston2 sender deployment (manual only) if: github.event_name == 'workflow_dispatch' && inputs.deploy_sender == true @@ -219,7 +239,7 @@ jobs: live_fcc_setup: name: Coston2 FCC scaffold setup (manual only) - if: github.event_name == 'workflow_dispatch' && inputs.setup_fcc == true + if: github.event_name == 'workflow_dispatch' && inputs.setup_fcc == true && github.ref != 'refs/heads/agent/concord-rebuild' concurrency: group: concord-coston2-fcc-setup cancel-in-progress: false diff --git a/.github/workflows/coston2-codespace-runtime.yml b/.github/workflows/coston2-codespace-runtime.yml deleted file mode 100644 index 79e5d95..0000000 --- a/.github/workflows/coston2-codespace-runtime.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: Concord Coston2 Codespace runtime - -on: - workflow_dispatch: - inputs: - codespace_name: - description: Existing Codespace to prepare - required: true - type: string - default: friendly-engine-r7r5qrwvvrj5c5777 - -permissions: - contents: read - -jobs: - prepare: - name: Prepare official FCC scaffold in Codespace - runs-on: ubuntu-latest - env: - CODESPACE_NAME: ${{ inputs.codespace_name }} - GH_TOKEN: ${{ secrets.CONCORD_CODESPACE_PAT }} - PROXY_PRIVATE_KEY: ${{ secrets.CONCORD_FCC_PROXY_PRIVATE_KEY }} - INDEXER_DB_HOST: ${{ secrets.CONCORD_FCC_INDEXER_DB_HOST }} - INDEXER_DB_NAME: ${{ secrets.CONCORD_FCC_INDEXER_DB_NAME }} - INDEXER_DB_USER: ${{ secrets.CONCORD_FCC_INDEXER_DB_USER }} - INDEXER_DB_PASSWORD: ${{ secrets.CONCORD_FCC_INDEXER_DB_PASSWORD }} - steps: - - name: Validate temporary runtime inputs - shell: bash - run: | - set -euo pipefail - test -n "$CODESPACE_NAME" - test -n "$GH_TOKEN" - test -n "$PROXY_PRIVATE_KEY" - test -n "$INDEXER_DB_HOST" - test -n "$INDEXER_DB_NAME" - test -n "$INDEXER_DB_USER" - test -n "$INDEXER_DB_PASSWORD" - gh --version - gh codespace list --json name,state + | jq -e --arg name "$CODESPACE_NAME" '.[] | select(.name == $name) | [.name,.state] | @tsv' - - - name: Write ignored scaffold config and start runtime - shell: bash - run: | - set -euo pipefail - - shell_quote() { - printf '%q' "$1" - } - - proxy_key_q="$(shell_quote "$PROXY_PRIVATE_KEY")" - db_host_q="$(shell_quote "$INDEXER_DB_HOST")" - db_name_q="$(shell_quote "$INDEXER_DB_NAME")" - db_user_q="$(shell_quote "$INDEXER_DB_USER")" - db_password_q="$(shell_quote "$INDEXER_DB_PASSWORD")" - - { - printf '%s\n' 'set -euo pipefail' - printf '%s\n' 'PROJECT=/workspaces/Concord' - printf '%s\n' 'umask 077' - printf '%s\n' 'mkdir -p "$PROJECT/config/proxy"' - printf '%s\n' 'cat > "$PROJECT/.env" <<'\''CONCORD_ENV'\'' - printf '%s\n' 'CHAIN=coston2' - printf '%s\n' 'CHAIN_URL=https://coston2-api.flare.network/ext/C/rpc' - printf '%s\n' 'LOCAL_MODE=false' - printf '%s\n' 'SIMULATED_TEE=true' - printf '%s\n' 'EXTENSION_ID=0x000000000000000000000000000000000000000000000000000000000001028c' - printf '%s\n' 'INITIAL_OWNER=0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3' - printf '%s\n' 'GOVERNANCE_SIGNERS=0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3' - printf '%s\n' 'GOVERNANCE_THRESHOLD=1' - printf '%s\n' 'EXT_PROXY_URL=http://localhost:6674' - printf '%s\n' 'PROXY_PRIVATE_KEY='"$proxy_key_q" - printf '%s\n' 'CONCORD_ENV' - printf '%s\n' 'cat > "$PROJECT/config/extension.env" <<'\''EXTENSION_ENV'\'' - printf '%s\n' 'EXTENSION_ID=0x000000000000000000000000000000000000000000000000000000000001028c' - printf '%s\n' 'INSTRUCTION_SENDER=0x574b523eA944EFe9143AF9d6c46bfA925beE2968' - printf '%s\n' 'EXTENSION_ENV' - printf '%s\n' 'cat > "$PROJECT/config/proxy/extension_proxy.coston2.docker.toml" <<'\''PROXY_TOML'\'' - printf '%s\n' 'redis_port = "redis:6379"' - printf '%s\n' 'private_key_variable = "PROXY_PRIVATE_KEY"' - printf '%s\n' 'initial_signing_policy_offset = 2' - printf '%s\n' 'signing_policy_fetch_interval = "20s"' - printf '%s\n' '' - printf '%s\n' 'chain_id = 114' - printf '%s\n' '' - printf '%s\n' '[db]' - printf '%s\n' 'host = '"$db_host_q" - printf '%s\n' 'port = 3306' - printf '%s\n' 'database = '"$db_name_q" - printf '%s\n' 'username = '"$db_user_q" - printf '%s\n' 'password = '"$db_password_q" - printf '%s\n' 'log_queries = false' - printf '%s\n' '' - printf '%s\n' '[addresses]' - printf '%s\n' 'flare_systems_manager = "0xA90Db6D10F856799b10ef2A77EBCbF460aC71e52"' - printf '%s\n' 'relay = "0xa10B672D1c62e5457b17af63d4302add6A99d7dE"' - printf '%s\n' 'voter_registry = "0x6a0AF07b7972177B176d3D422555cbc98DfDe914"' - printf '%s\n' '' - printf '%s\n' '[ports]' - printf '%s\n' 'internal = "6663"' - printf '%s\n' 'external = "6664"' - printf '%s\n' '' - printf '%s\n' '[info_timing]' - printf '%s\n' 'cycle_internal = "10s"' - printf '%s\n' 'cycle_queue_response_wait = "2s"' - printf '%s\n' '' - printf '%s\n' '[voting]' - printf '%s\n' 'proposal_expiration = "12s"' - printf '%s\n' 'max_pending_request = 10000' - printf '%s\n' 'PROXY_TOML' - printf '%s\n' 'cd "$PROJECT"' - printf '%s\n' './scripts/start-services.sh --chain coston2' - printf '%s\n' 'gh codespace ports visibility 6674:public -c "$CODESPACE_NAME"' - printf '%s\n' 'curl --fail --silent --show-error http://localhost:6674/info' - } | gh codespace ssh -c "$CODESPACE_NAME" -- bash -s diff --git a/scripts/prepare-coston2-codespace.sh b/scripts/prepare-coston2-codespace.sh new file mode 100644 index 0000000..9749669 --- /dev/null +++ b/scripts/prepare-coston2-codespace.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Prepare the existing disposable Codespace with the ignored Coston2 FCC +# runtime configuration, start the official scaffold, and expose only port +# 6674. Secrets are streamed from the caller to the Codespace and never +# written to the repository. +set -euo pipefail + +: "${CODESPACE_NAME:?CODESPACE_NAME must be set}" +: "${GH_TOKEN:?GH_TOKEN must be set}" +: "${PROXY_PRIVATE_KEY:?PROXY_PRIVATE_KEY must be set}" +: "${INDEXER_DB_HOST:?INDEXER_DB_HOST must be set}" +: "${INDEXER_DB_NAME:?INDEXER_DB_NAME must be set}" +: "${INDEXER_DB_USER:?INDEXER_DB_USER must be set}" +: "${INDEXER_DB_PASSWORD:?INDEXER_DB_PASSWORD must be set}" + +shell_quote() { + printf '%q' "$1" +} + +proxy_key_q="$(shell_quote "$PROXY_PRIVATE_KEY")" +db_host_q="$(shell_quote "$INDEXER_DB_HOST")" +db_name_q="$(shell_quote "$INDEXER_DB_NAME")" +db_user_q="$(shell_quote "$INDEXER_DB_USER")" +db_password_q="$(shell_quote "$INDEXER_DB_PASSWORD")" + +gh codespace list --json name,state \ + | jq -e --arg name "$CODESPACE_NAME" '.[] | select(.name == $name) | [.name,.state] | @tsv' + +{ + printf '%s\n' 'set -euo pipefail' + printf '%s\n' 'PROJECT=/workspaces/Concord' + printf '%s\n' 'umask 077' + printf '%s\n' 'mkdir -p "$PROJECT/config/proxy"' + printf '%s\n' "cat > /workspaces/Concord/.env <<'CONCORD_ENV'" + printf '%s\n' 'CHAIN=coston2' + printf '%s\n' 'CHAIN_URL=https://coston2-api.flare.network/ext/C/rpc' + printf '%s\n' 'LOCAL_MODE=false' + printf '%s\n' 'SIMULATED_TEE=true' + printf '%s\n' 'EXTENSION_ID=0x000000000000000000000000000000000000000000000000000000000001028c' + printf '%s\n' 'INITIAL_OWNER=0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3' + printf '%s\n' 'GOVERNANCE_SIGNERS=0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3' + printf '%s\n' 'GOVERNANCE_THRESHOLD=1' + printf '%s\n' 'EXT_PROXY_URL=http://localhost:6674' + printf '%s\n' 'PROXY_PRIVATE_KEY='"$proxy_key_q" + printf '%s\n' 'CONCORD_ENV' + printf '%s\n' "cat > /workspaces/Concord/config/extension.env <<'EXTENSION_ENV'" + printf '%s\n' 'EXTENSION_ID=0x000000000000000000000000000000000000000000000000000000000001028c' + printf '%s\n' 'INSTRUCTION_SENDER=0x574b523eA944EFe9143AF9d6c46bfA925beE2968' + printf '%s\n' 'EXTENSION_ENV' + printf '%s\n' "cat > /workspaces/Concord/config/proxy/extension_proxy.coston2.docker.toml <<'PROXY_TOML'" + printf '%s\n' 'redis_port = "redis:6379"' + printf '%s\n' 'private_key_variable = "PROXY_PRIVATE_KEY"' + printf '%s\n' 'initial_signing_policy_offset = 2' + printf '%s\n' 'signing_policy_fetch_interval = "20s"' + printf '%s\n' '' + printf '%s\n' 'chain_id = 114' + printf '%s\n' '' + printf '%s\n' '[db]' + printf '%s\n' 'host = '"$db_host_q" + printf '%s\n' 'port = 3306' + printf '%s\n' 'database = '"$db_name_q" + printf '%s\n' 'username = '"$db_user_q" + printf '%s\n' 'password = '"$db_password_q" + printf '%s\n' 'log_queries = false' + printf '%s\n' '' + printf '%s\n' '[addresses]' + printf '%s\n' 'flare_systems_manager = "0xA90Db6D10F856799b10ef2A77EBCbF460aC71e52"' + printf '%s\n' 'relay = "0xa10B672D1c62e5457b17af63d4302add6A99d7dE"' + printf '%s\n' 'voter_registry = "0x6a0AF07b7972177B176d3D422555cbc98DfDe914"' + printf '%s\n' '' + printf '%s\n' '[ports]' + printf '%s\n' 'internal = "6663"' + printf '%s\n' 'external = "6664"' + printf '%s\n' '' + printf '%s\n' '[info_timing]' + printf '%s\n' 'cycle_internal = "10s"' + printf '%s\n' 'cycle_queue_response_wait = "2s"' + printf '%s\n' '' + printf '%s\n' '[voting]' + printf '%s\n' 'proposal_expiration = "12s"' + printf '%s\n' 'max_pending_request = 10000' + printf '%s\n' 'PROXY_TOML' + printf '%s\n' 'cd "$PROJECT"' + printf '%s\n' './scripts/start-services.sh --chain coston2' + printf '%s\n' 'gh codespace ports visibility 6674:public -c "$CODESPACE_NAME"' + printf '%s\n' 'curl --fail --silent --show-error http://localhost:6674/info' +} | gh codespace ssh -c "$CODESPACE_NAME" -- bash -s From e2348fb6fc54ac5e3f39436819314b6834059667 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 19:24:14 +0200 Subject: [PATCH 066/107] fix: restore scaffold process helper --- scripts/e2e.sh | 117 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 scripts/e2e.sh diff --git a/scripts/e2e.sh b/scripts/e2e.sh new file mode 100644 index 0000000..7b7a37f --- /dev/null +++ b/scripts/e2e.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# Process and readiness helper used by start-services.sh and stop-services.sh. +set -euo pipefail + +usage() { + cat >&2 <<'USAGE' +Usage: + e2e.sh wait-for-url URL [timeout_seconds] + e2e.sh start NAME PID_FILE LOG_FILE COMMAND [ARGS...] + e2e.sh status PID_DIR + e2e.sh stop-all PID_DIR +USAGE + exit 2 +} + +wait_for_url() { + local url="$1" + local timeout="${2:-60}" + local attempt + + for ((attempt = 1; attempt <= timeout; attempt++)); do + if curl --fail --silent --show-error --max-time 5 "$url" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + + echo "Timed out waiting for $url after ${timeout}s" >&2 + return 1 +} + +start_process() { + local name="$1" + local pid_file="$2" + local log_file="$3" + shift 3 + (($# > 0)) || { echo "No command supplied for $name" >&2; return 2; } + + mkdir -p "$(dirname "$pid_file")" "$(dirname "$log_file")" + if [[ -s "$pid_file" ]]; then + local existing_pid + existing_pid="$(<"$pid_file")" + if kill -0 "$existing_pid" 2>/dev/null; then + echo "$name already running (pid $existing_pid)" + return 0 + fi + fi + + nohup "$@" >"$log_file" 2>&1 < /dev/null & + local pid=$! + printf '%s\n' "$pid" >"$pid_file" + sleep 1 + if ! kill -0 "$pid" 2>/dev/null; then + echo "$name exited during startup; see $log_file" >&2 + return 1 + fi + echo "$name started (pid $pid)" +} + +status_processes() { + local pid_dir="$1" + local found=false + shopt -s nullglob + local pid_file pid + for pid_file in "$pid_dir"/*.pid; do + found=true + pid="$(<"$pid_file")" + if kill -0 "$pid" 2>/dev/null; then + echo "$(basename "$pid_file" .pid): running (pid $pid)" + else + echo "$(basename "$pid_file" .pid): stopped (stale pid $pid)" + fi + done + shopt -u nullglob + [[ "$found" == true ]] || echo "No managed processes in $pid_dir" +} + +stop_all() { + local pid_dir="$1" + shopt -s nullglob + local pid_file pid + for pid_file in "$pid_dir"/*.pid; do + pid="$(<"$pid_file")" + if kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null || true + for _ in 1 2 3 4 5; do + kill -0 "$pid" 2>/dev/null || break + sleep 1 + done + kill -9 "$pid" 2>/dev/null || true + fi + rm -f "$pid_file" + done + shopt -u nullglob +} + +case "${1:-}" in + wait-for-url) + (($# >= 2 && $# <= 3)) || usage + wait_for_url "$2" "${3:-60}" + ;; + start) + (($# >= 5)) || usage + start_process "$2" "$3" "$4" "${@:5}" + ;; + status) + (($# == 2)) || usage + status_processes "$2" + ;; + stop-all) + (($# == 2)) || usage + stop_all "$2" + ;; + *) + usage + ;; +esac From 70a3ac6115a58a4c8b3dfd878d087a97d822e68b Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 19:29:42 +0200 Subject: [PATCH 067/107] fix: quote indexer values in Codespace proxy config --- scripts/prepare-coston2-codespace.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/prepare-coston2-codespace.sh b/scripts/prepare-coston2-codespace.sh index 9749669..a0e2adb 100644 --- a/scripts/prepare-coston2-codespace.sh +++ b/scripts/prepare-coston2-codespace.sh @@ -17,11 +17,15 @@ shell_quote() { printf '%q' "$1" } +toml_quote() { + printf '%s' "$1" | jq -Rs . +} + proxy_key_q="$(shell_quote "$PROXY_PRIVATE_KEY")" -db_host_q="$(shell_quote "$INDEXER_DB_HOST")" -db_name_q="$(shell_quote "$INDEXER_DB_NAME")" -db_user_q="$(shell_quote "$INDEXER_DB_USER")" -db_password_q="$(shell_quote "$INDEXER_DB_PASSWORD")" +db_host_q="$(toml_quote "$INDEXER_DB_HOST")" +db_name_q="$(toml_quote "$INDEXER_DB_NAME")" +db_user_q="$(toml_quote "$INDEXER_DB_USER")" +db_password_q="$(toml_quote "$INDEXER_DB_PASSWORD")" gh codespace list --json name,state \ | jq -e --arg name "$CODESPACE_NAME" '.[] | select(.name == $name) | [.name,.state] | @tsv' From 3c7b174c58f0b5aaabe2c32ada41c0c01a6223f5 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 19:33:08 +0200 Subject: [PATCH 068/107] fix: pass Codespace name to port visibility command --- scripts/prepare-coston2-codespace.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare-coston2-codespace.sh b/scripts/prepare-coston2-codespace.sh index a0e2adb..a4eeafb 100644 --- a/scripts/prepare-coston2-codespace.sh +++ b/scripts/prepare-coston2-codespace.sh @@ -86,6 +86,6 @@ gh codespace list --json name,state \ printf '%s\n' 'PROXY_TOML' printf '%s\n' 'cd "$PROJECT"' printf '%s\n' './scripts/start-services.sh --chain coston2' - printf '%s\n' 'gh codespace ports visibility 6674:public -c "$CODESPACE_NAME"' + printf '%s\n' "gh codespace ports visibility 6674:public -c $(shell_quote "$CODESPACE_NAME")" printf '%s\n' 'curl --fail --silent --show-error http://localhost:6674/info' } | gh codespace ssh -c "$CODESPACE_NAME" -- bash -s From 7da20b2c3abd1dc133d9346d8297873068761456 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 19:44:39 +0200 Subject: [PATCH 069/107] ci: make Codespace FCC relay reproducible --- .github/workflows/ci.yml | 37 +++++++++++++++++++++++ infra/cloudflare/fcc-ingress/src/index.ts | 2 ++ scripts/prepare-coston2-codespace.sh | 1 - 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b26261..8f4ffce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,6 +130,43 @@ jobs: - name: Prepare and start official scaffold shell: bash run: bash scripts/prepare-coston2-codespace.sh + - name: Register proxy port with Codespaces + shell: bash + run: | + set -euo pipefail + log_file="${RUNNER_TEMP}/concord-codespace-port-forward.log" + gh codespace ports forward 6674:6674 -c "$CODESPACE_NAME" >"$log_file" 2>&1 & + forward_pid=$! + cleanup() { + kill "$forward_pid" 2>/dev/null || true + } + trap cleanup EXIT + for _ in $(seq 1 30); do + if gh codespace ports --codespace "$CODESPACE_NAME" --json sourcePort \ + | jq -e 'any(.[]; .sourcePort == 6674)' >/dev/null; then + echo "Codespaces registered proxy port 6674" + exit 0 + fi + if ! kill -0 "$forward_pid" 2>/dev/null; then + cat "$log_file" >&2 || true + exit 1 + fi + sleep 1 + done + cat "$log_file" >&2 || true + exit 1 + - name: Make proxy port public + shell: bash + run: gh codespace ports visibility 6674:public -c "$CODESPACE_NAME" + - name: Verify public proxy port + shell: bash + run: | + set -euo pipefail + public_url="$(gh codespace ports --codespace "$CODESPACE_NAME" --json sourcePort,browseUrl,visibility \ + | jq -r '.[] | select(.sourcePort == 6674 and .visibility == "public") | .browseUrl' | head -n 1)" + test -n "$public_url" + curl --fail --silent --show-error "$public_url/info" \ + | jq '{extensionId: .machineData.extensionId, codeHash: .machineData.codeHash, platform: .machineData.platform, chainId: .teeInfo.chainId, lastSigningPolicyId: .teeInfo.lastSigningPolicyId}' live_sender_deploy: diff --git a/infra/cloudflare/fcc-ingress/src/index.ts b/infra/cloudflare/fcc-ingress/src/index.ts index 7a2555e..7d429f6 100644 --- a/infra/cloudflare/fcc-ingress/src/index.ts +++ b/infra/cloudflare/fcc-ingress/src/index.ts @@ -58,6 +58,7 @@ export default { const headers = new Headers(request.headers); headers.delete("host"); headers.delete("content-length"); + headers.delete("cookie"); headers.set("cache-control", "no-store"); headers.set("x-concord-ingress", "cloudflare-workers-development-relay"); @@ -73,6 +74,7 @@ export default { }); const responseHeaders = new Headers(response.headers); + responseHeaders.delete("set-cookie"); responseHeaders.set("cache-control", "no-store"); responseHeaders.set( "x-concord-ingress", diff --git a/scripts/prepare-coston2-codespace.sh b/scripts/prepare-coston2-codespace.sh index a4eeafb..74fdf61 100644 --- a/scripts/prepare-coston2-codespace.sh +++ b/scripts/prepare-coston2-codespace.sh @@ -86,6 +86,5 @@ gh codespace list --json name,state \ printf '%s\n' 'PROXY_TOML' printf '%s\n' 'cd "$PROJECT"' printf '%s\n' './scripts/start-services.sh --chain coston2' - printf '%s\n' "gh codespace ports visibility 6674:public -c $(shell_quote "$CODESPACE_NAME")" printf '%s\n' 'curl --fail --silent --show-error http://localhost:6674/info' } | gh codespace ssh -c "$CODESPACE_NAME" -- bash -s From cca74d105812c31d48153c284d7825580566d58d Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 19:49:23 +0200 Subject: [PATCH 070/107] ci: register FCC machine through relay fallback --- .github/workflows/ci.yml | 58 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f4ffce..1d1cb5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,6 +167,64 @@ jobs: test -n "$public_url" curl --fail --silent --show-error "$public_url/info" \ | jq '{extensionId: .machineData.extensionId, codeHash: .machineData.codeHash, platform: .machineData.platform, chainId: .teeInfo.chainId, lastSigningPolicyId: .teeInfo.lastSigningPolicyId}' + - name: Install Go for relay registration + uses: actions/setup-go@v5 + with: + go-version: "1.25.1" + cache: false + - name: Register simulated development TEE through stable Worker relay + shell: bash + env: + CHAIN: coston2 + CHAIN_URL: https://coston2-api.flare.network/ext/C/rpc + NORMAL_PROXY_URL: https://tee-proxy-coston2-1.flare.rocks + EXT_PROXY_URL: ${{ secrets.CONCORD_FCC_PUBLIC_PROXY_URL }} + EXT_PROXY_HOST_URL: ${{ secrets.CONCORD_FCC_PUBLIC_PROXY_URL }} + LOCAL_MODE: "false" + SIMULATED_TEE: "true" + DEPLOYMENT_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_DEPLOYER_PRIVATE_KEY }} + INITIAL_OWNER: "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3" + GOVERNANCE_SIGNERS: "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3" + GOVERNANCE_THRESHOLD: "1" + ADDRESSES_FILE: ./config/coston2/deployed-addresses.json + REGISTER_TEE_COMMAND: rRap + run: | + set -euo pipefail + test -n "$DEPLOYMENT_PRIVATE_KEY" + test -n "$EXT_PROXY_URL" + test -n "$EXT_PROXY_HOST_URL" + case "$EXT_PROXY_URL" in + https://*) ;; + *) echo 'relay registration requires a public HTTPS Worker URL' >&2; exit 1 ;; + esac + curl --fail --silent --show-error --max-time 20 "$EXT_PROXY_URL/info" \ + | jq -e '.machineData.extensionId == "0x000000000000000000000000000000000000000000000000000000000001028c" and .teeInfo.chainId == 114' >/dev/null + bash scripts/post-build.sh + - name: Verify one active production TEE machine + shell: bash + env: + CHAIN_URL: https://coston2-api.flare.network/ext/C/rpc + run: | + set -euo pipefail + (cd tools && go run ./cmd/query-tee \ + -rpc "$CHAIN_URL" \ + -reg 0x1a9C4A0f9D76c0b1D91d22E24E573a9b377618aE \ + -ext 66188) | tee "$RUNNER_TEMP/concord-tee-query.txt" + active_count="$(grep -Ec '^[[:space:]]+[0-9]+: 0x[0-9a-fA-F]{40} url="https://' "$RUNNER_TEMP/concord-tee-query.txt" || true)" + test "$active_count" = 1 + tee_id="$(sed -n 's/^[[:space:]]*[0-9]*: \(0x[0-9a-fA-F]\{40\}\) url=.*/\1/p' "$RUNNER_TEMP/concord-tee-query.txt" | head -n 1)" + test -n "$tee_id" + (cd tools && go run ./cmd/query-tee \ + -rpc "$CHAIN_URL" \ + -reg 0x1a9C4A0f9D76c0b1D91d22E24E573a9b377618aE \ + "$tee_id") | tee "$RUNNER_TEMP/concord-tee-machine.txt" + grep -q 'getTeeMachineStatus: 2' "$RUNNER_TEMP/concord-tee-machine.txt" + grep -q 'getTeeMachine:.*url="https://' "$RUNNER_TEMP/concord-tee-machine.txt" + { + echo 'Coston2 FCC scaffold registered exactly one simulated development TEE through the stable Worker relay.' + echo "TEE machine status: 2 (PRODUCTION); teeId: $tee_id" + echo 'The relay is an ingress fallback; it is not a named Cloudflare Tunnel or production hardware-TEE claim.' + } >> "$GITHUB_STEP_SUMMARY" live_sender_deploy: From ddf07a0cb9b05a570f07082763f8ae7814539c79 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 20:10:52 +0200 Subject: [PATCH 071/107] fix: preserve FCC identity across Codespace prep --- scripts/prepare-coston2-codespace.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/prepare-coston2-codespace.sh b/scripts/prepare-coston2-codespace.sh index 74fdf61..3e65cbd 100644 --- a/scripts/prepare-coston2-codespace.sh +++ b/scripts/prepare-coston2-codespace.sh @@ -85,6 +85,11 @@ gh codespace list --json name,state \ printf '%s\n' 'max_pending_request = 10000' printf '%s\n' 'PROXY_TOML' printf '%s\n' 'cd "$PROJECT"' - printf '%s\n' './scripts/start-services.sh --chain coston2' + printf '%s\n' 'running_services="$(docker compose -f docker-compose.yaml -f docker-compose.coston2.yaml ps --status running --services)"' + printf '%s\n' 'if grep -qx "ext-proxy" <<<"$running_services" && grep -qx "extension-tee" <<<"$running_services" && curl --fail --silent --show-error http://localhost:6674/info >/dev/null; then' + printf '%s\n' ' echo "official scaffold already running; preserving current TEE identity"' + printf '%s\n' 'else' + printf '%s\n' ' ./scripts/start-services.sh --chain coston2' + printf '%s\n' 'fi' printf '%s\n' 'curl --fail --silent --show-error http://localhost:6674/info' } | gh codespace ssh -c "$CODESPACE_NAME" -- bash -s From e36cea5bde63bde58bbcbb19ede7f43b82e558eb Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 20:32:51 +0200 Subject: [PATCH 072/107] fix: relay FCC action results through Worker --- infra/cloudflare/fcc-ingress/README.md | 1 + infra/cloudflare/fcc-ingress/src/index.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/infra/cloudflare/fcc-ingress/README.md b/infra/cloudflare/fcc-ingress/README.md index 16e47a0..af9dc1b 100644 --- a/infra/cloudflare/fcc-ingress/README.md +++ b/infra/cloudflare/fcc-ingress/README.md @@ -14,6 +14,7 @@ semantics. It only forwards the proxy endpoints that the FCC setup needs: - POST /instruction - GET /metrics - GET /action/status... +- GET /action/result... The upstream URL is intentionally a secret so it is not committed. For this hackathon fallback it points at the public HTTPS URL for the persistent diff --git a/infra/cloudflare/fcc-ingress/src/index.ts b/infra/cloudflare/fcc-ingress/src/index.ts index 7d429f6..cbc0a3c 100644 --- a/infra/cloudflare/fcc-ingress/src/index.ts +++ b/infra/cloudflare/fcc-ingress/src/index.ts @@ -9,7 +9,9 @@ function isAllowedPath(pathname) { return ( pathname in ALLOWED_METHODS || pathname === "/action/status" || - pathname.startsWith("/action/status/") + pathname.startsWith("/action/status/") || + pathname === "/action/result" || + pathname.startsWith("/action/result/") ); } From 62eb54740b4a25e3e2e4b9b0b9c637840cf67592 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 20:38:22 +0200 Subject: [PATCH 073/107] docs: record live FCC registration evidence --- config/coston2/concord-deployment.json | 14 +++++++++++--- docs/CONCORD_STATUS.md | 26 +++++++++++++++++++------- docs/source-snapshot-2026-08-13.yaml | 9 ++++++--- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/config/coston2/concord-deployment.json b/config/coston2/concord-deployment.json index bf62d58..03509a6 100644 --- a/config/coston2/concord-deployment.json +++ b/config/coston2/concord-deployment.json @@ -1,7 +1,7 @@ { "network": "Coston2", "chainId": 114, - "observedAt": "2026-08-13T05:00:30Z", + "observedAt": "2026-08-13T18:35:30Z", "rpcUrl": "https://coston2-api.flare.network/ext/C/rpc", "explorerUrl": "https://coston2-explorer.flare.network", "teeRegistry": "0x1a9C4A0f9D76c0b1D91d22E24E573a9b377618aE", @@ -9,7 +9,14 @@ "id": "0x000000000000000000000000000000000000000000000000000000000001028c", "decimalId": 66188, "instructionSender": "0x574b523eA944EFe9143AF9d6c46bfA925beE2968", - "activeTeeMachines": 0 + "activeTeeMachines": 1, + "activeTeeIds": [ + "0xeE39d5e7d1C5043232282e3CC884B41a9Db22c85" + ], + "activeTeeUrls": [ + "https://concord-fcc-ingress.microcosm.workers.dev" + ], + "teeMode": "simulated_development" }, "assets": { "fxrp": "0x0b6A3645c240605887a5532109323A3E12273dc7", @@ -83,7 +90,7 @@ "canonicalFacilityDeployed": true, "assetBindingCurrent": true, "extensionRegistered": true, - "activeTeeMachine": false, + "activeTeeMachine": true, "providerWalletsFunded": true, "providerUSDT0BalancesObserved": true, "usdt0AvailableToTreasury": true, @@ -100,6 +107,7 @@ "The current faucet token is 0xC1A5B41512496B80903D1f32d6dEa3a73212E71F (USD₮0, 6 decimals). The replacement facility and root round are deployed, and provider gas is funded; FCC execution and quote/funding steps remain.", "The current Flare/Foundry toolchain still reports chain 114 as unsupported for EIP-3855; deployment succeeded after targeting solc 0.8.27, optimizer=true, optimizer_runs=200, and evm_version=paris. Keep this warning in the deployment record.", "The Coston2 FCC development path is not hardware-TEE evidence; the intended path uses the documented simulated development TEE.", + "The Concord extension is registered through a stable Cloudflare Workers.dev ingress relay backed by the disposable Codespace public port; this is not a named Cloudflare Tunnel or a production ingress claim.", "No private key is stored in this file." ], "explorerLinks": { diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index dc2c9d8..194e8b5 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -64,9 +64,13 @@ bound to the current token, and its current root round is recorded below. ## Not yet evidenced -- No active TEE machine is registered for extension `66188` (the live registry - query returned zero active machines), so the OPEN round has not yet produced a - live FCC instruction result. +- One simulated development TEE machine is active for extension `66188` at + status `2` (`PRODUCTION`): tee id + `0xeE39d5e7d1C5043232282e3CC884B41a9Db22c85`. Its registered stable HTTPS + URL is `https://concord-fcc-ingress.microcosm.workers.dev`, a narrow + Cloudflare Workers.dev relay to the disposable Codespace proxy. The setup + workflow obtained the availability proof and verified the on-chain machine + record. - The three disposable provider wallets and the treasury now each hold 10 units of the current six-decimal `USDT0 test` token. Each provider also has 105 C2FLR after the guarded gas-funding step, so approval and funding @@ -79,10 +83,10 @@ bound to the current token, and its current root round is recorded below. superseded 18-decimal liquidity token. - No FCC quote/finalize instruction, allocation verification, child Accord, draw, settlement, repayment, or restored-capacity receipt has been observed. -- The current Coston2 indexer configuration is now stored as encrypted GitHub - Actions secrets using the current DevHub values. FCC setup still needs a - stable public extension URL and named-tunnel token; no machine registration - or instruction flow has been dispatched. +- The current Coston2 indexer configuration is stored as encrypted GitHub + Actions secrets using the current DevHub values. FCC registration is now + evidenced through the stable Workers.dev relay. This is a development ingress + fallback, not a named Cloudflare Tunnel or production hardware-TEE claim. ## Privacy truth @@ -151,3 +155,11 @@ Explorer links: parity, and the read-only preflight gate. - The same commit passed the read-only [Coston2 asset-resolution run](https://github.com/etvjay/Concord/actions/runs/31646160617). +- Commit `e36cea5bde63bde58bbcbb19ede7f43b82e558eb` passed the guarded + [Coston2 FCC setup run](https://github.com/etvjay/Concord/actions/runs/31731326000): + the official scaffold remained running, the Codespace proxy port was made + public, the Worker relay was verified, the simulated TEE availability proof + was obtained, and the machine was promoted to status `2`. +- This run proves registration and live FCC ingress only. It does not prove + quote submission, CoFill finalization, provider funding, draw settlement, + repayment, or restored capacity. diff --git a/docs/source-snapshot-2026-08-13.yaml b/docs/source-snapshot-2026-08-13.yaml index e7990cf..1c79609 100644 --- a/docs/source-snapshot-2026-08-13.yaml +++ b/docs/source-snapshot-2026-08-13.yaml @@ -38,13 +38,16 @@ snapshot: statement: The DevHub getting-started walkthrough still documents quick tunnels for development and says to update EXT_PROXY_URL when a quick-tunnel URL changes. current_operational_constraint: A registered Coston2 machine must use one stable public HTTPS URL; restarting a rotating tunnel strands the onchain machine record and can create a new simulated TEE identity. resolution: Concord permits quick tunnels only for unregistered smoke tests and requires a stable named-tunnel URL/token for the guarded registration workflow. + - id: FCC-WORKERS-DEV-INGRESS-FALLBACK + statement: The official path requires a stable public HTTPS extension-proxy URL; a named Cloudflare Tunnel is one supported way to provide it. + current_operational_constraint: This account cannot self-assign the Cloudflare Connectivity Directory Admin role required for the VPC/tunnel path, and no custom domain is available. + resolution: Concord uses the stable Workers.dev URL `https://concord-fcc-ingress.microcosm.workers.dev` as a narrow development relay to the disposable Codespace public port. It forwards `/instruction`, `/info`, `/health`, `/metrics`, `/action/status...`, and `/action/result...`; it is not represented as a named tunnel or production ingress. concord_decisions: - No product-semantic contradiction was found. - Simulated TEE evidence remains development-path evidence, not production hardware-backed confidentiality. - FDC remains an evidence seam and is not added to the MVP. live_gate_inputs_still_required: - funded Coston2 deployment/proxy accounts - - stable named-tunnel HTTPS EXT_PROXY_URL (not a rotating quick tunnel) - - stable named-tunnel token in CONCORD_FCC_TUNNEL_ARGS - - current extension registration and active TEE-machine evidence + - a stable public HTTPS EXT_PROXY_URL (the current guarded branch uses the Workers.dev relay fallback) + - current extension registration and active TEE-machine evidence (now observed for extension 66188) - real provider funding, draw, repayment, and explorer receipts From 3f5e6dce548cd93c9dfa9f014cf610177b0cb232 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 20:49:22 +0200 Subject: [PATCH 074/107] feat: execute and materialize live FCC round --- .github/workflows/ci.yml | 108 +++++++ docs/testing-against-coston2.md | 34 +++ go/cmd/build-quotes/main.go | 439 ++++++++++++++++++++++++++++ go/internal/extension/extension.go | 16 + tools/cmd/verify-allocation/main.go | 120 ++++++++ 5 files changed, 717 insertions(+) create mode 100644 go/cmd/build-quotes/main.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d1cb5c..fe548e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,11 @@ on: required: false default: false type: boolean + execute_fcc_round: + description: 'Submit three signed provider offers, finalize CoFill, verify FCC evidence, and materialize Child Accords' + required: false + default: false + type: boolean permissions: contents: read @@ -227,6 +232,109 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" + live_fcc_round: + name: Coston2 FCC round and Child Accord materialization + if: github.event_name == 'workflow_dispatch' && inputs.setup_fcc == true && inputs.execute_fcc_round == true && github.ref == 'refs/heads/agent/concord-rebuild' + needs: codespace_fcc_runtime + concurrency: + group: concord-coston2-fcc-round + cancel-in-progress: false + runs-on: ubuntu-latest + env: + CHAIN_URL: https://coston2-api.flare.network/ext/C/rpc + EXT_PROXY_URL: ${{ secrets.CONCORD_FCC_PUBLIC_PROXY_URL }} + DEPLOYMENT_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_DEPLOYER_PRIVATE_KEY }} + CONCORD_E2E_PROVIDER_A_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_PROVIDER_A_PRIVATE_KEY }} + CONCORD_E2E_PROVIDER_B_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_PROVIDER_B_PRIVATE_KEY }} + CONCORD_E2E_PROVIDER_C_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_PROVIDER_C_PRIVATE_KEY }} + INSTRUCTION_SENDER: 0x574b523eA944EFe9143AF9d6c46bfA925beE2968 + FACILITY: 0xfaff601a18a9fca33378953515aa0f3ef9286ecd + TEE_REGISTRY: 0x1a9C4A0f9D76c0b1D91d22E24E573a9b377618aE + EXTENSION_ID: 0x000000000000000000000000000000000000000000000000000000000001028c + ADDRESSES_FILE: ./config/coston2/deployed-addresses.json + steps: + - name: Check out requested ref + uses: actions/checkout@v4 + + - name: Install Go 1.25.1 + uses: actions/setup-go@v5 + with: + go-version: "1.25.1" + cache: false + + - name: Build ephemeral signed provider offers from live round state + shell: bash + run: | + set -euo pipefail + round_id="$(jq -r '.rootRound.roundId' config/coston2/concord-deployment.json)" + root_id="$(jq -r '.rootRound.rootAccordId' config/coston2/concord-deployment.json)" + provider_a="$(jq -r '.rootRound.eligibleProviders[0]' config/coston2/concord-deployment.json)" + provider_b="$(jq -r '.rootRound.eligibleProviders[1]' config/coston2/concord-deployment.json)" + provider_c="$(jq -r '.rootRound.eligibleProviders[2]' config/coston2/concord-deployment.json)" + test "$round_id" != "null" + test "$root_id" != "null" + mkdir -p "$RUNNER_TEMP/concord-quotes" + (cd go && GOTOOLCHAIN=local go run ./cmd/build-quotes \ + -rpc "$CHAIN_URL" \ + -facility "$FACILITY" \ + -extension-id "$EXTENSION_ID" \ + -round-id "$round_id" \ + -root-accord-id "$root_id" \ + -out-dir "$RUNNER_TEMP/concord-quotes" \ + -provider-a "$provider_a" \ + -provider-b "$provider_b" \ + -provider-c "$provider_c") + jq -e '.roundId == $round and .rootAccordId == $root' \ + --arg round "$round_id" --arg root "$root_id" \ + "$RUNNER_TEMP/concord-quotes/finalize-round.json" >/dev/null + + - name: Submit quotes and finalize CoFill through FCC + shell: bash + env: + CONCORD_QUOTES: ${{ runner.temp }}/concord-quotes/quote-a.json,${{ runner.temp }}/concord-quotes/quote-b.json,${{ runner.temp }}/concord-quotes/quote-c.json + CONCORD_FINALIZE: ${{ runner.temp }}/concord-quotes/finalize-round.json + CONCORD_EVIDENCE_OUT: ${{ runner.temp }}/concord-fcc-action.json + LOCAL_MODE: "false" + run: | + set -euo pipefail + case "$EXT_PROXY_URL" in https://*) ;; *) echo 'FCC round requires public HTTPS proxy URL' >&2; exit 1 ;; esac + bash scripts/test.sh + + - name: Verify FCC evidence and materialize Child Accords + shell: bash + run: | + set -euo pipefail + round_id="$(jq -r '.rootRound.roundId' config/coston2/concord-deployment.json)" + root_id="$(jq -r '.rootRound.rootAccordId' config/coston2/concord-deployment.json)" + (cd tools && GOTOOLCHAIN=local go run ./cmd/verify-allocation \ + -c "$CHAIN_URL" \ + -facility "$FACILITY" \ + -teeRegistry "$TEE_REGISTRY" \ + -result "$RUNNER_TEMP/concord-fcc-action.json" \ + -extensionId "$EXTENSION_ID" \ + -roundId "$round_id" \ + -rootAccordId "$root_id" \ + -mark \ + -materialize \ + -out "$RUNNER_TEMP/concord-allocation-verification.json") + jq -e '.broadcast == true and .materialized == true' \ + "$RUNNER_TEMP/concord-allocation-verification.json" >/dev/null + { + echo 'FCC round evidence verified against the active Coston2 TEE machine.' + echo 'Allocation digest authorized and Child Accords materialized on the canonical CapitalFacility.' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Preserve FCC round evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: concord-fcc-round-evidence + path: | + ${{ runner.temp }}/concord-fcc-action.json + ${{ runner.temp }}/concord-allocation-verification.json + if-no-files-found: ignore + + live_sender_deploy: name: Coston2 sender deployment (manual only) if: github.event_name == 'workflow_dispatch' && inputs.deploy_sender == true diff --git a/docs/testing-against-coston2.md b/docs/testing-against-coston2.md index 6ce34dc..6d5ed60 100644 --- a/docs/testing-against-coston2.md +++ b/docs/testing-against-coston2.md @@ -56,6 +56,27 @@ The runner submits each quote through encrypted `SUBMIT_QUOTE` instructions, then submits `FINALIZE_ROUND` and writes the complete signed FCC evidence envelope to `CONCORD_EVIDENCE_OUT`. +For the disposable live operator path, build those files from the current +onchain round without placing them in the repository: + +```bash +mkdir -p "$RUNNER_TEMP/concord-quotes" +(cd go && go run ./cmd/build-quotes \ + -rpc "$CHAIN_URL" \ + -facility "$CAPITAL_FACILITY" \ + -extension-id "$EXTENSION_ID" \ + -round-id "$ROUND_ID" \ + -root-accord-id "$ROOT_ACCORD_ID" \ + -out-dir "$RUNNER_TEMP/concord-quotes" \ + -provider-a "$PROVIDER_A" \ + -provider-b "$PROVIDER_B" \ + -provider-c "$PROVIDER_C") +``` + +`build-quotes` reads the three provider private keys from the environment, +checks the live round and eligibility onchain, and writes mode-0600 quote +payloads plus the finalization payload. It never writes a provider key. + Verify that envelope against the deployed CapitalFacility before materializing children: @@ -76,6 +97,19 @@ The verifier is read-only by default. Use `-mark` only after the signed action, active-machine, facility-binding, and digest checks pass; the caller must be the facility's configured `allocationVerifier`. +After that verification, the treasury borrower can materialize the selected +Child Accords in the same bounded step: + +```bash +go run ./cmd/verify-allocation \ + ...same verification flags... \ + -mark -materialize +``` + +`-materialize` requires `-mark`; it first authorizes the verified digest and +then calls `materializeAllocation`. A selected child is still not funded until +its provider's USDT0 transfer succeeds. + Counters are in memory, so the numbers restart at 1 after any TEE relaunch. ## Testing a local proxy against Coston2 diff --git a/go/cmd/build-quotes/main.go b/go/cmd/build-quotes/main.go new file mode 100644 index 0000000..468bacb --- /dev/null +++ b/go/cmd/build-quotes/main.go @@ -0,0 +1,439 @@ +// Command build-quotes creates the ephemeral, provider-signed inputs for one +// live Concord Makkari round. It is an operator tool, not part of the FCC +// runtime: provider keys are read from environment variables and never written +// to the repository or included in the output summary. +package main + +import ( + "context" + "crypto/ecdsa" + "encoding/json" + "flag" + "fmt" + "math/big" + "os" + "path/filepath" + "strings" + "time" + + "concord/internal/extension" + "concord/pkg/types" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" +) + +const roundABIJSON = `[ + { + "inputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "name": "rounds", + "outputs": [ + {"internalType": "bytes32", "name": "id", "type": "bytes32"}, + {"internalType": "bytes32", "name": "rootAccordId", "type": "bytes32"}, + {"internalType": "uint256", "name": "targetCapacity", "type": "uint256"}, + {"internalType": "uint32", "name": "maxFeeBps", "type": "uint32"}, + {"internalType": "uint64", "name": "roundExpiry", "type": "uint64"}, + {"internalType": "uint8", "name": "status", "type": "uint8"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "name": "getEligibleProviders", + "outputs": [{"internalType": "address[]", "name": "", "type": "address[]"}], + "stateMutability": "view", + "type": "function" + } +]` + +type providerSpec struct { + label string + address common.Address + keyEnv string + capacity *big.Int + feeBps uint32 + nonce *big.Int +} + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "build-quotes: %v\n", err) + os.Exit(1) + } +} + +func run() error { + flags := flag.NewFlagSet("build-quotes", flag.ContinueOnError) + rpcURL := flags.String("rpc", "", "Coston2 RPC URL") + facilityText := flags.String("facility", "", "CapitalFacility address") + extensionText := flags.String("extension-id", "", "FCC extension id") + roundText := flags.String("round-id", "", "Makkari round id") + rootText := flags.String("root-accord-id", "", "root Accord id") + outDir := flags.String("out-dir", "", "directory for ephemeral quote and finalize JSON files") + evaluationText := flags.String("evaluation-time", "", "optional Unix evaluation time; defaults to current time") + providerA := flags.String("provider-a", "", "provider A address") + providerB := flags.String("provider-b", "", "provider B address") + providerC := flags.String("provider-c", "", "provider C address") + keyEnvA := flags.String("key-env-a", "CONCORD_E2E_PROVIDER_A_PRIVATE_KEY", "environment variable containing provider A key") + keyEnvB := flags.String("key-env-b", "CONCORD_E2E_PROVIDER_B_PRIVATE_KEY", "environment variable containing provider B key") + keyEnvC := flags.String("key-env-c", "CONCORD_E2E_PROVIDER_C_PRIVATE_KEY", "environment variable containing provider C key") + capacityA := flags.String("capacity-a", "3000000", "provider A capacity in base units") + capacityB := flags.String("capacity-b", "3000000", "provider B capacity in base units") + capacityC := flags.String("capacity-c", "3000000", "provider C capacity in base units") + feeA := flags.Uint("fee-a", 610, "provider A fee in basis points") + feeB := flags.Uint("fee-b", 640, "provider B fee in basis points") + feeC := flags.Uint("fee-c", 680, "provider C fee in basis points") + if err := flags.Parse(os.Args[1:]); err != nil { + return err + } + for name, value := range map[string]string{ + "rpc": *rpcURL, "facility": *facilityText, "extension-id": *extensionText, + "round-id": *roundText, "root-accord-id": *rootText, "out-dir": *outDir, + "provider-a": *providerA, "provider-b": *providerB, "provider-c": *providerC, + } { + if strings.TrimSpace(value) == "" { + return fmt.Errorf("-%s is required", name) + } + } + if *feeA > 10000 || *feeB > 10000 || *feeC > 10000 { + return fmt.Errorf("provider fee exceeds 10000 bps") + } + + facility, err := parseAddress(*facilityText, "facility") + if err != nil { + return err + } + extensionID, err := parseHash(*extensionText, "extension-id") + if err != nil { + return err + } + roundID, err := parseHash(*roundText, "round-id") + if err != nil { + return err + } + rootID, err := parseHash(*rootText, "root-accord-id") + if err != nil { + return err + } + evaluationTime := uint64(time.Now().Unix()) + if *evaluationText != "" { + parsed, parseErr := parseUint64(*evaluationText, "evaluation-time") + if parseErr != nil { + return parseErr + } + evaluationTime = parsed + } + + parsedCapacityA, err := parsePositive(*capacityA, "capacity-a") + if err != nil { + return err + } + parsedCapacityB, err := parsePositive(*capacityB, "capacity-b") + if err != nil { + return err + } + parsedCapacityC, err := parsePositive(*capacityC, "capacity-c") + if err != nil { + return err + } + providers := []providerSpec{ + {label: "a", address: common.HexToAddress(*providerA), keyEnv: *keyEnvA, capacity: parsedCapacityA, feeBps: uint32(*feeA), nonce: big.NewInt(1)}, + {label: "b", address: common.HexToAddress(*providerB), keyEnv: *keyEnvB, capacity: parsedCapacityB, feeBps: uint32(*feeB), nonce: big.NewInt(2)}, + {label: "c", address: common.HexToAddress(*providerC), keyEnv: *keyEnvC, capacity: parsedCapacityC, feeBps: uint32(*feeC), nonce: big.NewInt(3)}, + } + for _, provider := range providers { + if provider.address == (common.Address{}) { + return fmt.Errorf("provider %s address is zero", provider.label) + } + } + + client, err := ethclient.Dial(*rpcURL) + if err != nil { + return fmt.Errorf("connect to RPC: %w", err) + } + defer client.Close() + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + chainID, err := client.ChainID(ctx) + if err != nil { + return fmt.Errorf("read chain id: %w", err) + } + if chainID.Uint64() != 114 { + return fmt.Errorf("expected Coston2 chain id 114, got %s", chainID) + } + contractABI, err := abi.JSON(strings.NewReader(roundABIJSON)) + if err != nil { + return fmt.Errorf("parse CapitalFacility ABI: %w", err) + } + roundValues, err := call(ctx, client, contractABI, facility, "rounds", roundID) + if err != nil { + return err + } + if len(roundValues) != 6 { + return fmt.Errorf("round getter returned %d values, expected 6", len(roundValues)) + } + chainRoot, err := hashValue(roundValues[1], "round rootAccordId") + if err != nil { + return err + } + if chainRoot != rootID { + return fmt.Errorf("round is bound to root %s, not %s", chainRoot.Hex(), rootID.Hex()) + } + target, ok := roundValues[2].(*big.Int) + if !ok || target.Sign() <= 0 { + return fmt.Errorf("round targetCapacity is invalid") + } + maxFee, err := uint32Value(roundValues[3], "round maxFeeBps") + if err != nil { + return err + } + roundExpiry, err := uint64Value(roundValues[4], "round roundExpiry") + if err != nil { + return err + } + status, err := uint64Value(roundValues[5], "round status") + if err != nil { + return err + } + if status != 1 { + return fmt.Errorf("round is not OPEN; status=%d", status) + } + if evaluationTime >= roundExpiry { + return fmt.Errorf("evaluation time %d is at or after round expiry %d", evaluationTime, roundExpiry) + } + if maxFee > 10000 { + return fmt.Errorf("round maxFeeBps exceeds protocol bound") + } + eligibleValues, err := call(ctx, client, contractABI, facility, "getEligibleProviders", roundID) + if err != nil { + return err + } + eligible, err := addressesValue(eligibleValues, "eligible providers") + if err != nil { + return err + } + eligibleSet := make(map[common.Address]bool, len(eligible)) + for _, address := range eligible { + eligibleSet[address] = true + } + capacityTotal := new(big.Int) + quotes := make([]types.QuoteRequest, 0, len(providers)) + for _, provider := range providers { + if !eligibleSet[provider.address] { + return fmt.Errorf("provider %s is not eligible for round", provider.address.Hex()) + } + if provider.feeBps > maxFee { + return fmt.Errorf("provider %s fee %d exceeds round max %d", provider.address.Hex(), provider.feeBps, maxFee) + } + key, keyErr := privateKeyFromEnv(provider.keyEnv) + if keyErr != nil { + return fmt.Errorf("provider %s: %w", provider.label, keyErr) + } + derived := crypto.PubkeyToAddress(key.PublicKey) + if derived != provider.address { + return fmt.Errorf("provider %s key resolves to %s, expected %s", provider.label, derived.Hex(), provider.address.Hex()) + } + quote := types.QuoteRequest{ + RoundID: roundID.Hex(), + RootAccordID: rootID.Hex(), + Provider: provider.address.Hex(), + Capacity: provider.capacity.String(), + FeeBps: provider.feeBps, + ValidUntil: roundExpiry, + Nonce: provider.nonce.String(), + } + quote.ProviderSignature, err = extension.SignQuote(quote, key) + if err != nil { + return fmt.Errorf("sign provider %s quote: %w", provider.label, err) + } + capacityTotal.Add(capacityTotal, provider.capacity) + quotes = append(quotes, quote) + } + if capacityTotal.Cmp(target) < 0 { + return fmt.Errorf("quote capacity %s is below target %s", capacityTotal, target) + } + + finalize := types.FinalizeRoundRequest{ + ExtensionID: extensionID.Hex(), + RoundID: roundID.Hex(), + RootAccordID: rootID.Hex(), + TargetCapacity: target.String(), + MaxFeeBps: maxFee, + RoundExpiry: roundExpiry, + EvaluationTime: evaluationTime, + EligibleProviders: eligible, + Quotes: quotes, + } + if err := os.MkdirAll(*outDir, 0700); err != nil { + return fmt.Errorf("create output directory: %w", err) + } + for _, provider := range providers { + quote := quotes[providerIndex(providers, provider.label)] + if err := writeJSON(filepath.Join(*outDir, "quote-"+provider.label+".json"), quote); err != nil { + return err + } + } + if err := writeJSON(filepath.Join(*outDir, "finalize-round.json"), finalize); err != nil { + return err + } + fmt.Printf("built 3 signed quotes for round %s; target=%s; expiry=%d; output=%s\n", roundID.Hex(), target.String(), roundExpiry, *outDir) + return nil +} + +func providerIndex(providers []providerSpec, label string) int { + for index, provider := range providers { + if provider.label == label { + return index + } + } + return -1 +} + +func privateKeyFromEnv(name string) (*ecdsa.PrivateKey, error) { + value := strings.TrimSpace(os.Getenv(name)) + if value == "" { + return nil, fmt.Errorf("%s is not set", name) + } + value = strings.TrimPrefix(strings.TrimPrefix(value, "0x"), "0X") + key, err := crypto.HexToECDSA(value) + if err != nil { + return nil, fmt.Errorf("decode %s: %w", name, err) + } + return key, nil +} + +func writeJSON(path string, value any) error { + data, err := json.MarshalIndent(value, "", " ") + if err != nil { + return fmt.Errorf("encode %s: %w", path, err) + } + data = append(data, '\n') + if err := os.WriteFile(path, data, 0600); err != nil { + return fmt.Errorf("write %s: %w", path, err) + } + return nil +} + +func call(ctx context.Context, client *ethclient.Client, contractABI abi.ABI, address common.Address, method string, args ...any) ([]any, error) { + input, err := contractABI.Pack(method, args...) + if err != nil { + return nil, fmt.Errorf("pack %s: %w", method, err) + } + output, err := client.CallContract(ctx, ethereum.CallMsg{To: &address, Data: input}, nil) + if err != nil { + return nil, fmt.Errorf("call %s: %w", method, err) + } + values, err := contractABI.Unpack(method, output) + if err != nil { + return nil, fmt.Errorf("decode %s: %w", method, err) + } + return values, nil +} + +func parseAddress(value, name string) (common.Address, error) { + if !common.IsHexAddress(value) || common.HexToAddress(value) == (common.Address{}) { + return common.Address{}, fmt.Errorf("%s must be a non-zero EVM address", name) + } + return common.HexToAddress(value), nil +} + +func parseHash(value, name string) (common.Hash, error) { + if !strings.HasPrefix(value, "0x") || len(value) != 66 { + return common.Hash{}, fmt.Errorf("%s must be a 32-byte 0x-prefixed value", name) + } + hash := common.HexToHash(value) + if hash == (common.Hash{}) { + return common.Hash{}, fmt.Errorf("%s must be non-zero", name) + } + return hash, nil +} + +func parsePositive(value, name string) (*big.Int, error) { + number, ok := new(big.Int).SetString(value, 10) + if !ok || number.Sign() <= 0 { + return nil, fmt.Errorf("%s must be a positive decimal integer", name) + } + return number, nil +} + +func parseUint64(value, name string) (uint64, error) { + number, ok := new(big.Int).SetString(value, 10) + if !ok || number.Sign() < 0 || !number.IsUint64() { + return 0, fmt.Errorf("%s must be a uint64", name) + } + return number.Uint64(), nil +} + +func hashValue(value any, name string) (common.Hash, error) { + switch typed := value.(type) { + case [32]byte: + return common.BytesToHash(typed[:]), nil + case common.Hash: + return typed, nil + case []byte: + if len(typed) != 32 { + return common.Hash{}, fmt.Errorf("%s is not bytes32", name) + } + return common.BytesToHash(typed), nil + default: + return common.Hash{}, fmt.Errorf("%s has unexpected type %T", name, value) + } +} + +func uint32Value(value any, name string) (uint32, error) { + switch typed := value.(type) { + case uint8: + return uint32(typed), nil + case uint16: + return uint32(typed), nil + case uint32: + return typed, nil + case uint64: + if typed > 1<<32-1 { + return 0, fmt.Errorf("%s exceeds uint32", name) + } + return uint32(typed), nil + case *big.Int: + if typed.Sign() < 0 || !typed.IsUint64() || typed.Uint64() > 1<<32-1 { + return 0, fmt.Errorf("%s exceeds uint32", name) + } + return uint32(typed.Uint64()), nil + default: + return 0, fmt.Errorf("%s has unexpected type %T", name, value) + } +} + +func uint64Value(value any, name string) (uint64, error) { + switch typed := value.(type) { + case uint8: + return uint64(typed), nil + case uint16: + return uint64(typed), nil + case uint32: + return uint64(typed), nil + case uint64: + return typed, nil + case *big.Int: + if typed.Sign() < 0 || !typed.IsUint64() { + return 0, fmt.Errorf("%s is outside uint64", name) + } + return typed.Uint64(), nil + default: + return 0, fmt.Errorf("%s has unexpected type %T", name, value) + } +} + +func addressesValue(values []any, name string) ([]common.Address, error) { + if len(values) != 1 { + return nil, fmt.Errorf("%s returned %d values, expected one", name, len(values)) + } + addresses, ok := values[0].([]common.Address) + if !ok { + return nil, fmt.Errorf("%s has unexpected type %T", name, values[0]) + } + return addresses, nil +} diff --git a/go/internal/extension/extension.go b/go/internal/extension/extension.go index da77db3..e6ea3bd 100644 --- a/go/internal/extension/extension.go +++ b/go/internal/extension/extension.go @@ -2,6 +2,7 @@ package extension import ( "bytes" + "crypto/ecdsa" "encoding/binary" "encoding/json" "fmt" @@ -408,6 +409,21 @@ func quoteDigest(q types.QuoteRequest) (common.Hash, error) { return crypto.Keccak256Hash(append(packed, sig...)), nil } +// SignQuote creates the provider signature for the canonical QuoteRequest +// encoding. It is exposed for the operator-side fixture builder; the runtime +// extension never receives provider private keys. +func SignQuote(q types.QuoteRequest, key *ecdsa.PrivateKey) (string, error) { + packed, err := quotePacked(q) + if err != nil { + return "", err + } + signature, err := crypto.Sign(accounts.TextHash(crypto.Keccak256(packed)), key) + if err != nil { + return "", err + } + return hexutil.Encode(signature), nil +} + func verifyQuoteSignature(q types.QuoteRequest) (common.Address, error) { packed, err := quotePacked(q) if err != nil { diff --git a/tools/cmd/verify-allocation/main.go b/tools/cmd/verify-allocation/main.go index 7376e0d..a73bbbc 100644 --- a/tools/cmd/verify-allocation/main.go +++ b/tools/cmd/verify-allocation/main.go @@ -49,6 +49,19 @@ type actionEvidence struct { VerifiedAt string `json:"verifiedAt"` } +type contractAllocationResult struct { + ExtensionID [32]byte + RoundID [32]byte + RootAccordID [32]byte + Success bool + SelectedProviders []common.Address + AllocatedCapacity []*big.Int + AcceptedFeeBps []uint32 + TermsCommitments [][32]byte + RoundExpiry uint64 + ResultDigest [32]byte +} + const facilityABIJSON = `[ { "inputs": [], @@ -94,6 +107,29 @@ const facilityABIJSON = `[ "outputs": [], "stateMutability": "nonpayable", "type": "function" + }, + { + "inputs": [{ + "components": [ + {"internalType": "bytes32", "name": "extensionId", "type": "bytes32"}, + {"internalType": "bytes32", "name": "roundId", "type": "bytes32"}, + {"internalType": "bytes32", "name": "rootAccordId", "type": "bytes32"}, + {"internalType": "bool", "name": "success", "type": "bool"}, + {"internalType": "address[]", "name": "selectedProviders", "type": "address[]"}, + {"internalType": "uint256[]", "name": "allocatedCapacity", "type": "uint256[]"}, + {"internalType": "uint32[]", "name": "acceptedFeeBps", "type": "uint32[]"}, + {"internalType": "bytes32[]", "name": "termsCommitments", "type": "bytes32[]"}, + {"internalType": "uint64", "name": "roundExpiry", "type": "uint64"}, + {"internalType": "bytes32", "name": "resultDigest", "type": "bytes32"} + ], + "internalType": "struct ConcordTypes.AllocationResult", + "name": "result", + "type": "tuple" + }], + "name": "materializeAllocation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ]` @@ -126,12 +162,16 @@ func run() error { rootFlag := flag.String("rootAccordId", "", "expected root Accord id") teeRegistryFlag := flag.String("teeRegistry", "", "FlareTeeManager diamond address for active-machine verification") mark := flag.Bool("mark", false, "broadcast markAllocationVerified after local verification") + materialize := flag.Bool("materialize", false, "after -mark, materialize the verified allocation into Child Accords") evidenceFile := flag.String("out", "", "optional JSON evidence output path") flag.Parse() if *facilityFlag == "" || *resultFile == "" || *extensionFlag == "" || *roundFlag == "" || *rootFlag == "" { return fmt.Errorf("--facility, --result, --extensionId, --roundId, and --rootAccordId are required") } + if *materialize && !*mark { + return fmt.Errorf("-materialize requires -mark so the FCC digest is authorized first") + } if !common.IsHexAddress(*facilityFlag) || common.HexToAddress(*facilityFlag) == (common.Address{}) { return fmt.Errorf("facility must be a non-zero EVM address") } @@ -275,9 +315,89 @@ func run() error { } } fmt.Printf("allocation digest marked verified: tx=%s\n", tx.Hash().Hex()) + if *materialize { + allocation, err := contractAllocation(result) + if err != nil { + return err + } + materializeTx, err := bound.Transact(opts, "materializeAllocation", allocation) + if err != nil { + return fmt.Errorf("send materializeAllocation: %w", err) + } + materializeReceipt, err := bind.WaitMined(ctx, client, materializeTx) + if err != nil { + return fmt.Errorf("wait for materializeAllocation %s: %w", materializeTx.Hash().Hex(), err) + } + if materializeReceipt.Status != coretypes.ReceiptStatusSuccessful { + return fmt.Errorf("materializeAllocation reverted in transaction %s", materializeTx.Hash().Hex()) + } + evidence["materialized"] = true + evidence["materializeTxHash"] = materializeTx.Hash().Hex() + if materializeReceipt.BlockNumber != nil { + evidence["materializeBlockNumber"] = materializeReceipt.BlockNumber.String() + } + if *evidenceFile != "" { + if err := writeEvidence(*evidenceFile, evidence); err != nil { + return err + } + } + fmt.Printf("allocation materialized into Child Accords: tx=%s\n", materializeTx.Hash().Hex()) + } return nil } +func contractAllocation(result allocationResult) (contractAllocationResult, error) { + extensionID, err := parseBytes32(result.ExtensionID, "result extensionId", false) + if err != nil { + return contractAllocationResult{}, err + } + roundID, err := parseBytes32(result.RoundID, "result roundId", false) + if err != nil { + return contractAllocationResult{}, err + } + rootID, err := parseBytes32(result.RootAccordID, "result rootAccordId", false) + if err != nil { + return contractAllocationResult{}, err + } + digest, err := parseBytes32(result.ResultDigest, "resultDigest", false) + if err != nil { + return contractAllocationResult{}, err + } + providers := make([]common.Address, len(result.SelectedProviders)) + amounts := make([]*big.Int, len(result.AllocatedCapacity)) + terms := make([][32]byte, len(result.TermsCommitments)) + if len(providers) != len(result.AcceptedFeeBps) || len(providers) != len(terms) { + return contractAllocationResult{}, fmt.Errorf("allocation arrays have different lengths") + } + for i := range providers { + providers[i], err = parseAddress(result.SelectedProviders[i], fmt.Sprintf("selectedProviders[%d]", i)) + if err != nil { + return contractAllocationResult{}, err + } + amounts[i], err = parsePositiveUint256(result.AllocatedCapacity[i], fmt.Sprintf("allocatedCapacity[%d]", i)) + if err != nil { + return contractAllocationResult{}, err + } + term, err := parseBytes32(result.TermsCommitments[i], fmt.Sprintf("termsCommitments[%d]", i), true) + if err != nil { + return contractAllocationResult{}, err + } + terms[i] = term + } + return contractAllocationResult{ + ExtensionID: extensionID, + RoundID: roundID, + RootAccordID: rootID, + Success: result.Success, + SelectedProviders: providers, + AllocatedCapacity: amounts, + AcceptedFeeBps: result.AcceptedFeeBps, + TermsCommitments: terms, + RoundExpiry: result.RoundExpiry, + ResultDigest: digest, + }, nil +} + func decodeEvidence(payload []byte) (allocationResult, *actionEvidence, error) { var envelope actionEvidence if err := json.Unmarshal(payload, &envelope); err == nil && envelope.Response != nil { From 51f630ea1f8140ca91c16120332b9dba44568d7a Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 20:50:13 +0200 Subject: [PATCH 075/107] fix: quote live round workflow addresses --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe548e0..0beac55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -247,10 +247,10 @@ jobs: CONCORD_E2E_PROVIDER_A_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_PROVIDER_A_PRIVATE_KEY }} CONCORD_E2E_PROVIDER_B_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_PROVIDER_B_PRIVATE_KEY }} CONCORD_E2E_PROVIDER_C_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_PROVIDER_C_PRIVATE_KEY }} - INSTRUCTION_SENDER: 0x574b523eA944EFe9143AF9d6c46bfA925beE2968 - FACILITY: 0xfaff601a18a9fca33378953515aa0f3ef9286ecd - TEE_REGISTRY: 0x1a9C4A0f9D76c0b1D91d22E24E573a9b377618aE - EXTENSION_ID: 0x000000000000000000000000000000000000000000000000000000000001028c + INSTRUCTION_SENDER: "0x574b523eA944EFe9143AF9d6c46bfA925beE2968" + FACILITY: "0xfaff601a18a9fca33378953515aa0f3ef9286ecd" + TEE_REGISTRY: "0x1a9C4A0f9D76c0b1D91d22E24E573a9b377618aE" + EXTENSION_ID: "0x000000000000000000000000000000000000000000000000000000000001028c" ADDRESSES_FILE: ./config/coston2/deployed-addresses.json steps: - name: Check out requested ref From 7f8a99a4da5cf2058cd18f2ea3908fb2a242ce48 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 20:53:24 +0200 Subject: [PATCH 076/107] fix: encode eligible providers for finalize payload --- go/cmd/build-quotes/main.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/go/cmd/build-quotes/main.go b/go/cmd/build-quotes/main.go index 468bacb..7cc84a2 100644 --- a/go/cmd/build-quotes/main.go +++ b/go/cmd/build-quotes/main.go @@ -220,6 +220,10 @@ func run() error { for _, address := range eligible { eligibleSet[address] = true } + eligibleProviderStrings := make([]string, len(eligible)) + for index, address := range eligible { + eligibleProviderStrings[index] = address.Hex() + } capacityTotal := new(big.Int) quotes := make([]types.QuoteRequest, 0, len(providers)) for _, provider := range providers { @@ -265,7 +269,7 @@ func run() error { MaxFeeBps: maxFee, RoundExpiry: roundExpiry, EvaluationTime: evaluationTime, - EligibleProviders: eligible, + EligibleProviders: eligibleProviderStrings, Quotes: quotes, } if err := os.MkdirAll(*outDir, 0700); err != nil { From 49f688ba70bfc0945e7c6cc0bb3b79cf61a87711 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 20:54:37 +0200 Subject: [PATCH 077/107] fix: make TEE promotion idempotent --- tools/pkg/fccutils/registration.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tools/pkg/fccutils/registration.go b/tools/pkg/fccutils/registration.go index 604ac6e..d99965b 100644 --- a/tools/pkg/fccutils/registration.go +++ b/tools/pkg/fccutils/registration.go @@ -155,14 +155,22 @@ func RegisterNode(s *support.Support, teeInfo *types.SignedTeeInfoResponse, host } if strings.Contains(command, "p") { - toProductionProof, err := GetFTDCAvailabilityCheckResult(ftdcTeeURL, instructionID) - if err != nil { - return err - } + // Registration is intentionally idempotent for a still-running TEE. + // The machine manager rejects ToProduction for a machine that is already + // in status 2 (PRODUCTION). This is the normal path when a workflow + // reuses the persistent Codespace and its existing simulated TEE identity. + status, statusErr := s.TeeMachineRegistry.GetTeeMachineStatus(nil, teeID) + if statusErr == nil && status == 2 { + logger.Infof("TEE machine %s is already in PRODUCTION, skipping promotion", teeID.Hex()) + } else { + toProductionProof, proofErr := GetFTDCAvailabilityCheckResult(ftdcTeeURL, instructionID) + if proofErr != nil { + return proofErr + } - err = ToProduction(s, toProductionProof) - if err != nil { - return err + if productionErr := ToProduction(s, toProductionProof); productionErr != nil { + return productionErr + } } } From 787570efe8735afaf43f32fc84dee6be1940985a Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 21:00:44 +0200 Subject: [PATCH 078/107] fix: recover verified FCC allocations --- .github/workflows/ci.yml | 75 +++++++++++++++++++++++++++++ tools/cmd/verify-allocation/main.go | 20 ++++---- 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0beac55..b1f4713 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,16 @@ on: required: false default: false type: boolean + materialize_existing: + description: 'Materialize a previously verified FCC artifact without replaying the finalized round' + required: false + default: false + type: boolean + source_run_id: + description: 'Workflow run ID containing concord-fcc-round-evidence when materialize_existing is true' + required: false + default: '' + type: string permissions: contents: read @@ -335,6 +345,71 @@ jobs: if-no-files-found: ignore + live_materialize_existing: + name: Materialize previously verified FCC allocation + if: github.event_name == 'workflow_dispatch' && inputs.materialize_existing == true && github.ref == 'refs/heads/agent/concord-rebuild' + permissions: + actions: read + contents: read + concurrency: + group: concord-coston2-fcc-materialization + cancel-in-progress: false + runs-on: ubuntu-latest + env: + CHAIN_URL: https://coston2-api.flare.network/ext/C/rpc + DEPLOYMENT_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_DEPLOYER_PRIVATE_KEY }} + FACILITY: "0xfaff601a18a9fca33378953515aa0f3ef9286ecd" + TEE_REGISTRY: "0x1a9C4A0f9D76c0b1D91d22E24E573a9b377618aE" + EXTENSION_ID: "0x000000000000000000000000000000000000000000000000000000000001028c" + steps: + - name: Check out requested ref + uses: actions/checkout@v4 + + - name: Install Go 1.25.1 + uses: actions/setup-go@v5 + with: + go-version: "1.25.1" + cache: false + + - name: Require source run id + shell: bash + run: test -n "${{ inputs.source_run_id }}" + + - name: Download signed FCC evidence from source run + uses: actions/download-artifact@v4 + with: + name: concord-fcc-round-evidence + path: ${{ runner.temp }}/concord-fcc-round-evidence + run-id: ${{ inputs.source_run_id }} + github-token: ${{ github.token }} + + - name: Verify digest and materialize Child Accords + shell: bash + run: | + set -euo pipefail + round_id="$(jq -r '.rootRound.roundId' config/coston2/concord-deployment.json)" + root_id="$(jq -r '.rootRound.rootAccordId' config/coston2/concord-deployment.json)" + action_file="$(find "$RUNNER_TEMP/concord-fcc-round-evidence" -name concord-fcc-action.json -type f -print -quit)" + test -n "$action_file" + (cd tools && GOTOOLCHAIN=local go run ./cmd/verify-allocation \ + -c "$CHAIN_URL" \ + -facility "$FACILITY" \ + -teeRegistry "$TEE_REGISTRY" \ + -result "$action_file" \ + -extensionId "$EXTENSION_ID" \ + -roundId "$round_id" \ + -rootAccordId "$root_id" \ + -mark \ + -materialize \ + -out "$RUNNER_TEMP/concord-allocation-verification.json") + jq -e '.broadcast == true and .materialized == true' \ + "$RUNNER_TEMP/concord-allocation-verification.json" >/dev/null + { + echo "Recovered signed FCC artifact from run ${{ inputs.source_run_id }}." + echo 'Allocation digest was already authorized; Child Accords are now materialized on the canonical CapitalFacility.' + } >> "$GITHUB_STEP_SUMMARY" + + live_sender_deploy: name: Coston2 sender deployment (manual only) if: github.event_name == 'workflow_dispatch' && inputs.deploy_sender == true diff --git a/tools/cmd/verify-allocation/main.go b/tools/cmd/verify-allocation/main.go index a73bbbc..9b009d2 100644 --- a/tools/cmd/verify-allocation/main.go +++ b/tools/cmd/verify-allocation/main.go @@ -50,16 +50,16 @@ type actionEvidence struct { } type contractAllocationResult struct { - ExtensionID [32]byte - RoundID [32]byte - RootAccordID [32]byte - Success bool - SelectedProviders []common.Address - AllocatedCapacity []*big.Int - AcceptedFeeBps []uint32 - TermsCommitments [][32]byte - RoundExpiry uint64 - ResultDigest [32]byte + ExtensionID [32]byte `abi:"extensionId"` + RoundID [32]byte `abi:"roundId"` + RootAccordID [32]byte `abi:"rootAccordId"` + Success bool `abi:"success"` + SelectedProviders []common.Address `abi:"selectedProviders"` + AllocatedCapacity []*big.Int `abi:"allocatedCapacity"` + AcceptedFeeBps []uint32 `abi:"acceptedFeeBps"` + TermsCommitments [][32]byte `abi:"termsCommitments"` + RoundExpiry uint64 `abi:"roundExpiry"` + ResultDigest [32]byte `abi:"resultDigest"` } const facilityABIJSON = `[ From 6dd69525bb5e4fc75f89586f40bc1dce83fdd471 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 21:08:40 +0200 Subject: [PATCH 079/107] feat: run funded facility lifecycle --- .github/workflows/ci.yml | 61 +++ tools/cmd/facility-flow/main.go | 756 ++++++++++++++++++++++++++++++++ 2 files changed, 817 insertions(+) create mode 100644 tools/cmd/facility-flow/main.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1f4713..747aba9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,6 +50,11 @@ on: required: false default: '' type: string + execute_facility_flow: + description: 'Fund all materialized children, execute a multi-child draw, repay it, and assert restored capacity' + required: false + default: false + type: boolean permissions: contents: read @@ -410,6 +415,62 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" + live_facility_flow: + name: Coston2 funding, multi-child draw, and repayment + if: github.event_name == 'workflow_dispatch' && inputs.execute_facility_flow == true && github.ref == 'refs/heads/agent/concord-rebuild' + permissions: + contents: read + concurrency: + group: concord-coston2-facility-flow + cancel-in-progress: false + runs-on: ubuntu-latest + env: + CHAIN_URL: https://coston2-api.flare.network/ext/C/rpc + DEPLOYMENT_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_DEPLOYER_PRIVATE_KEY }} + CONCORD_E2E_PROVIDER_A_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_PROVIDER_A_PRIVATE_KEY }} + CONCORD_E2E_PROVIDER_B_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_PROVIDER_B_PRIVATE_KEY }} + CONCORD_E2E_PROVIDER_C_PRIVATE_KEY: ${{ secrets.CONCORD_E2E_PROVIDER_C_PRIVATE_KEY }} + FACILITY: "0xfaff601a18a9fca33378953515aa0f3ef9286ecd" + USDT0: "0xC1A5B41512496B80903D1f32d6dEa3a73212E71F" + steps: + - name: Check out requested ref + uses: actions/checkout@v4 + + - name: Install Go 1.25.1 + uses: actions/setup-go@v5 + with: + go-version: "1.25.1" + cache: false + + - name: Run funded facility lifecycle + shell: bash + run: | + set -euo pipefail + root_id="$(jq -r '.rootRound.rootAccordId' config/coston2/concord-deployment.json)" + test "$root_id" != "null" + mkdir -p "$RUNNER_TEMP/concord-facility-flow" + (cd tools && GOTOOLCHAIN=local go run ./cmd/facility-flow \ + -rpc "$CHAIN_URL" \ + -facility "$FACILITY" \ + -token "$USDT0" \ + -root-accord-id "$root_id" \ + -out "$RUNNER_TEMP/concord-facility-flow/facility-flow.json") + jq -e 'all(.invariants[]; . == true) and .drawLegCount >= 2 and .drawnAfter == "0"' \ + "$RUNNER_TEMP/concord-facility-flow/facility-flow.json" >/dev/null + { + echo 'All materialized Child Accords funded with real Coston2 USDT0 transfers.' + echo 'A multi-child draw was settled, fully repaid, and available capacity was restored.' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Preserve facility lifecycle evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: concord-facility-flow-evidence + path: ${{ runner.temp }}/concord-facility-flow/facility-flow.json + if-no-files-found: ignore + + live_sender_deploy: name: Coston2 sender deployment (manual only) if: github.event_name == 'workflow_dispatch' && inputs.deploy_sender == true diff --git a/tools/cmd/facility-flow/main.go b/tools/cmd/facility-flow/main.go new file mode 100644 index 0000000..140a929 --- /dev/null +++ b/tools/cmd/facility-flow/main.go @@ -0,0 +1,756 @@ +// Command facility-flow exercises the funded Concord facility lifecycle on a +// configured network. It uses the provider keys only to approve and fund their +// already-materialized Child Accords, then uses the treasury key to draw and +// repay. Every transition is checked against onchain state before the next one +// is attempted. +package main + +import ( + "context" + "crypto/ecdsa" + "encoding/json" + "flag" + "fmt" + "math/big" + "os" + "strings" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + coretypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" +) + +const facilityABIJSON = `[ + { + "inputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "name": "rootAccords", + "outputs": [ + {"internalType": "bytes32", "name": "id", "type": "bytes32"}, + {"internalType": "address", "name": "borrower", "type": "address"}, + {"internalType": "address", "name": "collateralAsset", "type": "address"}, + {"internalType": "address", "name": "liquidityAsset", "type": "address"}, + {"internalType": "uint256", "name": "targetCapacity", "type": "uint256"}, + {"internalType": "uint256", "name": "committedCapacity", "type": "uint256"}, + {"internalType": "uint256", "name": "drawnPrincipal", "type": "uint256"}, + {"internalType": "uint256", "name": "collateralLocked", "type": "uint256"}, + {"internalType": "uint64", "name": "validUntil", "type": "uint64"}, + {"internalType": "bytes32", "name": "policyHash", "type": "bytes32"}, + {"internalType": "bytes32", "name": "syndicationRoundId", "type": "bytes32"}, + {"internalType": "uint8", "name": "state", "type": "uint8"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "name": "childAccords", + "outputs": [ + {"internalType": "bytes32", "name": "id", "type": "bytes32"}, + {"internalType": "bytes32", "name": "rootId", "type": "bytes32"}, + {"internalType": "bytes32", "name": "allocationId", "type": "bytes32"}, + {"internalType": "address", "name": "provider", "type": "address"}, + {"internalType": "uint256", "name": "selectedCapacity", "type": "uint256"}, + {"internalType": "uint256", "name": "committedCapacity", "type": "uint256"}, + {"internalType": "uint256", "name": "drawnPrincipal", "type": "uint256"}, + {"internalType": "uint32", "name": "feeBps", "type": "uint32"}, + {"internalType": "uint64", "name": "validUntil", "type": "uint64"}, + {"internalType": "bytes32", "name": "termsCommitment", "type": "bytes32"}, + {"internalType": "uint8", "name": "state", "type": "uint8"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "name": "draws", + "outputs": [ + {"internalType": "bytes32", "name": "id", "type": "bytes32"}, + {"internalType": "bytes32", "name": "rootAccordId", "type": "bytes32"}, + {"internalType": "uint256", "name": "principal", "type": "uint256"}, + {"internalType": "uint256", "name": "repaidPrincipal", "type": "uint256"}, + {"internalType": "uint64", "name": "createdAt", "type": "uint64"}, + {"internalType": "bool", "name": "exists", "type": "bool"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "name": "getChildIds", + "outputs": [{"internalType": "bytes32[]", "name": "", "type": "bytes32[]"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "name": "getDrawLegIds", + "outputs": [{"internalType": "bytes32[]", "name": "", "type": "bytes32[]"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "name": "drawLegs", + "outputs": [ + {"internalType": "bytes32", "name": "id", "type": "bytes32"}, + {"internalType": "bytes32", "name": "drawId", "type": "bytes32"}, + {"internalType": "bytes32", "name": "childAccordId", "type": "bytes32"}, + {"internalType": "uint256", "name": "principal", "type": "uint256"}, + {"internalType": "uint256", "name": "repaidPrincipal", "type": "uint256"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "bytes32", "name": "rootId", "type": "bytes32"}], + "name": "availableCapacity", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "bytes32", "name": "childId", "type": "bytes32"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], + "name": "fundChild", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{"internalType": "bytes32", "name": "drawId", "type": "bytes32"}, {"internalType": "bytes32", "name": "rootId", "type": "bytes32"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], + "name": "draw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{"internalType": "bytes32", "name": "drawId", "type": "bytes32"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], + "name": "repay", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +]` + +const tokenABIJSON = `[ + { + "inputs": [{"internalType": "address", "name": "spender", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], + "name": "approve", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + } +]` + +type rootView struct { + ID common.Hash + Borrower common.Address + Target *big.Int + Committed *big.Int + Drawn *big.Int + State uint8 +} + +type childView struct { + ID common.Hash + Provider common.Address + Selected *big.Int + Committed *big.Int + Drawn *big.Int + State uint8 +} + +type drawView struct { + ID common.Hash + Principal *big.Int + Repaid *big.Int + Exists bool +} + +type txEvidence struct { + Label string `json:"label"` + Hash string `json:"hash"` +} + +type childEvidence struct { + ID string `json:"id"` + Provider string `json:"provider"` + Selected string `json:"selected"` + Committed string `json:"committed"` + Drawn string `json:"drawn"` + State uint8 `json:"state"` +} + +type flowEvidence struct { + Network string `json:"network"` + Facility string `json:"facility"` + LiquidityAsset string `json:"liquidityAsset"` + RootAccordID string `json:"rootAccordId"` + DrawID string `json:"drawId"` + DrawAmount string `json:"drawAmount"` + Funding []txEvidence `json:"funding"` + Draw *txEvidence `json:"draw,omitempty"` + Repayment *txEvidence `json:"repayment,omitempty"` + Children []childEvidence `json:"children"` + DrawLegCount int `json:"drawLegCount"` + RootStateAfter uint8 `json:"rootStateAfter"` + CommittedAfter string `json:"committedAfter"` + DrawnAfter string `json:"drawnAfter"` + AvailableAfter string `json:"availableAfter"` + Invariants map[string]bool `json:"invariants"` + CompletedAt string `json:"completedAt"` +} + +type providerKey struct { + Name string + Env string + Key *ecdsa.PrivateKey +} + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "facility-flow: %v\n", err) + os.Exit(1) + } +} + +func run() error { + flags := flag.NewFlagSet("facility-flow", flag.ContinueOnError) + rpcURL := flags.String("rpc", "", "chain RPC URL") + facilityText := flags.String("facility", "", "CapitalFacility address") + tokenText := flags.String("token", "", "USDT0 address") + rootText := flags.String("root-accord-id", "", "root Accord id") + drawText := flags.String("draw-id", "", "draw id; defaults to a deterministic Concord test id") + drawAmountText := flags.String("draw-amount", "4000000", "draw amount in token base units") + outFile := flags.String("out", "", "JSON evidence output path") + if err := flags.Parse(os.Args[1:]); err != nil { + return err + } + for name, value := range map[string]string{"rpc": *rpcURL, "facility": *facilityText, "token": *tokenText, "root-accord-id": *rootText, "out": *outFile} { + if strings.TrimSpace(value) == "" { + return fmt.Errorf("-%s is required", name) + } + } + facility, err := parseAddress(*facilityText, "facility") + if err != nil { + return err + } + token, err := parseAddress(*tokenText, "token") + if err != nil { + return err + } + rootID, err := parseHash(*rootText, "root-accord-id") + if err != nil { + return err + } + drawAmount, err := positive(*drawAmountText, "draw-amount") + if err != nil { + return err + } + drawID := common.HexToHash(*drawText) + if *drawText == "" { + drawID = crypto.Keccak256Hash([]byte("CONCORD_C2_E2E_DRAW_01")) + } else if drawID == (common.Hash{}) { + return fmt.Errorf("draw-id must be a non-zero bytes32 value") + } + + client, err := ethclient.Dial(*rpcURL) + if err != nil { + return fmt.Errorf("connect to RPC: %w", err) + } + defer client.Close() + ctx, cancel := context.WithTimeout(context.Background(), 12*time.Minute) + defer cancel() + chainID, err := client.ChainID(ctx) + if err != nil { + return fmt.Errorf("read chain id: %w", err) + } + if chainID.Uint64() != 114 { + return fmt.Errorf("expected Coston2 chain id 114, got %s", chainID) + } + facilityABI, err := abi.JSON(strings.NewReader(facilityABIJSON)) + if err != nil { + return fmt.Errorf("parse facility ABI: %w", err) + } + tokenABI, err := abi.JSON(strings.NewReader(tokenABIJSON)) + if err != nil { + return fmt.Errorf("parse token ABI: %w", err) + } + providerKeys := []providerKey{ + {Name: "provider_a", Env: "CONCORD_E2E_PROVIDER_A_PRIVATE_KEY"}, + {Name: "provider_b", Env: "CONCORD_E2E_PROVIDER_B_PRIVATE_KEY"}, + {Name: "provider_c", Env: "CONCORD_E2E_PROVIDER_C_PRIVATE_KEY"}, + } + for index := range providerKeys { + providerKeys[index].Key, err = privateKeyFromEnv(providerKeys[index].Env) + if err != nil { + return fmt.Errorf("%s: %w", providerKeys[index].Name, err) + } + } + + root, err := readRoot(ctx, client, facilityABI, facility, rootID) + if err != nil { + return err + } + if root.State != 3 && root.State != 4 { + return fmt.Errorf("root must be FUNDING or ACTIVE before facility flow; state=%d", root.State) + } + childIDs, err := readHashArray(ctx, client, facilityABI, facility, "getChildIds", rootID) + if err != nil { + return err + } + if len(childIDs) < 2 { + return fmt.Errorf("root has %d children; at least two are required for the draw", len(childIDs)) + } + children := make([]childView, 0, len(childIDs)) + for _, childID := range childIDs { + child, childErr := readChild(ctx, client, facilityABI, facility, childID) + if childErr != nil { + return childErr + } + children = append(children, child) + } + + evidence := flowEvidence{ + Network: "Coston2", + Facility: facility.Hex(), + LiquidityAsset: token.Hex(), + RootAccordID: rootID.Hex(), + DrawID: drawID.Hex(), + DrawAmount: drawAmount.String(), + Funding: []txEvidence{}, + Children: []childEvidence{}, + Invariants: map[string]bool{}, + } + for _, child := range children { + key, found := keyForProvider(providerKeys, child.Provider) + if !found { + return fmt.Errorf("no configured provider key matches child %s provider %s", child.ID.Hex(), child.Provider.Hex()) + } + if child.Committed.Cmp(child.Selected) > 0 { + return fmt.Errorf("child %s is overfunded", child.ID.Hex()) + } + remaining := new(big.Int).Sub(child.Selected, child.Committed) + if remaining.Sign() > 0 { + approveHash, approveErr := transact(ctx, client, tokenABI, token, key.Key, "approve", facility, child.Selected) + if approveErr != nil { + return fmt.Errorf("approve %s: %w", key.Name, approveErr) + } + evidence.Funding = append(evidence.Funding, txEvidence{Label: key.Name + " approve", Hash: approveHash.Hex()}) + fundHash, fundErr := transact(ctx, client, facilityABI, facility, key.Key, "fundChild", child.ID, remaining) + if fundErr != nil { + return fmt.Errorf("fund child %s: %w", child.ID.Hex(), fundErr) + } + evidence.Funding = append(evidence.Funding, txEvidence{Label: key.Name + " fundChild", Hash: fundHash.Hex()}) + } + } + + root, err = readRoot(ctx, client, facilityABI, facility, rootID) + if err != nil { + return err + } + if root.Committed.Cmp(root.Target) != 0 || root.State != 4 { + return fmt.Errorf("root did not activate after funding: state=%d committed=%s target=%s", root.State, root.Committed, root.Target) + } + + draw, err := readDraw(ctx, client, facilityABI, facility, drawID) + if err != nil { + return err + } + if !draw.Exists { + drawHash, drawErr := transact(ctx, client, facilityABI, facility, privateKeyFromTreasury(), "draw", drawID, rootID, drawAmount) + if drawErr != nil { + return fmt.Errorf("draw: %w", drawErr) + } + evidence.Draw = &txEvidence{Label: "treasury draw", Hash: drawHash.Hex()} + } else if draw.Principal.Cmp(drawAmount) != 0 { + return fmt.Errorf("existing draw %s has principal %s, expected %s", drawID.Hex(), draw.Principal, drawAmount) + } + + draw, err = readDraw(ctx, client, facilityABI, facility, drawID) + if err != nil { + return err + } + legIDs, err := readHashArray(ctx, client, facilityABI, facility, "getDrawLegIds", drawID) + if err != nil { + return err + } + if len(legIDs) < 2 { + return fmt.Errorf("draw %s has %d legs; expected at least two", drawID.Hex(), len(legIDs)) + } + evidence.DrawLegCount = len(legIDs) + + treasuryKey := privateKeyFromTreasury() + outstanding := new(big.Int).Sub(draw.Principal, draw.Repaid) + if outstanding.Sign() > 0 { + approveHash, approveErr := transact(ctx, client, tokenABI, token, treasuryKey, "approve", facility, outstanding) + if approveErr != nil { + return fmt.Errorf("treasury repayment approval: %w", approveErr) + } + evidence.Funding = append(evidence.Funding, txEvidence{Label: "treasury repay approve", Hash: approveHash.Hex()}) + repayHash, repayErr := transact(ctx, client, facilityABI, facility, treasuryKey, "repay", drawID, outstanding) + if repayErr != nil { + return fmt.Errorf("repay: %w", repayErr) + } + evidence.Repayment = &txEvidence{Label: "treasury repay", Hash: repayHash.Hex()} + } + + root, err = readRoot(ctx, client, facilityABI, facility, rootID) + if err != nil { + return err + } + children, err = readChildren(ctx, client, facilityABI, facility, childIDs) + if err != nil { + return err + } + available, err := readUint(ctx, client, facilityABI, facility, "availableCapacity", rootID) + if err != nil { + return err + } + childrenDrawn := new(big.Int) + committedChildren := new(big.Int) + for _, child := range children { + childrenDrawn.Add(childrenDrawn, child.Drawn) + if child.State != 5 && child.State != 6 { + committedChildren.Add(committedChildren, child.Committed) + } + evidence.Children = append(evidence.Children, childEvidence{ + ID: child.ID.Hex(), Provider: child.Provider.Hex(), Selected: child.Selected.String(), + Committed: child.Committed.String(), Drawn: child.Drawn.String(), State: child.State, + }) + } + draw, err = readDraw(ctx, client, facilityABI, facility, drawID) + if err != nil { + return err + } + evidence.RootStateAfter = root.State + evidence.CommittedAfter = root.Committed.String() + evidence.DrawnAfter = root.Drawn.String() + evidence.AvailableAfter = available.String() + evidence.Invariants = map[string]bool{ + "rootActive": root.State == 4, + "rootCommittedEqualsChild": root.Committed.Cmp(committedChildren) == 0, + "rootDrawnEqualsChildren": root.Drawn.Cmp(childrenDrawn) == 0, + "rootDrawnZeroAfterRepay": root.Drawn.Sign() == 0, + "drawFullyRepaid": draw.Repaid.Cmp(draw.Principal) == 0, + "capacityRestored": available.Cmp(root.Committed) == 0, + "multiChildDraw": evidence.DrawLegCount >= 2, + } + for name, ok := range evidence.Invariants { + if !ok { + return fmt.Errorf("facility invariant failed: %s", name) + } + } + evidence.CompletedAt = time.Now().UTC().Format(time.RFC3339) + if err := writeJSON(*outFile, evidence); err != nil { + return err + } + fmt.Printf("facility flow complete: draw=%s legs=%d available=%s evidence=%s\n", drawID.Hex(), evidence.DrawLegCount, available.String(), *outFile) + return nil +} + +// The treasury key is deliberately read at the final signing boundary from +// the same environment contract used by the rest of the Coston2 tools. +func privateKeyFromTreasury() *ecdsa.PrivateKey { + key, err := privateKeyFromEnv("DEPLOYMENT_PRIVATE_KEY") + if err != nil { + panic(err) + } + return key +} + +func keyForProvider(keys []providerKey, address common.Address) (providerKey, bool) { + for _, key := range keys { + if crypto.PubkeyToAddress(key.Key.PublicKey) == address { + return key, true + } + } + return providerKey{}, false +} + +func readRoot(ctx context.Context, client *ethclient.Client, contractABI abi.ABI, facility, rootID common.Hash) (rootView, error) { + values, err := call(ctx, client, contractABI, facility, "rootAccords", rootID) + if err != nil { + return rootView{}, err + } + if len(values) != 12 { + return rootView{}, fmt.Errorf("rootAccords returned %d values, expected 12", len(values)) + } + id, err := hashValue(values[0], "root id") + if err != nil { + return rootView{}, err + } + borrower, err := addressValue(values[1], "root borrower") + if err != nil { + return rootView{}, err + } + state, err := uint8Value(values[11], "root state") + if err != nil { + return rootView{}, err + } + target, ok := values[4].(*big.Int) + if !ok { + return rootView{}, fmt.Errorf("root target has type %T", values[4]) + } + committed, ok := values[5].(*big.Int) + if !ok { + return rootView{}, fmt.Errorf("root committed has type %T", values[5]) + } + drawn, ok := values[6].(*big.Int) + if !ok { + return rootView{}, fmt.Errorf("root drawn has type %T", values[6]) + } + return rootView{ID: id, Borrower: borrower, Target: target, Committed: committed, Drawn: drawn, State: state}, nil +} + +func readChild(ctx context.Context, client *ethclient.Client, contractABI abi.ABI, facility, childID common.Hash) (childView, error) { + values, err := call(ctx, client, contractABI, facility, "childAccords", childID) + if err != nil { + return childView{}, err + } + if len(values) != 11 { + return childView{}, fmt.Errorf("childAccords returned %d values, expected 11", len(values)) + } + id, err := hashValue(values[0], "child id") + if err != nil { + return childView{}, err + } + provider, err := addressValue(values[3], "child provider") + if err != nil { + return childView{}, err + } + selected, ok := values[4].(*big.Int) + if !ok { + return childView{}, fmt.Errorf("child selected has type %T", values[4]) + } + committed, ok := values[5].(*big.Int) + if !ok { + return childView{}, fmt.Errorf("child committed has type %T", values[5]) + } + drawn, ok := values[6].(*big.Int) + if !ok { + return childView{}, fmt.Errorf("child drawn has type %T", values[6]) + } + state, err := uint8Value(values[10], "child state") + if err != nil { + return childView{}, err + } + return childView{ID: id, Provider: provider, Selected: selected, Committed: committed, Drawn: drawn, State: state}, nil +} + +func readChildren(ctx context.Context, client *ethclient.Client, contractABI abi.ABI, facility common.Address, ids []common.Hash) ([]childView, error) { + children := make([]childView, 0, len(ids)) + for _, id := range ids { + child, err := readChild(ctx, client, contractABI, facility, id) + if err != nil { + return nil, err + } + children = append(children, child) + } + return children, nil +} + +func readDraw(ctx context.Context, client *ethclient.Client, contractABI abi.ABI, facility, drawID common.Hash) (drawView, error) { + values, err := call(ctx, client, contractABI, facility, "draws", drawID) + if err != nil { + return drawView{}, err + } + if len(values) != 6 { + return drawView{}, fmt.Errorf("draws returned %d values, expected 6", len(values)) + } + id, err := hashValue(values[0], "draw id") + if err != nil { + return drawView{}, err + } + principal, ok := values[2].(*big.Int) + if !ok { + return drawView{}, fmt.Errorf("draw principal has type %T", values[2]) + } + repaid, ok := values[3].(*big.Int) + if !ok { + return drawView{}, fmt.Errorf("draw repaid has type %T", values[3]) + } + exists, ok := values[5].(bool) + if !ok { + return drawView{}, fmt.Errorf("draw exists has type %T", values[5]) + } + return drawView{ID: id, Principal: principal, Repaid: repaid, Exists: exists}, nil +} + +func readUint(ctx context.Context, client *ethclient.Client, contractABI abi.ABI, facility common.Address, method string, arg common.Hash) (*big.Int, error) { + values, err := call(ctx, client, contractABI, facility, method, arg) + if err != nil { + return nil, err + } + if len(values) != 1 { + return nil, fmt.Errorf("%s returned %d values, expected one", method, len(values)) + } + value, ok := values[0].(*big.Int) + if !ok { + return nil, fmt.Errorf("%s returned %T", method, values[0]) + } + return value, nil +} + +func readHashArray(ctx context.Context, client *ethclient.Client, contractABI abi.ABI, facility common.Address, method string, arg common.Hash) ([]common.Hash, error) { + values, err := call(ctx, client, contractABI, facility, method, arg) + if err != nil { + return nil, err + } + if len(values) != 1 { + return nil, fmt.Errorf("%s returned %d values, expected one", method, len(values)) + } + result := []common.Hash{} + switch typed := values[0].(type) { + case [][32]byte: + for _, value := range typed { + result = append(result, common.BytesToHash(value[:])) + } + case []common.Hash: + result = append(result, typed...) + default: + return nil, fmt.Errorf("%s returned %T", method, values[0]) + } + return result, nil +} + +func transact(ctx context.Context, client *ethclient.Client, contractABI abi.ABI, address common.Address, key *ecdsa.PrivateKey, method string, args ...any) (common.Hash, error) { + chainID, err := client.ChainID(ctx) + if err != nil { + return common.Hash{}, err + } + opts, err := bind.NewKeyedTransactorWithChainID(key, chainID) + if err != nil { + return common.Hash{}, err + } + opts.Context = ctx + bound := bind.NewBoundContract(address, contractABI, client, client, client) + tx, err := bound.Transact(opts, method, args...) + if err != nil { + return common.Hash{}, err + } + receipt, err := bind.WaitMined(ctx, client, tx) + if err != nil { + return common.Hash{}, err + } + if receipt.Status != coretypes.ReceiptStatusSuccessful { + return common.Hash{}, fmt.Errorf("transaction %s reverted", tx.Hash().Hex()) + } + return tx.Hash(), nil +} + +func call(ctx context.Context, client *ethclient.Client, contractABI abi.ABI, address common.Address, method string, args ...any) ([]any, error) { + input, err := contractABI.Pack(method, args...) + if err != nil { + return nil, fmt.Errorf("pack %s: %w", method, err) + } + output, err := client.CallContract(ctx, ethereum.CallMsg{To: &address, Data: input}, nil) + if err != nil { + return nil, fmt.Errorf("call %s: %w", method, err) + } + values, err := contractABI.Unpack(method, output) + if err != nil { + return nil, fmt.Errorf("decode %s: %w", method, err) + } + return values, nil +} + +func privateKeyFromEnv(name string) (*ecdsa.PrivateKey, error) { + value := strings.TrimSpace(os.Getenv(name)) + if value == "" { + return nil, fmt.Errorf("%s is not set", name) + } + value = strings.TrimPrefix(strings.TrimPrefix(value, "0x"), "0X") + key, err := crypto.HexToECDSA(value) + if err != nil { + return nil, fmt.Errorf("decode %s: %w", name, err) + } + return key, nil +} + +func writeJSON(path string, value any) error { + data, err := json.MarshalIndent(value, "", " ") + if err != nil { + return err + } + data = append(data, '\n') + return os.WriteFile(path, data, 0600) +} + +func parseAddress(value, name string) (common.Address, error) { + if !common.IsHexAddress(value) || common.HexToAddress(value) == (common.Address{}) { + return common.Address{}, fmt.Errorf("%s must be a non-zero address", name) + } + return common.HexToAddress(value), nil +} + +func parseHash(value, name string) (common.Hash, error) { + if !strings.HasPrefix(value, "0x") || len(value) != 66 { + return common.Hash{}, fmt.Errorf("%s must be a 32-byte 0x value", name) + } + hash := common.HexToHash(value) + if hash == (common.Hash{}) { + return common.Hash{}, fmt.Errorf("%s must be non-zero", name) + } + return hash, nil +} + +func positive(value, name string) (*big.Int, error) { + number, ok := new(big.Int).SetString(value, 10) + if !ok || number.Sign() <= 0 { + return nil, fmt.Errorf("%s must be a positive decimal integer", name) + } + return number, nil +} + +func hashValue(value any, name string) (common.Hash, error) { + switch typed := value.(type) { + case [32]byte: + return common.BytesToHash(typed[:]), nil + case common.Hash: + return typed, nil + case []byte: + if len(typed) != 32 { + return common.Hash{}, fmt.Errorf("%s is not bytes32", name) + } + return common.BytesToHash(typed), nil + default: + return common.Hash{}, fmt.Errorf("%s has type %T", name, value) + } +} + +func addressValue(value any, name string) (common.Address, error) { + address, ok := value.(common.Address) + if !ok || address == (common.Address{}) { + return common.Address{}, fmt.Errorf("%s has type %T or is zero", name, value) + } + return address, nil +} + +func uint8Value(value any, name string) (uint8, error) { + switch typed := value.(type) { + case uint8: + return typed, nil + case uint16: + if typed > 255 { + return 0, fmt.Errorf("%s exceeds uint8", name) + } + return uint8(typed), nil + case uint32: + if typed > 255 { + return 0, fmt.Errorf("%s exceeds uint8", name) + } + return uint8(typed), nil + case *big.Int: + if typed.Sign() < 0 || !typed.IsUint64() || typed.Uint64() > 255 { + return 0, fmt.Errorf("%s exceeds uint8", name) + } + return uint8(typed.Uint64()), nil + default: + return 0, fmt.Errorf("%s has type %T", name, value) + } +} From ed6305a8345056c065e6038623fef4184333d18e Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 21:11:13 +0200 Subject: [PATCH 080/107] fix: gate facility flow on compiled tooling --- .github/workflows/ci.yml | 1 + tools/cmd/facility-flow/main.go | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 747aba9..bbac0fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -418,6 +418,7 @@ jobs: live_facility_flow: name: Coston2 funding, multi-child draw, and repayment if: github.event_name == 'workflow_dispatch' && inputs.execute_facility_flow == true && github.ref == 'refs/heads/agent/concord-rebuild' + needs: go permissions: contents: read concurrency: diff --git a/tools/cmd/facility-flow/main.go b/tools/cmd/facility-flow/main.go index 140a929..23df312 100644 --- a/tools/cmd/facility-flow/main.go +++ b/tools/cmd/facility-flow/main.go @@ -472,7 +472,7 @@ func keyForProvider(keys []providerKey, address common.Address) (providerKey, bo return providerKey{}, false } -func readRoot(ctx context.Context, client *ethclient.Client, contractABI abi.ABI, facility, rootID common.Hash) (rootView, error) { +func readRoot(ctx context.Context, client *ethclient.Client, contractABI abi.ABI, facility common.Address, rootID common.Hash) (rootView, error) { values, err := call(ctx, client, contractABI, facility, "rootAccords", rootID) if err != nil { return rootView{}, err @@ -507,7 +507,7 @@ func readRoot(ctx context.Context, client *ethclient.Client, contractABI abi.ABI return rootView{ID: id, Borrower: borrower, Target: target, Committed: committed, Drawn: drawn, State: state}, nil } -func readChild(ctx context.Context, client *ethclient.Client, contractABI abi.ABI, facility, childID common.Hash) (childView, error) { +func readChild(ctx context.Context, client *ethclient.Client, contractABI abi.ABI, facility common.Address, childID common.Hash) (childView, error) { values, err := call(ctx, client, contractABI, facility, "childAccords", childID) if err != nil { return childView{}, err @@ -554,7 +554,7 @@ func readChildren(ctx context.Context, client *ethclient.Client, contractABI abi return children, nil } -func readDraw(ctx context.Context, client *ethclient.Client, contractABI abi.ABI, facility, drawID common.Hash) (drawView, error) { +func readDraw(ctx context.Context, client *ethclient.Client, contractABI abi.ABI, facility common.Address, drawID common.Hash) (drawView, error) { values, err := call(ctx, client, contractABI, facility, "draws", drawID) if err != nil { return drawView{}, err From f41953ee23d078702b9fb9330185b9020578bdf4 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 21:22:49 +0200 Subject: [PATCH 081/107] docs: record complete Coston2 lifecycle evidence --- config/coston2/concord-deployment.json | 96 ++++++++++++++++++++++---- docs/CONCORD_STATUS.md | 62 ++++++++++++++--- docs/coston2-live-runbook.md | 30 ++++++-- docs/source-snapshot-2026-08-13.yaml | 27 ++++++-- 4 files changed, 180 insertions(+), 35 deletions(-) diff --git a/config/coston2/concord-deployment.json b/config/coston2/concord-deployment.json index 03509a6..0541f0b 100644 --- a/config/coston2/concord-deployment.json +++ b/config/coston2/concord-deployment.json @@ -1,7 +1,7 @@ { "network": "Coston2", "chainId": 114, - "observedAt": "2026-08-13T18:35:30Z", + "observedAt": "2026-08-13T19:14:40Z", "rpcUrl": "https://coston2-api.flare.network/ext/C/rpc", "explorerUrl": "https://coston2-explorer.flare.network", "teeRegistry": "0x1a9C4A0f9D76c0b1D91d22E24E573a9b377618aE", @@ -96,15 +96,15 @@ "usdt0AvailableToTreasury": true, "rootAccordCreated": true, "rootRoundAssetBindingCurrent": true, - "fccRoundExecuted": false, - "childrenMaterialized": false, - "facilityFunded": false, - "drawSettled": false, - "repaid": false + "fccRoundExecuted": true, + "childrenMaterialized": true, + "facilityFunded": true, + "drawSettled": true, + "repaid": true }, "warnings": [ "The previously deployed facility and root round use a separate 18-decimal token at 0x479854495cefBc8D12B971A3Ec4d18E6dbcE81a3; they are superseded for the current Coston2 USDT0 test flow.", - "The current faucet token is 0xC1A5B41512496B80903D1f32d6dEa3a73212E71F (USD₮0, 6 decimals). The replacement facility and root round are deployed, and provider gas is funded; FCC execution and quote/funding steps remain.", + "The current faucet token is 0xC1A5B41512496B80903D1f32d6dEa3a73212E71F (USD₮0, 6 decimals). The replacement facility completed the observed FCC, funding, multi-child draw, and repayment flow.", "The current Flare/Foundry toolchain still reports chain 114 as unsupported for EIP-3855; deployment succeeded after targeting solc 0.8.27, optimizer=true, optimizer_runs=200, and evm_version=paris. Keep this warning in the deployment record.", "The Coston2 FCC development path is not hardware-TEE evidence; the intended path uses the documented simulated development TEE.", "The Concord extension is registered through a stable Cloudflare Workers.dev ingress relay backed by the disposable Codespace public port; this is not a named Cloudflare Tunnel or a production ingress claim.", @@ -160,8 +160,8 @@ "url": "https://faucet.flare.network/", "perAddress": "100 C2FLR, 10 USDT0, 10 FXRP per 24 hours", "neededForNextStep": [ - "provider_a/provider_b/provider_c: approve and fund selected child allocations", - "FCC: active machine, reachable proxy, and current indexer credentials" + "frontend wiring and interface QA against the observed relationship state", + "optional named Cloudflare Tunnel if the Workers.dev development relay is replaced" ] }, "providerGasFunding": { @@ -178,8 +178,8 @@ "facility": "0xfaff601a18a9fca33378953515aa0f3ef9286ecd", "rootAccordId": "0x6e03af41b0194c5a369a50629474090cfc5b041a712144855e6efb1a574cfddd", "roundId": "0x7323280ede4786463744a755e4cddf9ce2a91be952d05d152679b58b67d176ce", - "rootState": "SYNDICATING", - "roundStatus": "OPEN", + "rootState": "ACTIVE", + "roundStatus": "FINALIZED", "borrower": "0x1aF42c70837f08c6C89a6FA274EB9eeF040820B3", "targetCapacity": "9000000", "targetDisplay": "9 USDT0", @@ -199,16 +199,84 @@ "openSyndication": "0x393174832b260cdd34a91a3df24f23066ac3672a64f1b0bcf25468236b7314c8" }, "workflowRun": "https://github.com/etvjay/Concord/actions/runs/31701379940", - "verifiedAt": "2026-08-13T12:43:43Z", + "verifiedAt": "2026-08-13T19:14:40Z", "verifiedOnchain": { "facilityFxrpBalance": "1", - "committedCapacity": "0", + "committedCapacity": "9000000", "drawnPrincipal": "0", "eligibleProviderCount": 3, "registryHasRoot": true, "rootChildIds": [ - "0x7323280ede4786463744a755e4cddf9ce2a91be952d05d152679b58b67d176ce" + "0x4335182fe9119ef6af37b5bd838f514d7a896a877be0a63578015b373a0bdfc0", + "0x2225a13ff17ac4f577d169d626b51e5b8311c01b336c6d5b09861a3363bfe97a", + "0xbf169c1b664684a8cfde0fea5a870e9358a903a1dd5042d650fe2d21bf43086a" ] + }, + "allocation": { + "resultDigest": "0xf17a292655b898d8b00c9794565972ed838cd3de2812f6fa59d877d6963c88ec", + "selectedProviders": [ + "0x8d9340278094DF3F032c674C912D8eA08c17437C", + "0xF2E25459108A0454a85d03848AD6059C271cc9a5", + "0x9537eA25C28e8c1663e8B57C52A0761a244183A1" + ], + "allocatedCapacity": ["3000000", "3000000", "3000000"], + "acceptedFeeBps": [610, 640, 680], + "submitQuoteTransactions": [ + "0xa6a200f2ecd1a3c06366ad2b88ecbc7046378a3f290ddab3e118a56298dbddb3", + "0x64f6eb5f6b94848718b7e3da4657e2fc68e5243aaa1527be4c90fc9f7fbd311b", + "0xef861e50be8dc58a47b8fdfe556597779ee56fbe436ecfe7ba8ecd483834d02a" + ], + "finalizeRoundTransaction": "0x2c5b06bf45e6318ece5262f30e5df91a8cbbacb4676f6efd997e714fcb3a8f0f", + "verifyAllocationTransaction": "0xb77400b70e8d54157fdb455b47797d5c583a387968afeb699319fe13fae95544", + "materializeTransaction": "0x4cdd56ca3a80f83b6333e4e0434d4f202fcb9cf5ad5dd3ced820a53779a7559a", + "workflowRun": "https://github.com/etvjay/Concord/actions/runs/31733200629", + "recoveryWorkflowRun": "https://github.com/etvjay/Concord/actions/runs/31733740564", + "evidenceArtifact": "https://github.com/etvjay/Concord/actions/runs/31733200629/artifacts/9194157272" + }, + "children": [ + { + "id": "0x4335182fe9119ef6af37b5bd838f514d7a896a877be0a63578015b373a0bdfc0", + "provider": "0x8d9340278094DF3F032c674C912D8eA08c17437C", + "selectedCapacity": "3000000", + "committedCapacity": "3000000", + "drawnPrincipal": "0", + "state": "ACTIVE" + }, + { + "id": "0x2225a13ff17ac4f577d169d626b51e5b8311c01b336c6d5b09861a3363bfe97a", + "provider": "0xF2E25459108A0454a85d03848AD6059C271cc9a5", + "selectedCapacity": "3000000", + "committedCapacity": "3000000", + "drawnPrincipal": "0", + "state": "ACTIVE" + }, + { + "id": "0xbf169c1b664684a8cfde0fea5a870e9358a903a1dd5042d650fe2d21bf43086a", + "provider": "0x9537eA25C28e8c1663e8B57C52A0761a244183A1", + "selectedCapacity": "3000000", + "committedCapacity": "3000000", + "drawnPrincipal": "0", + "state": "ACTIVE" + } + ], + "facilityLifecycle": { + "drawId": "0x85f634b07d8dfd83fe0f0f1a9b34504973e5e2d5e1e3886656bfb072c26f56ec", + "drawAmount": "4000000", + "drawLegCount": 2, + "fundingTransactions": [ + "0xfcb3ae5b2cdca68fbb974d30f1780defeb0ea65b49d3fe542e2b6ff395fd3bf0", + "0x061e3b64c8978a405cc22510bb9620263697860be4fa3e7badcb7feb4b2ebb0f", + "0xfbb3fa72ab3294807697a6aa973247c786f85021f30d41568f2300dc27b0be0c" + ], + "drawTransaction": "0xea0348757169428b73dbbc20eca7f2faf267c29941e5f0c0291bc3923b12cbe5", + "repaymentApprovalTransaction": "0x3ed2f0b47c3c853d38fb654167a3236877a402b422437285b993fd769031f213", + "repaymentTransaction": "0xe2146fb4c498964754b5754a1dd91992c62e29d3d627cd6fb0ebba6432d8c46c", + "finalRootState": "ACTIVE", + "finalCommittedCapacity": "9000000", + "finalDrawnPrincipal": "0", + "finalAvailableCapacity": "9000000", + "workflowRun": "https://github.com/etvjay/Concord/actions/runs/31734593116", + "evidenceArtifact": "https://github.com/etvjay/Concord/actions/runs/31734593116/artifacts/9194681063" } }, "supersededRootRound": { diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index 194e8b5..492ff77 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -1,6 +1,6 @@ # Concord status -Updated 2026-08-13. +Updated 2026-08-13 after the complete observed Coston2 lifecycle run. - The fresh source snapshot used for this checkpoint is [source-snapshot-2026-08-13.yaml](source-snapshot-2026-08-13.yaml). It records the current official Coston2 FCC development path, including simulated-TEE labeling, public HTTPS proxy exposure, and current indexer-credential requirements. @@ -49,6 +49,10 @@ The following values were read from the live Coston2 RPC and explorer on token: `AccordRegistry 0x68b19a3967760489b57341669cd7ea960b5f7367` and `CapitalFacility 0xfaff601a18a9fca33378953515aa0f3ef9286ecd`. The deployment receipts and registry-link transaction are in the evidence file. +- The canonical root is now `ACTIVE` with three `ACTIVE` child Accords, each + funded with `3 USDT0` base units (`3,000,000` at six decimals). The final + observed root state, child lineage, allocation digest, and lifecycle receipts + are recorded in the deployment file. ## Asset binding correction @@ -62,7 +66,7 @@ is a deployment contradiction, not a config-only change; the old facility and its 18-decimal root round are superseded. The replacement facility is now bound to the current token, and its current root round is recorded below. -## Not yet evidenced +## Live Coston2 vertical slice evidenced - One simulated development TEE machine is active for extension `66188` at status `2` (`PRODUCTION`): tee id @@ -75,18 +79,48 @@ bound to the current token, and its current root round is recorded below. units of the current six-decimal `USDT0 test` token. Each provider also has 105 C2FLR after the guarded gas-funding step, so approval and funding transactions can proceed. -- The replacement facility has a current SYNDICATING root Accord and OPEN - Makkari round: root `0x6e03af…cfddd`, round - `0x732328…76ce`, 1 FXRP locked, and a 9 USDT0 target at 6 decimals. The - four transaction receipts and workflow run are in the evidence file. +- The replacement facility has the current root Accord and Makkari round: + root `0x6e03af…cfddd`, round `0x732328…76ce`, 1 FXRP locked, and a 9 USDT0 + target at 6 decimals. The round is finalized and the root is now `ACTIVE`; + the creation, FCC, funding, draw, and repayment receipts are in the + deployment file. - The old facility's live round remains historical only because it uses the superseded 18-decimal liquidity token. -- No FCC quote/finalize instruction, allocation verification, child Accord, draw, - settlement, repayment, or restored-capacity receipt has been observed. +- Three signed provider quotes entered the confidential flow, CoFill finalized + the deterministic allocation, and the verified result digest is + `0xf17a292655b898d8b00c9794565972ed838cd3de2812f6fa59d877d6963c88ec`. + The signed action evidence, active TEE binding, and recovery materialization + run are linked from the deployment file. +- Child Accords were materialized for providers A, B, and C. Each provider then + transferred its exact `3,000,000` base-unit allocation, moving the root from + `FUNDING` to `ACTIVE` and increasing committed capacity to `9,000,000`. +- The treasury executed draw + `0x85f634b07d8dfd83fe0f0f1a9b34504973e5e2d5e1e3886656bfb072c26f56ec` for + `4,000,000` base units across two DrawLegs, then repaid the draw through a + real USDT0 transfer. Final observed state is root `ACTIVE`, committed + `9,000,000`, drawn `0`, and available `9,000,000`. +- The complete funding/draw/repayment job passed in + [GitHub Actions run 31734593116](https://github.com/etvjay/Concord/actions/runs/31734593116), + with the public evidence artifact at + [9194681063](https://github.com/etvjay/Concord/actions/runs/31734593116/artifacts/9194681063). +- The FCC round and materialization receipts are preserved in the earlier + [FCC run 31733200629](https://github.com/etvjay/Concord/actions/runs/31733200629), + its [signed action artifact 9194157272](https://github.com/etvjay/Concord/actions/runs/31733200629/artifacts/9194157272), + and the [materialization recovery run 31733740564](https://github.com/etvjay/Concord/actions/runs/31733740564). - The current Coston2 indexer configuration is stored as encrypted GitHub - Actions secrets using the current DevHub values. FCC registration is now - evidenced through the stable Workers.dev relay. This is a development ingress - fallback, not a named Cloudflare Tunnel or production hardware-TEE claim. + Actions secrets. FCC registration and the live instruction path use the + stable Workers.dev relay as a development ingress fallback, not a named + Cloudflare Tunnel or production hardware-TEE claim. + +## Not yet evidenced / intentionally bounded + +- Production hardware-backed TEE execution has not been claimed; this run used + the documented Coston2 simulated development TEE path. +- A named Cloudflare Tunnel or custom-domain ingress has not been provisioned; + the stable Workers.dev relay remains the current development ingress. +- The full consumer/institutional frontend and its browser-wallet UX are not + yet the source of the live evidence; the evidence above comes from the + contract/FCC runners and explorer receipts. ## Privacy truth @@ -163,3 +197,9 @@ Explorer links: - This run proves registration and live FCC ingress only. It does not prove quote submission, CoFill finalization, provider funding, draw settlement, repayment, or restored capacity. +- The live FCC round, signed-result verification, Child Accord materialization, + provider funding, two-leg draw, real USDT0 repayment, and restored-capacity + assertions passed in the [FCC recovery run](https://github.com/etvjay/Concord/actions/runs/31733740564) + and the [complete facility lifecycle run](https://github.com/etvjay/Concord/actions/runs/31734593116). + This is Coston2 simulated-development-TEE evidence with public settlement + receipts; it is not production hardware-TEE evidence. diff --git a/docs/coston2-live-runbook.md b/docs/coston2-live-runbook.md index d46777a..d2e6440 100644 --- a/docs/coston2-live-runbook.md +++ b/docs/coston2-live-runbook.md @@ -1,15 +1,19 @@ # Coston2 live integration runbook This is the operator path for the first truthful Concord vertical slice. It is -an execution checklist, not deployment evidence. The committed evidence remains -`not_observed` until the corresponding live receipts and explorer links exist. +an execution checklist, not deployment evidence by itself or a substitute for +the recorded deployment evidence. The observed +2026-08-13 Coston2 run is recorded in +`config/coston2/concord-deployment.json` and summarized in +`docs/CONCORD_STATUS.md`. A fresh run must produce its own receipts before it +changes the recorded status. ## Current gate -The repository is ready for a credential-gated Coston2 run. The read-only asset -resolver can run from GitHub Actions or a local Foundry installation. No private -key, proxy credential, deployment address, FCC registration, or settlement receipt -belongs in the repository. +The first complete credential-gated Coston2 run has passed. The read-only asset +resolver can still run from GitHub Actions or a local Foundry installation, and +the same guarded workflow can be repeated against a newly created disposable +environment. No private key or proxy credential belongs in the repository. The current development path may use `SIMULATED_TEE=true`. That proves the documented development integration path only; it is not production @@ -102,6 +106,13 @@ curl -s "$EXT_PROXY_URL/info" | jq '{extensionId, codeHash, platform}' If `platform` is the simulated development value or the code hash is documented as simulated, preserve that limitation in every demo and release note. +The observed run used +`https://concord-fcc-ingress.microcosm.workers.dev` as the stable Workers.dev +relay, extension `66188`, and simulated TEE id +`0xeE39d5e7d1C5043232282e3CC884B41a9Db22c85`. The relay is a development +fallback backed by the disposable Codespace proxy; it is not a named tunnel or +production ingress claim. + ### 4. Deploy and register in two phases Deploy the instruction sender first. Register the extension through the official @@ -162,6 +173,13 @@ Record only observed values: - whether the TEE path was simulated or hardware-backed; - any failure, retry, expiry, or replay result that affects the claim. +For the completed 2026-08-13 run, use the recorded +[facility lifecycle workflow](https://github.com/etvjay/Concord/actions/runs/31734593116), +[facility evidence artifact](https://github.com/etvjay/Concord/actions/runs/31734593116/artifacts/9194681063), +[FCC workflow](https://github.com/etvjay/Concord/actions/runs/31733200629), and +[materialization recovery workflow](https://github.com/etvjay/Concord/actions/runs/31733740564) +as the public CI evidence trail. The exact hashes remain in the deployment JSON. + ## Hard stop conditions - Asset resolver output differs from the centralized config. diff --git a/docs/source-snapshot-2026-08-13.yaml b/docs/source-snapshot-2026-08-13.yaml index 1c79609..951f98f 100644 --- a/docs/source-snapshot-2026-08-13.yaml +++ b/docs/source-snapshot-2026-08-13.yaml @@ -1,5 +1,5 @@ snapshot: - captured_at_utc: 2026-08-13T15:46:54Z + captured_at_utc: 2026-08-13T19:14:40Z freshness: FRESH environment: Coston2 chain_id: 114 @@ -46,8 +46,27 @@ snapshot: - No product-semantic contradiction was found. - Simulated TEE evidence remains development-path evidence, not production hardware-backed confidentiality. - FDC remains an evidence seam and is not added to the MVP. - live_gate_inputs_still_required: + live_gate_inputs_completed: - funded Coston2 deployment/proxy accounts - a stable public HTTPS EXT_PROXY_URL (the current guarded branch uses the Workers.dev relay fallback) - - current extension registration and active TEE-machine evidence (now observed for extension 66188) - - real provider funding, draw, repayment, and explorer receipts + - current extension registration and active TEE-machine evidence for extension 66188 + - signed quote submission, CoFill finalization, allocation verification, and child materialization + - real provider funding, multi-child draw, repayment, and explorer receipts + live_evidence: + extension_id: "0x000000000000000000000000000000000000000000000000000000000001028c" + tee_id: "0xeE39d5e7d1C5043232282e3CC884B41a9Db22c85" + facility: "0xfaff601a18a9fca33378953515aa0f3ef9286ecd" + root_accord_id: "0x6e03af41b0194c5a369a50629474090cfc5b041a712144855e6efb1a574cfddd" + round_id: "0x7323280ede4786463744a755e4cddf9ce2a91be952d05d152679b58b67d176ce" + allocation_digest: "0xf17a292655b898d8b00c9794565972ed838cd3de2812f6fa59d877d6963c88ec" + complete_lifecycle_run: "https://github.com/etvjay/Concord/actions/runs/31734593116" + evidence_artifact: "https://github.com/etvjay/Concord/actions/runs/31734593116/artifacts/9194681063" + final_root_state: ACTIVE + final_committed_capacity: 9000000 + final_drawn_principal: 0 + final_available_capacity: 9000000 + draw_leg_count: 2 + remaining_boundaries: + - production hardware-backed TEE execution is not evidenced; the run used the documented simulated development TEE + - a named Cloudflare Tunnel/custom-domain ingress is not provisioned; the stable Workers.dev relay is a development fallback + - the browser frontend and wallet UX are not the source of the live lifecycle evidence From 2a9a5210bf329596617032244f559d0faec54fe9 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 21:42:26 +0200 Subject: [PATCH 082/107] docs: add judge-ready evidence path --- docs/CONCORD_STATUS.md | 7 +-- docs/JUDGES.md | 101 +++++++++++++++++++++++++++++++++++++++++ docs/README.md | 2 + scripts/check-docs.sh | 2 +- 4 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 docs/JUDGES.md diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index 492ff77..3bd7e27 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -194,9 +194,10 @@ Explorer links: the official scaffold remained running, the Codespace proxy port was made public, the Worker relay was verified, the simulated TEE availability proof was obtained, and the machine was promoted to status `2`. -- This run proves registration and live FCC ingress only. It does not prove - quote submission, CoFill finalization, provider funding, draw settlement, - repayment, or restored capacity. +- That setup run proves registration and live FCC ingress. The later FCC + recovery and facility lifecycle runs below extend the evidence to quote + submission, CoFill finalization, provider funding, draw settlement, + repayment, and restored capacity. - The live FCC round, signed-result verification, Child Accord materialization, provider funding, two-leg draw, real USDT0 repayment, and restored-capacity assertions passed in the [FCC recovery run](https://github.com/etvjay/Concord/actions/runs/31733740564) diff --git a/docs/JUDGES.md b/docs/JUDGES.md new file mode 100644 index 0000000..926adf7 --- /dev/null +++ b/docs/JUDGES.md @@ -0,0 +1,101 @@ +# Concord judge quickstart + +This page is the shortest truthful path for evaluating Concord. A live Docker +endpoint is not required to inspect the completed Coston2 proof. The live FCC +runtime is an optional, scheduled verification path and must not be confused +with the recorded onchain evidence. + +## What Concord is + +Concord coordinates one FXRP-backed capital facility across independent +providers. A treasury creates a root Accord, provider terms enter a Makkari / +Flare Confidential Compute session, CoFill determines the allocation, and the +selected providers become independently governed child Accords. Funding, a +multi-child draw, repayment, and restored capacity remain queryable through the +root-child lineage. + +## Five-minute evaluation path + +1. Read the [current status](CONCORD_STATUS.md) and the + [deployment record](../config/coston2/concord-deployment.json). +2. Open the [complete facility lifecycle run](https://github.com/etvjay/Concord/actions/runs/31734593116) + and its [public evidence artifact](https://github.com/etvjay/Concord/actions/runs/31734593116/artifacts/9194681063). +3. Inspect the [FCC round run](https://github.com/etvjay/Concord/actions/runs/31733200629), + [signed action evidence](https://github.com/etvjay/Concord/actions/runs/31733200629/artifacts/9194157272), + and [materialization recovery run](https://github.com/etvjay/Concord/actions/runs/31733740564). +4. Verify the public Coston2 contract and settlement receipts in the + [Coston2 explorer](https://coston2-explorer.flare.network): + `CapitalFacility 0xfaff601a18a9fca33378953515aa0f3ef9286ecd` and + `AccordRegistry 0x68b19a3967760489b57341669cd7ea960b5f7367`. +5. Review the contract lifecycle tests and the deterministic CoFill tests in + `contracts/test/`, `go/`, and `tools/`. + +## Observed result + +The completed Coston2 flow used the six-decimal Coston2 USDT0 test token and +one FXRP collateral unit: + +| Stage | Observed result | +|---|---| +| Root Accord | `ACTIVE`; `9,000,000` base units committed | +| FCC / CoFill | Three signed provider offers; digest `0xf17a292655b898d8b00c9794565972ed838cd3de2812f6fa59d877d6963c88ec` | +| Child Accords | Three children, `3,000,000` selected and funded per provider | +| Draw | `4,000,000` base units across two DrawLegs | +| Repayment | Real USDT0 repayment transaction completed | +| Final capacity | `9,000,000` committed, `0` drawn, `9,000,000` available | + +The exact child IDs, transaction hashes, and invariant results are in the +deployment record and evidence artifact. + +## Live verification boundary + +Concord's recorded proof does not depend on a continuously running public app. +If a live FCC rerun is requested, the team must start the disposable Codespace +runtime, confirm the stable Workers.dev relay `/info` binding, and use the +credential-gated workflow. The runtime may be available only during a scheduled +judge window. + +The current relay is: + +`https://concord-fcc-ingress.microcosm.workers.dev` + +It is a development ingress fallback backed by the Codespace proxy. It is not a +named Cloudflare Tunnel, permanent production hosting, or a production +hardware-backed TEE. Restarting the simulated TEE can require a new identity +and fresh registration, so no restart should be made immediately before a +planned live window without re-running the registration checks. + +## What is and is not claimed + +Claimed: + +- Concord coordinates persistent capital relationships on Flare Coston2. +- Multiple providers contribute to one facility through child Accords. +- FXRP is used as root-facility collateral. +- FCC is used for confidential provider coordination. +- Public settlement and lineage receipts verify the economic lifecycle. + +Not claimed: + +- private FXRP or USDT0 transfers; +- fully private EVM execution; +- continuous public availability; +- production hardware-backed TEE security; +- production institutional readiness. + +Never send private keys, indexer credentials, GitHub tokens, or tunnel tokens to +judges. They are not required to inspect the recorded proof. + +## Reproducibility levels + +- **Repository level:** contract, Go, tooling, and documentation checks can be + run from a clean checkout. +- **Recorded-chain level:** judges can inspect the Coston2 transactions, + workflow runs, allocation digest, child lineage, draw, and repayment without + any private credential. +- **Live-FCC level:** a new confidential round requires the team-operated + credential-gated runtime and an agreed live verification window. + +The submission should be judged primarily from the first two levels. The third +level is an optional demonstration convenience, not a reason to claim that the +runtime is permanently hosted. diff --git a/docs/README.md b/docs/README.md index 13dd15f..a88d14b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -35,6 +35,8 @@ record before making a deployment or privacy claim. ## Evidence and truth - [CONCORD_STATUS.md](CONCORD_STATUS.md) — what is and is not evidenced. +- [JUDGES.md](JUDGES.md) — five-minute evaluation path, public evidence, and + live-verification boundaries. - [templates/coston2-evidence.template.json](templates/coston2-evidence.template.json) — blank evidence shape with `not_observed` defaults. - [canonical-reconciliation-audit.md](canonical-reconciliation-audit.md) — diff --git a/scripts/check-docs.sh b/scripts/check-docs.sh index 7c8ab02..231ea13 100755 --- a/scripts/check-docs.sh +++ b/scripts/check-docs.sh @@ -12,7 +12,7 @@ warn() { echo -e "${YELLOW}WARN${NC} $*"; } ok() { echo -e "${GREEN}ok${NC} $*"; } # These are the Concord documents that must exist in a clean checkout. -REQUIRED="architecture.md getting-started.md testing.md testing-against-coston2.md cloudflared.md CONCORD_STATUS.md shared-product-contract.md frontend-map.md cli.md coston2-live-runbook.md experiments/coston2-failure-matrix.md" +REQUIRED="architecture.md getting-started.md testing.md testing-against-coston2.md cloudflared.md CONCORD_STATUS.md JUDGES.md shared-product-contract.md frontend-map.md cli.md coston2-live-runbook.md experiments/coston2-failure-matrix.md" MAX_LINES=500 echo "docs: $DOCS" From c5c81b9687a24507b501b862b438d66f525bb7ff Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 13 Aug 2026 21:50:41 +0200 Subject: [PATCH 083/107] docs: reconcile frontend navigation direction --- docs/frontend-design-system.md | 3 ++- docs/frontend-map.md | 31 ++++++++++++++++++------------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/docs/frontend-design-system.md b/docs/frontend-design-system.md index f2ffe5e..4d9d60d 100644 --- a/docs/frontend-design-system.md +++ b/docs/frontend-design-system.md @@ -428,7 +428,8 @@ secondary cue only. ### Desktop -- Persistent left navigation for facilities, activity, evidence, and settings. +- One stable global header for facilities, activity, evidence, network, wallet, + and settings; no permanent left sidebar in the first version. - Relationship workspace uses the full container; metrics stay above the fold. - Relationship spine is horizontal; child table remains tabular. diff --git a/docs/frontend-map.md b/docs/frontend-map.md index 34188a7..e711ede 100644 --- a/docs/frontend-map.md +++ b/docs/frontend-map.md @@ -9,21 +9,26 @@ implementation follows the live contract/FCC integration evidence gate. ```text ┌──────────────────────────────────────────────────────────────────────┐ │ Concord Facilities Activity Evidence Network Wallet │ -├───────────────┬──────────────────────────────────────────────────────┤ -│ role switcher │ Root Accord │ -│ │ FXRP collateral · committed · drawn · available │ -│ Treasury │ expiry · state explanation · next permitted action │ -│ Provider ├──────────────────────────────────────────────────────┤ -│ Institution │ Relationship map │ -│ Auditor │ Root → Makkari → CoFill → children → draw legs │ -│ Observer ├──────────────────────────────────────────────────────┤ -│ │ Child Accords │ -│ guided/detail │ provider · selected · funded · drawn · available │ -│ ├──────────────────────────────────────────────────────┤ -│ │ Activity / evidence / action review │ -└───────────────┴──────────────────────────────────────────────────────┘ +├──────────────────────────────────────────────────────────────────────┤ +│ facility context · role/organization · guided/detail │ +├──────────────────────────────────────────────────────────────────────┤ +│ Root Accord · state · FXRP collateral · committed · drawn · available │ +│ expiry · state explanation · next permitted action │ +├──────────────────────────────────────────────────────────────────────┤ +│ Relationship spine: Root → Makkari → CoFill → children → draw legs │ +├──────────────────────────────────────────────────────────────────────┤ +│ Child Accords · provider · selected · funded · drawn · available │ +├──────────────────────────────────────────────────────────────────────┤ +│ Activity · evidence · action review │ +└──────────────────────────────────────────────────────────────────────┘ ``` +The first version uses one stable global header and no permanent desktop +sidebar. Role and organization context are shown in the facility header or +context controls, while the primary action remains in the relationship +workspace. On mobile, the global header collapses into the accessible hamburger +drawer defined in the art direction. + The relationship view is the primary screen. A chart or graph is subordinate to readable relationship cards and exact amounts; the graph is not decoration and cannot be the only way to understand causality. From bb484729c40c7af883abaa9268ad126da239c82d Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 14 Aug 2026 07:12:41 +0200 Subject: [PATCH 084/107] feat: build institutional frontend and hosted FCC path --- .dockerignore | 8 + .github/workflows/frontend.yml | 40 + .github/workflows/hosted-fcc-smoke.yml | 32 + .gitignore | 3 + README.md | 20 +- config/coston2/concord-deployment.json | 33 + docs/CONCORD_STATUS.md | 22 +- docs/fcc-always-on-hosting.md | 137 ++ docs/frontend-art-direction.md | 23 +- docs/frontend-design-system.md | 14 +- docs/source-snapshot-2026-08-14.yaml | 55 + frontend/Dockerfile | 22 + frontend/README.md | 60 +- frontend/index.html | 17 + frontend/nginx.conf | 17 + frontend/package-lock.json | 2560 ++++++++++++++++++++++++ frontend/package.json | 32 + frontend/src/App.test.tsx | 61 + frontend/src/App.tsx | 731 +++++++ frontend/src/data/concord.ts | 278 +++ frontend/src/global.d.ts | 1 + frontend/src/main.tsx | 26 + frontend/src/styles.css | 474 +++++ frontend/src/test/setup.ts | 5 + frontend/tsconfig.json | 24 + frontend/vite.config.ts | 21 + infra/railway/proxy.Dockerfile | 22 + infra/railway/proxy.railway.toml | 8 + infra/railway/start-proxy.sh | 51 + infra/railway/tee.railway.toml | 6 + scripts/check-hosted-fcc.sh | 43 + 31 files changed, 4803 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/frontend.yml create mode 100644 .github/workflows/hosted-fcc-smoke.yml create mode 100644 docs/fcc-always-on-hosting.md create mode 100644 docs/source-snapshot-2026-08-14.yaml create mode 100644 frontend/Dockerfile create mode 100644 frontend/index.html create mode 100644 frontend/nginx.conf create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/App.test.tsx create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/data/concord.ts create mode 100644 frontend/src/global.d.ts create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/styles.css create mode 100644 frontend/src/test/setup.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts create mode 100644 infra/railway/proxy.Dockerfile create mode 100644 infra/railway/proxy.railway.toml create mode 100755 infra/railway/start-proxy.sh create mode 100644 infra/railway/tee.railway.toml create mode 100755 scripts/check-hosted-fcc.sh diff --git a/.dockerignore b/.dockerignore index a322541..7ac1564 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,6 +10,11 @@ testing/ cache/ out/ config/ +!config/ +config/** +!config/coston2/ +config/coston2/** +!config/coston2/concord-deployment.json contracts/ scripts/ proxy/ @@ -17,3 +22,6 @@ foundry.toml docker-compose*.yaml Dockerfile* .dockerignore +frontend/node_modules/ +frontend/dist/ +frontend/*.tsbuildinfo diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 0000000..e6b3e33 --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,40 @@ +name: Concord frontend + +on: + push: + paths: + - "frontend/**" + - "sdk/typescript/**" + - "config/coston2/concord-deployment.json" + - ".github/workflows/frontend.yml" + pull_request: + paths: + - "frontend/**" + - "sdk/typescript/**" + - "config/coston2/concord-deployment.json" + - ".github/workflows/frontend.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "24.19.0" + cache: npm + cache-dependency-path: frontend/package-lock.json + - name: Install exact frontend dependencies + working-directory: frontend + run: npm ci --ignore-scripts --no-audit --no-fund + - name: Typecheck, test, and build + working-directory: frontend + run: | + npm run build + npm test + - name: Build production container + run: docker build -f frontend/Dockerfile -t concord-frontend:${{ github.sha }} . diff --git a/.github/workflows/hosted-fcc-smoke.yml b/.github/workflows/hosted-fcc-smoke.yml new file mode 100644 index 0000000..a0bfa6c --- /dev/null +++ b/.github/workflows/hosted-fcc-smoke.yml @@ -0,0 +1,32 @@ +name: Hosted FCC read-only smoke check + +on: + workflow_dispatch: + inputs: + proxy_url: + description: "Stable public HTTPS URL of the extension proxy" + required: true + type: string + extension_id: + description: "Expected Concord FCC extension ID" + required: true + default: "66188" + type: string + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.25.1" + cache-dependency-path: tools/go.sum + - name: Confirm proxy binding and active TEE registry state + env: + EXT_PROXY_URL: ${{ inputs.proxy_url }} + CONCORD_EXTENSION_ID: ${{ inputs.extension_id }} + run: ./scripts/check-hosted-fcc.sh diff --git a/.gitignore b/.gitignore index cc512d9..94ed673 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,9 @@ tools/pkg/contracts/*/*.bin # anchor for `npm ci` in typescript/Dockerfile. typescript/node_modules/ typescript/dist/ +frontend/node_modules/ +frontend/dist/ +frontend/*.tsbuildinfo python/__pycache__/ python/**/__pycache__/ python/.venv/ diff --git a/README.md b/README.md index 5d2f761..2bfc459 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,9 @@ repayments are explicit nodes in its lineage. - The FCC extension requires ECIES-encrypted payloads through the TEE decrypt endpoint. Local unit tests inject a decrypt function; they do not turn the production handler into a plaintext path. +- The React frontend renders the recorded Coston2 Root Accord, Makkari/CoFill + formation, three funded Child Accords, explicit draw legs, repayment, + restored capacity, and evidence boundary through one shared SDK model. ## Truth boundary @@ -76,8 +79,20 @@ pre-build → start-services --chain coston2 → post-build → run-test The observed Coston2 sender, FCC extension registration, and facility deployment receipts are recorded in [docs/CONCORD_STATUS.md](docs/CONCORD_STATUS.md). The -complete Coston2 proof is not claimed until an active FCC machine, funded -provider offers, and the full settlement/repayment lineage are observed. +complete active-machine, provider-funding, multi-child settlement, repayment, +and restored-capacity proof is recorded there with transaction and workflow +evidence. + +Build the frontend from its exact lockfile: + +```bash +(cd frontend && npm ci --ignore-scripts --no-audit --no-fund) +(cd frontend && npm run build && npm test) +``` + +The current always-on FCC development-host plan and safe simulated-TEE identity +cutover are in +[docs/fcc-always-on-hosting.md](docs/fcc-always-on-hosting.md). ## Repository map @@ -97,6 +112,7 @@ scripts/ scaffold lifecycle and binding commands api/ REST/OpenAPI integration contract sdk/typescript/ typed read and unsigned-intent client frontend/ frontend boundary and implementation map +infra/railway/ long-running FCC proxy/TEE deployment config ``` ## Shared surfaces diff --git a/config/coston2/concord-deployment.json b/config/coston2/concord-deployment.json index 0541f0b..6e3ba8c 100644 --- a/config/coston2/concord-deployment.json +++ b/config/coston2/concord-deployment.json @@ -184,7 +184,12 @@ "targetCapacity": "9000000", "targetDisplay": "9 USDT0", "targetDecimals": 6, + "validUntilUnix": "1787229810", + "validUntil": "2026-08-20T12:43:30Z", + "policyHash": "0xcb41179c81b6d45a22487986ae6ca4faa1c025ff4492c02ce3d26c5b0de8443e", "maxFeeBps": 700, + "roundExpiryUnix": "1786884210", + "roundExpiry": "2026-08-16T12:43:30Z", "collateralLocked": "1000000", "collateralDisplay": "1 FXRP", "eligibleProviders": [ @@ -236,26 +241,38 @@ "children": [ { "id": "0x4335182fe9119ef6af37b5bd838f514d7a896a877be0a63578015b373a0bdfc0", + "allocationId": "0xf17a292655b898d8b00c9794565972ed838cd3de2812f6fa59d877d6963c88ec", "provider": "0x8d9340278094DF3F032c674C912D8eA08c17437C", "selectedCapacity": "3000000", "committedCapacity": "3000000", "drawnPrincipal": "0", + "feeBps": 610, + "validUntilUnix": "1786884210", + "termsCommitment": "0x73ba825304d56f474d450c58780aa250b5cd98e0924e9a56c79d2d1f5b0988bb", "state": "ACTIVE" }, { "id": "0x2225a13ff17ac4f577d169d626b51e5b8311c01b336c6d5b09861a3363bfe97a", + "allocationId": "0xf17a292655b898d8b00c9794565972ed838cd3de2812f6fa59d877d6963c88ec", "provider": "0xF2E25459108A0454a85d03848AD6059C271cc9a5", "selectedCapacity": "3000000", "committedCapacity": "3000000", "drawnPrincipal": "0", + "feeBps": 640, + "validUntilUnix": "1786884210", + "termsCommitment": "0x12730a1f45e22aec3175729182a7192e4ed51cc07b5493e4f4d8f5185c419f6a", "state": "ACTIVE" }, { "id": "0xbf169c1b664684a8cfde0fea5a870e9358a903a1dd5042d650fe2d21bf43086a", + "allocationId": "0xf17a292655b898d8b00c9794565972ed838cd3de2812f6fa59d877d6963c88ec", "provider": "0x9537eA25C28e8c1663e8B57C52A0761a244183A1", "selectedCapacity": "3000000", "committedCapacity": "3000000", "drawnPrincipal": "0", + "feeBps": 680, + "validUntilUnix": "1786884210", + "termsCommitment": "0xa4af953d87f72ee095b6ba6940055ee7f8fdfcd10c278278d60e10774569a7d7", "state": "ACTIVE" } ], @@ -263,6 +280,22 @@ "drawId": "0x85f634b07d8dfd83fe0f0f1a9b34504973e5e2d5e1e3886656bfb072c26f56ec", "drawAmount": "4000000", "drawLegCount": 2, + "drawCreatedAtUnix": "1786648474", + "drawCreatedAt": "2026-08-13T19:14:34Z", + "drawLegs": [ + { + "id": "0xb9afb4dc836bc1dacfbd810b7a4fa69c3d015f76a9ad8cc6057dfb994548b791", + "childAccordId": "0x4335182fe9119ef6af37b5bd838f514d7a896a877be0a63578015b373a0bdfc0", + "principal": "3000000", + "repaidPrincipal": "3000000" + }, + { + "id": "0xfabc9644b2e712870eddc4e6d195912bb34b56f0bc2a90a3b72559b3fb5835a1", + "childAccordId": "0x2225a13ff17ac4f577d169d626b51e5b8311c01b336c6d5b09861a3363bfe97a", + "principal": "1000000", + "repaidPrincipal": "1000000" + } + ], "fundingTransactions": [ "0xfcb3ae5b2cdca68fbb974d30f1780defeb0ea65b49d3fe542e2b6ff395fd3bf0", "0x061e3b64c8978a405cc22510bb9620263697860be4fa3e7badcb7feb4b2ebb0f", diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index 3bd7e27..581f47f 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -1,8 +1,10 @@ # Concord status -Updated 2026-08-13 after the complete observed Coston2 lifecycle run. +Updated 2026-08-14 after the first frontend implementation and always-on FCC +hosting package. The complete observed Coston2 lifecycle remains the canonical +deployment proof. -- The fresh source snapshot used for this checkpoint is [source-snapshot-2026-08-13.yaml](source-snapshot-2026-08-13.yaml). It records the current official Coston2 FCC development path, including simulated-TEE labeling, public HTTPS proxy exposure, and current indexer-credential requirements. +- The fresh source snapshot used for this checkpoint is [source-snapshot-2026-08-14.yaml](source-snapshot-2026-08-14.yaml). It records the current official Coston2 FCC development path and the Railway/Northflank availability decision. ## Works in the durable checkpoint @@ -28,6 +30,13 @@ Updated 2026-08-13 after the complete observed Coston2 lifecycle run. - `scripts/coston2-preflight.sh` provides a read-only offline/live operator gate, and the adversarial cases are recorded in `docs/experiments/coston2-failure-matrix.md`. +- The React/Vite frontend now implements the institutional landing page, + facility register, Root Accord workspace, funding, activity, evidence, + lineage, child, and draw views from the recorded Coston2 deployment and the + shared TypeScript SDK types. Its build and four semantic tests pass locally. +- `infra/railway/` and `docs/fcc-always-on-hosting.md` package the official + three-service FCC topology for a stable Railway deployment, with Northflank + documented as the fallback. No new hosted machine has been registered yet. ## Observed Coston2 deployment @@ -118,9 +127,12 @@ bound to the current token, and its current root round is recorded below. the documented Coston2 simulated development TEE path. - A named Cloudflare Tunnel or custom-domain ingress has not been provisioned; the stable Workers.dev relay remains the current development ingress. -- The full consumer/institutional frontend and its browser-wallet UX are not - yet the source of the live evidence; the evidence above comes from the - contract/FCC runners and explorer receipts. +- The institutional read frontend is implemented, but browser-wallet intent + generation and transaction submission remain disconnected. The evidence + above still comes from the contract/FCC runners and explorer receipts. +- The Railway/Northflank runtime bundle is reproducible but not yet deployed; + a fresh hosted simulated TEE identity must be registered and the stale + machine paused before the endpoint can be called continuously available. ## Privacy truth diff --git a/docs/fcc-always-on-hosting.md b/docs/fcc-always-on-hosting.md new file mode 100644 index 0000000..17bab44 --- /dev/null +++ b/docs/fcc-always-on-hosting.md @@ -0,0 +1,137 @@ +# Always-on Concord FCC development hosting + +This runbook moves Concord's current Coston2 FCC development runtime from the +disposable Codespace relay to a stable, long-running host. It does not alter +Accord, Makkari, CoFill, Lineage, or the completed facility evidence. + +## Recommended topology: Railway + +Railway is the primary target because it supports Dockerfile builds, +configuration-as-code, private service DNS, stable public domains, and an +`ALWAYS` restart policy. `ALWAYS` requires a paid plan; free/trial deployments +must not be described as continuously available. + +Create one Railway project with three services: + +1. `concord-fcc-proxy` from this repository, using + `infra/railway/proxy.railway.toml`. Give port `6664` a stable public HTTPS + domain and set `PORT=6664`. +2. `concord-fcc-tee` from this repository, using + `infra/railway/tee.railway.toml`. Keep it private. +3. A private Redis service. Persistence is not required for the current + ephemeral development queue; do not expose Redis publicly. + +The provider-facing path is direct: + +```text +Flare providers + -> https://stable-proxy-host/instruction :6664 + -> tee-proxy + -> private Railway DNS :6663 + -> Concord Go extension TEE +``` + +### Proxy variables + +Store these as Railway secrets/variables. Never commit their values. + +```text +PROXY_PRIVATE_KEY +REDIS_ENDPOINT=concord-redis.railway.internal:6379 +INDEXER_DB_HOST +INDEXER_DB_PORT=3306 +INDEXER_DB_NAME +INDEXER_DB_USER +INDEXER_DB_PASSWORD +PORT=6664 +``` + +The runtime renderer writes the proxy TOML inside the container. Current +Coston2 system addresses are defaults in that renderer and are recorded in the +fresh source snapshot. Override `FLARE_SYSTEMS_MANAGER`, `FLARE_RELAY`, and +`VOTER_REGISTRY` if a newer official scaffold changes them. + +### TEE variables + +```text +CHAIN_ID=114 +CHAIN_URL=https://coston2-api.flare.network/ext/C/rpc +MODE=1 +SIMULATED_TEE=true +EXTENSION_ID=0x000000000000000000000000000000000000000000000000000000000001028c +INITIAL_OWNER= +GOVERNANCE_SIGNERS= +GOVERNANCE_THRESHOLD=1 +PROXY_URL=http://concord-fcc-proxy.railway.internal:6663 +CONFIG_PORT=5501 +SIGN_PORT=7701 +EXTENSION_PORT=7702 +LOG_LEVEL=INFO +``` + +Service names determine Railway private DNS. If the proxy service has another +name, update `PROXY_URL` accordingly. + +## Identity and cutover rule + +The stable hostname survives a restart; the simulated TEE identity does not. +Restarting `concord-fcc-tee` creates a new `teeId`. The safe cutover is: + +1. Deploy all three services and verify the public `/info` response reports + extension `66188`. +2. Read the new TEE identity from `/info` and register it through the current + official scaffold tooling. +3. Wait for status `2` (`PRODUCTION`) and a fresh availability check. +4. Pause the stale identity. Maintain exactly one active machine for extension + `66188` and one machine per endpoint. +5. Run `./scripts/check-hosted-fcc.sh https://your-stable-domain 66188`. +6. Dispatch a new development-path instruction and retain the transaction, + proxy status, and result evidence before updating deployment records. + +Do not automate registration on every restart: that can accumulate stale active +machines and make provider selection appear random. Registration and stale +identity pause remain deliberate operator actions. + +The currently recorded machine is +`0xeE39d5e7d1C5043232282e3CC884B41a9Db22c85`, proxy identity +`0x801470C95f78D0cA444e589aF8Ea0858Ce6d613e`, status `2`, using the +Workers.dev-to-Codespace development relay. It remains historical evidence; it +is not evidence that the old upstream is continuously available. + +## Northflank fallback + +Northflank can run the same two Dockerfiles as long-running combined services. +Expose only proxy port `6664` with TLS, keep proxy port `6663`, the TEE ports, +and Redis private, configure an HTTP `/info` health check, and keep the service +scale at one or greater. Scale zero is unavailable and is not always-on. + +Use the same variables and the same identity cutover. Northflank changes the +service DNS name only; it does not change Concord or FCC semantics. + +## Reproducibility and operations + +- `frontend.yml` reproduces the frontend typecheck, semantic tests, bundle, and + production container. +- `hosted-fcc-smoke.yml` performs a manual, read-only `/info` and active-machine + registry check against any stable deployment. +- `proxy.Dockerfile` pins `tee-proxy` to the same `v0.0.18` line as the current + Concord extension and does not mix Flare dependency versions. +- Railway configuration declares `ALWAYS` restart. Use a paid plan and alerts; + restart policy alone is not a service-level guarantee. + +## Truth boundary + +This setup can improve development availability. It does not prove production +hardware-backed TEE security, private token transfers, private EVM execution, +or institutional production readiness. Coston2 USDT0/FXRP transfers and public +contract state remain public. + +Official platform references used for this decision: + +- Flare FCE scaffold: https://github.com/flare-foundation/fce-extension-scaffold +- Flare FCC getting started: https://dev.flare.network/fcc/guides/getting-started +- Railway config-as-code: https://docs.railway.com/config-as-code/reference +- Railway private networking: https://docs.railway.com/networking/private-networking +- Railway restart policy: https://docs.railway.com/deployments/restart-policy +- Northflank services: https://northflank.com/features/application-layer +- Northflank health checks: https://northflank.com/docs/v1/application/observe/configure-health-checks diff --git a/docs/frontend-art-direction.md b/docs/frontend-art-direction.md index 04df119..955e13f 100644 --- a/docs/frontend-art-direction.md +++ b/docs/frontend-art-direction.md @@ -1,6 +1,7 @@ # Concord frontend art direction and information architecture -Status: design phase, before React implementation. +Status: first React implementation checkpoint, updated from the supplied +institutional porcelain reference. This document turns the first visual territory exploration into a complete product direction. It governs the webapp's visual language, navigation, @@ -58,21 +59,17 @@ product than to a crypto exchange. ### Primary recommendation -Use **IBM Plex Sans** for interface, headings, labels, and body copy, paired -with **IBM Plex Mono** for IDs, hashes, addresses, block references, and exact -base-unit values. - -This pairing is appropriate because the IBM Plex family is designed for UI -environments, is available under the SIL Open Font License, and provides broad -script coverage including Latin, Arabic, Chinese, Cyrillic, Devanagari, Greek, -Hebrew, Japanese, Korean, and Thai. The product must still test actual -fallback rendering for every supported locale. +Use **Inter Variable** for interface, headings, labels, numerals, and body copy, +paired with the platform monospace stack for IDs, hashes, addresses, block +references, and exact base-unit values. Inter is self-hosted in the frontend +bundle; the system fallback remains responsible for scripts outside its +coverage and must be tested before a locale is declared supported. ### Type roles ```text -Plex Sans primary UI, headings, explanations, navigation, buttons -Plex Mono IDs, hashes, addresses, exact units, technical evidence +Inter Variable primary UI, headings, explanations, navigation, buttons +System mono IDs, hashes, addresses, exact units, technical evidence System fallback unavailable font or unsupported glyph fallback ``` @@ -114,7 +111,7 @@ names, large amounts, and translated labels before being frozen. --pending-surface: #fff1d5; --blocked: #b8495b; --blocked-surface: #fbe7eb; - --focus: #145fd7; + --focus: #205cf3; } ``` diff --git a/docs/frontend-design-system.md b/docs/frontend-design-system.md index 4d9d60d..4579065 100644 --- a/docs/frontend-design-system.md +++ b/docs/frontend-design-system.md @@ -1,7 +1,7 @@ # Concord frontend design system -Status: design specification only. This document defines the interface before -the React implementation begins. It must remain subordinate to +Status: first React implementation checkpoint. This document defines and now +audits the implemented interface. It remains subordinate to [`shared-product-contract.md`](./shared-product-contract.md) and [`frontend-map.md`](./frontend-map.md). @@ -81,8 +81,10 @@ economic relationship. decoration. - One proof accent is reserved for verified/observed states. Pending and blocked states use separate semantic colors plus text and icons. -- No rainbow gradients, crypto-market candlesticks, floating neon cards, - decorative 3D objects, or anonymous “AI” surfaces. +- No crypto-market candlesticks, floating neon cards, or anonymous “AI” + surfaces. One restrained spectral Root Accord artifact is permitted on the + public landing page as the persistent relationship signature; it does not + enter data-dense workspace views. - Elevation is restrained: borders and surface contrast carry hierarchy; shadows are soft and rare. @@ -91,11 +93,11 @@ should communicate precision and calm, not yield or price excitement. ### Layout constraints -The desktop content container is `1120px` maximum with responsive horizontal +The desktop workspace content container is `1320px` maximum with responsive horizontal padding: ```css ---container-max: 1120px; +--container-max: 1320px; --container-pad: clamp(20px, 4vw, 48px); ``` diff --git a/docs/source-snapshot-2026-08-14.yaml b/docs/source-snapshot-2026-08-14.yaml new file mode 100644 index 0000000..1a1f68a --- /dev/null +++ b/docs/source-snapshot-2026-08-14.yaml @@ -0,0 +1,55 @@ +snapshot: + captured_at_utc: 2026-08-14T05:09:35Z + freshness: FRESH + environment: Coston2 + chain_id: 114 + tee_mode: simulated + hardware_attestation_claim: false + official_flare_sources: + skills_repository: + url: https://github.com/flare-foundation/flare-ai-skills + revision: 0bd60deb977df97455da720df1aaf765480b8141 + fce_scaffold: + url: https://github.com/flare-foundation/fce-extension-scaffold + revision: e3f587949069780084e2ced8a53c9419ed05c250 + developer_hub: + - https://dev.flare.network/fcc/guides/getting-started + - https://dev.flare.network/network/overview + hosting_sources: + railway: + - https://docs.railway.com/config-as-code/reference + - https://docs.railway.com/networking/private-networking + - https://docs.railway.com/builds/dockerfiles + - https://docs.railway.com/deployments/restart-policy + northflank: + - https://northflank.com/features/application-layer + - https://northflank.com/docs/v1/application/getting-started/build-and-deploy-your-code + - https://northflank.com/docs/v1/application/network/configure-ports + - https://northflank.com/docs/v1/application/observe/configure-health-checks + - https://northflank.com/docs/v1/application/scale/scale-instances + verified_facts: + - The current official FCE scaffold still composes extension-tee, tee-proxy, and Redis; Concord keeps that runtime separation. + - The Coston2 provider-facing extension-proxy external port is 6664 and requires a stable public HTTPS URL. + - SIMULATED_TEE=true remains a supported Coston2 hackathon development path and is not production hardware-backed execution. + - A simulated TEE restart creates a new identity; the new machine must be registered and promoted and the stale identity paused. + - Exactly one active machine per endpoint avoids random dispatch to stale machines. + - Railway supports Dockerfile configuration-as-code, private service DNS, and stable public domains. + - Railway ALWAYS restart is unavailable on free/trial plans; an always-on claim requires a paid plan and no scale-to-zero behavior. + - Northflank long-running services are a viable fallback when scale remains at one or greater and only proxy port 6664 is public. + current_recorded_machine: + extension_id_decimal: 66188 + tee_id: 0xeE39d5e7d1C5043232282e3CC884B41a9Db22c85 + tee_proxy_id: 0x801470C95f78D0cA444e589aF8Ea0858Ce6d613e + status: 2 + registered_url: https://concord-fcc-ingress.microcosm.workers.dev + availability_now: unavailable_because_disposable_codespace_upstream_is_offline + decision: + primary_host: Railway paid long-running services + fallback_host: Northflank long-running combined services + cutover_required: true + reason: A stable host removes the disposable Codespace ingress dependency, but the fresh simulated TEE identity must still be deliberately registered and the stale machine paused. + product_semantics: + contradiction_found: false + mvp_changed: false + fdc_added: false + privacy_claim_expanded: false diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..cce8707 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,22 @@ +FROM node:24.19.0-alpine AS build + +WORKDIR /repo/frontend + +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci --ignore-scripts --no-audit --no-fund + +COPY frontend/ ./ +COPY sdk/typescript/ /repo/sdk/typescript/ +COPY config/coston2/concord-deployment.json /repo/config/coston2/concord-deployment.json + +RUN npm run build + +FROM nginx:1.29.4-alpine + +COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /repo/frontend/dist /usr/share/nginx/html + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -q -O /dev/null http://127.0.0.1:8080/ || exit 1 diff --git a/frontend/README.md b/frontend/README.md index 95e5034..20f6b58 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,19 +1,45 @@ # Concord frontend -The frontend is currently in the design phase. The semantic route/component -map is in [`docs/frontend-map.md`](../docs/frontend-map.md); the visual, -interaction, icon, responsive, and accessibility specification is in -[`docs/frontend-design-system.md`](../docs/frontend-design-system.md). -The complete art direction, navigation, page architecture, typography, motion, -imagery, and usability gate is in -[`docs/frontend-art-direction.md`](../docs/frontend-art-direction.md). - -The implementation must use the shared product contract and the TypeScript -SDK; it must not create a parallel facility state model. Heroicons is the -single icon family for the first frontend. - -Planned stack from the build handoff: React, TypeScript, Vite, Wagmi, Viem, -and TanStack Query. The first implementation checkpoint remains the Root -Accord workspace, but implementation begins only after the design review gate -in the design-system document is satisfied and contract/FCC semantics are -demonstrated with authoritative Coston2 evidence. +The first institutional frontend checkpoint is implemented in React, +TypeScript, and Vite. It translates the supplied visual reference into +Concord's actual product semantics: a public landing page, facility register, +Root Accord workspace, funding formation, activity, FCC evidence, explicit +lineage, child dossiers, and draw-leg detail. + +The rendered data comes directly from +`config/coston2/concord-deployment.json` and uses the canonical TypeScript SDK +types. It does not invent a second facility model or fake institutional data. +The recorded proof facility contains 9 USDT0 of funded capacity, three active +children, a repaid two-leg draw, and restored capacity. + +## Run locally + +```bash +cd frontend +npm ci --ignore-scripts --no-audit --no-fund +npm run dev +``` + +Use `npm run build` for the TypeScript and production-bundle gate and +`npm test` for semantic/invariant tests. The root-context production image is: + +```bash +docker build -f frontend/Dockerfile -t concord-frontend . +docker run --rm -p 8080:8080 concord-frontend +``` + +The GitHub workflow in `.github/workflows/frontend.yml` reproduces dependency +installation, build, tests, and the container build. + +The semantic map remains in `docs/frontend-map.md`. Visual decisions are in +`docs/frontend-design-system.md` and `docs/frontend-art-direction.md`. +Heroicons is the single interface icon family. Inter Variable is self-hosted +through the npm package; no runtime font CDN is required. + +## Current interaction boundary + +Read views use recorded Coston2 evidence. The “Prepare draw” review explains +the unsigned intent flow but does not fabricate calldata or transaction +success: a live intent service and wallet approval path are still required. +The FCC label is intentionally “simulated development TEE”; token settlement +and public EVM state are not represented as private. diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..ca5fbf9 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,17 @@ + + + + + + + + Concord — Private capital, coordinated. + + +

+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..fafe611 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,17 @@ +server { + listen 8080; + server_name _; + root /usr/share/nginx/html; + index index.html; + + location /assets/ { + try_files $uri =404; + expires 1y; + add_header Cache-Control "public, immutable"; + } + + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache"; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..55dc27a --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2560 @@ +{ + "name": "@concord-protocol/frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@concord-protocol/frontend", + "version": "0.1.0", + "dependencies": { + "@fontsource-variable/inter": "5.3.0", + "@heroicons/react": "2.2.0", + "@tanstack/react-query": "5.101.4", + "react": "19.2.8", + "react-dom": "19.2.8", + "react-router-dom": "7.18.2" + }, + "devDependencies": { + "@testing-library/jest-dom": "7.0.1", + "@testing-library/react": "16.3.2", + "@types/react": "19.2.18", + "@types/react-dom": "19.2.4", + "@vitejs/plugin-react": "6.0.5", + "jsdom": "30.0.1", + "typescript": "7.0.2", + "vite": "8.2.1", + "vitest": "4.1.10" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", + "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.3.0", + "@csstools/css-color-parser": "^4.1.10", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", + "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", + "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@fontsource-variable/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", + "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", + "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", + "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", + "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", + "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", + "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", + "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", + "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", + "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", + "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", + "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", + "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", + "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", + "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", + "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz", + "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.1.tgz", + "integrity": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=22", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": ">=10 <11", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", + "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/jsdom": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", + "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@exodus/bytes": "^1.15.1", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.2", + "undici": "^8.9.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-router": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", + "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.144.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.4", + "@rolldown/binding-darwin-arm64": "1.2.4", + "@rolldown/binding-darwin-x64": "1.2.4", + "@rolldown/binding-freebsd-x64": "1.2.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", + "@rolldown/binding-linux-arm64-gnu": "1.2.4", + "@rolldown/binding-linux-arm64-musl": "1.2.4", + "@rolldown/binding-linux-ppc64-gnu": "1.2.4", + "@rolldown/binding-linux-s390x-gnu": "1.2.4", + "@rolldown/binding-linux-x64-gnu": "1.2.4", + "@rolldown/binding-linux-x64-musl": "1.2.4", + "@rolldown/binding-openharmony-arm64": "1.2.4", + "@rolldown/binding-win32-arm64-msvc": "1.2.4", + "@rolldown/binding-win32-x64-msvc": "1.2.4" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", + "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.10" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz", + "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", + "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", + "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..a18c1cb --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,32 @@ +{ + "name": "@concord-protocol/frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "tsc -b && vite build", + "preview": "vite preview --host 0.0.0.0", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@fontsource-variable/inter": "5.3.0", + "@heroicons/react": "2.2.0", + "@tanstack/react-query": "5.101.4", + "react": "19.2.8", + "react-dom": "19.2.8", + "react-router-dom": "7.18.2" + }, + "devDependencies": { + "@testing-library/jest-dom": "7.0.1", + "@testing-library/react": "16.3.2", + "@types/react": "19.2.18", + "@types/react-dom": "19.2.4", + "@vitejs/plugin-react": "6.0.5", + "jsdom": "30.0.1", + "typescript": "7.0.2", + "vite": "8.2.1", + "vitest": "4.1.10" + } +} diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx new file mode 100644 index 0000000..71bfaad --- /dev/null +++ b/frontend/src/App.test.tsx @@ -0,0 +1,61 @@ +import { render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { describe, expect, it } from "vitest"; +import { App } from "./App"; +import { children, draw, facility, formatToken } from "./data/concord"; + +function renderRoute(path: string) { + return render( + + + , + ); +} + +describe("Concord frontend product semantics", () => { + it("presents the relationship as the product on the landing page", () => { + renderRoute("/"); + expect(screen.getByRole("heading", { name: /private capital,\s*coordinated/i })).toBeInTheDocument(); + expect(screen.getByText(/the relationship is the product/i)).toBeInTheDocument(); + expect(screen.getByText(/simulated development tee/i)).toBeInTheDocument(); + }); + + it("renders the observed Root Accord and distinguishes current exposure from restored capacity", () => { + renderRoute(`/facilities/${facility.id}`); + expect(screen.getByRole("heading", { level: 1, name: "Coston2 syndicated facility" })).toBeInTheDocument(); + expect(screen.getByText(/funded, repaid, and ready for another draw/i)).toBeInTheDocument(); + expect(screen.getAllByText(/9 USDT0/i).length).toBeGreaterThan(0); + expect(screen.getAllByText(/0 USDT0/i).length).toBeGreaterThan(0); + }); + + it("keeps child commitments and draw legs explicit", () => { + renderRoute(`/draws/${draw.id}`); + expect(screen.getByRole("heading", { name: /supplied by two child relationships/i })).toBeInTheDocument(); + expect(screen.getByText("Provider 1")).toBeInTheDocument(); + expect(screen.getByText("Provider 2")).toBeInTheDocument(); + expect(draw.legs).toHaveLength(2); + expect(children).toHaveLength(3); + }); + + it("does not convert an unknown Root Accord into the observed facility", () => { + renderRoute("/facilities/0xnot-observed"); + expect(screen.getByRole("heading", { name: /not in the recorded evidence/i })).toBeInTheDocument(); + expect(screen.queryByText(/funded, repaid, and ready for another draw/i)).not.toBeInTheDocument(); + }); +}); + +describe("recorded Coston2 invariants", () => { + it("derives root commitment and exposure from funded children", () => { + const childCommitment = children.reduce((sum, child) => sum + BigInt(child.committedCapacity), 0n); + const childExposure = children.reduce((sum, child) => sum + BigInt(child.drawnPrincipal), 0n); + expect(childCommitment).toBe(BigInt(facility.committedCapacity)); + expect(childExposure).toBe(BigInt(facility.drawnPrincipal)); + expect(BigInt(facility.availableCapacity)).toBe( + BigInt(facility.committedCapacity) - BigInt(facility.drawnPrincipal), + ); + }); + + it("formats large base-unit amounts without converting through unsafe JavaScript numbers", () => { + expect(formatToken("9007199254740993000000").replace(/\D/g, "")).toBe("9007199254740993"); + }); +}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..96fcd17 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,731 @@ +import { + ArrowDownTrayIcon, + ArrowLeftIcon, + ArrowRightIcon, + ArrowTopRightOnSquareIcon, + ArrowUpTrayIcon, + BanknotesIcon, + Bars3Icon, + BuildingLibraryIcon, + CheckCircleIcon, + ChevronRightIcon, + ClockIcon, + DocumentDuplicateIcon, + DocumentTextIcon, + EyeSlashIcon, + FunnelIcon, + InformationCircleIcon, + LockClosedIcon, + MagnifyingGlassIcon, + RectangleGroupIcon, + ServerStackIcon, + ShareIcon, + ShieldCheckIcon, + Squares2X2Icon, + UserGroupIcon, + WalletIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; +import { CheckCircleIcon as CheckCircleSolid } from "@heroicons/react/20/solid"; +import { + type ComponentType, + type PropsWithChildren, + type SVGProps, + useEffect, + useRef, + useState, +} from "react"; +import { + Link, + NavLink, + Route, + Routes, + useLocation, + useParams, +} from "react-router-dom"; +import { + activity, + children, + draw, + evidence, + explorerAddress, + explorerTx, + facility, + formatDate, + formatToken, + providerLabels, + round, + shortId, + snapshot, +} from "./data/concord"; + +type IconType = ComponentType>; + +const rootHref = `/facilities/${facility.id}`; + +function Brand({ inverted = false }: { inverted?: boolean }) { + return ( + + + ); +} + +function FacilityArtifact() { + return ( +
+ + +
+
+
+ CONCORD +
+
+ ROOT ACCORD +

Coston2 syndicated facility

+ {formatToken(facility.targetCapacity)} USDT0 +
+
+
+
Collateral
{formatToken(facility.collateralLocked)} FXRP
+
Providers
{children.length} independent
+
State
Active · observed
+
+ +
+
+ + ); +} + +function LandingPage() { + const capabilities = [ + { + icon: RectangleGroupIcon, + title: "One persistent facility", + copy: "A Root Accord holds participants, assets, capacity, authority, validity, and every derived relationship.", + }, + { + icon: LockClosedIcon, + title: "Confidential coordination", + copy: "Provider offers enter a bounded Makkari session and are evaluated through Flare Confidential Compute.", + }, + { + icon: Squares2X2Icon, + title: "Composed capacity", + copy: "CoFill deterministically selects and composes multiple providers without turning them into one anonymous pool.", + }, + { + icon: ShareIcon, + title: "Explicit lineage", + copy: "Every draw leg, settlement, and repayment remains attributable to the relationship that authorized it.", + }, + ]; + + return ( +
+
+ +
+
+

PROGRAMMABLE CAPITAL RELATIONSHIPS

+

Private capital,
coordinated.

+

+ Concord coordinates one FXRP-backed facility across independent capital providers. Private offers are composed through Flare FCC; funding, draws, repayments, and lineage stay explicit. +

+
+ Open proof facility
+
+ Live Coston2 contracts + 3 funded providers + Multi-child draw repaid +
+
+ +
+
+ IMPLEMENTATION TRUTH +
+
+
+
+
+ +
+
+

THE RELATIONSHIP IS THE PRODUCT

+

Capital stays understandable from offer to restored capacity.

+

Transactions are evidence inside the relationship. They are not the organizing object.

+
+
+ {capabilities.map(({ icon: Icon, title, copy }, index) => ( +
+ 0{index + 1} +
+ ))} +
+
+ +
+
+

ONE CAUSAL PATH

+

Complex underneath.
Legible at every step.

+

Guided views use familiar facility language. Detailed views preserve Root Accord IDs, CoFill commitments, child exposure, draw legs, and transaction evidence.

+ Follow the recorded lineage
+
    + {[ + ["Root Accord", "One facility and authority boundary", "Active"], + ["Makkari", "Three provider offers coordinated privately", "Finalized"], + ["CoFill", "3 + 3 + 3 USDT0 allocated", "Verified"], + ["Child Accords", "Three independently funded relationships", "Active"], + ["Draw + repayment", "4 USDT0 across two legs, fully repaid", "Restored"], + ].map(([title, copy, state], index) => ( +
  1. + {index + 1} +
    {title}{copy}
    + +
  2. + ))} +
+
+ +
+
+ +
+

RECORDED COSTON2 EVIDENCE

+

A working relationship, not a mock dashboard.

+

The recorded vertical slice created one root, materialized three children, funded 9 USDT0, drew 4 USDT0 across two children, repaid it, and restored the full 9 USDT0 capacity.

+
+
9USDT0 funded
+
2draw legs
+
9USDT0 available
+
+

+
+
+ +
+ EXPLORE THE RELATIONSHIP +

See why every movement was allowed.

+ Open Concord
+ +
+ +

Persistent programmable capital relationships on Flare.

+ +
+
+ ); +} + +const globalRoutes: Array<{ label: string; to: string; icon: IconType }> = [ + { label: "Facilities", to: "/facilities", icon: DocumentTextIcon }, + { label: "Funding", to: `${rootHref}/funding`, icon: BanknotesIcon }, + { label: "Activity", to: `${rootHref}/activity`, icon: ClockIcon }, + { label: "Evidence", to: `${rootHref}/evidence`, icon: ShieldCheckIcon }, +]; + +function MobileDrawer({ open, onClose }: { open: boolean; onClose: () => void }) { + const closeRef = useRef(null); + useEffect(() => { + if (!open) return; + closeRef.current?.focus(); + const onKey = (event: KeyboardEvent) => event.key === "Escape" && onClose(); + document.addEventListener("keydown", onKey); + document.body.classList.add("drawer-open"); + return () => { + document.removeEventListener("keydown", onKey); + document.body.classList.remove("drawer-open"); + }; + }, [open, onClose]); + if (!open) return null; + return ( +
+
+

WORKSPACE

+ +

CONTEXT

+ +
+ +
+ ); +} + +function AppShell({ children }: PropsWithChildren) { + const [drawer, setDrawer] = useState(false); + const location = useLocation(); + useEffect(() => setDrawer(false), [location.pathname]); + return ( +
+
+ + + +
+ + +
+
+ setDrawer(false)} /> +
{children}
+
Coston2 · chain 114 · recorded {formatDate(snapshot.deployment.observedAt)}
SourceDisclosure
+ +
+ ); +} + +function PageHeading({ eyebrow, title, description, action }: { eyebrow: string; title: string; description: string; action?: React.ReactNode }) { + return ( +
+

{eyebrow}

{title}

{description}

+ {action} +
+ ); +} + +function FacilitiesPage() { + return ( + + Create Root Accord} + /> +
+
+ + +
+
+
+
FacilityCommittedDrawnAvailableStatus
+ + Coston2 syndicated facilityFXRP-backed · 3 providers + Committed{formatToken(facility.committedCapacity)} USDT0 + Drawn{formatToken(facility.drawnPrincipal)} USDT0 + Available{formatToken(facility.availableCapacity)} USDT0 + + +
1 observed relationshipRecorded from Coston2 at {formatDate(snapshot.deployment.observedAt)}
+
+ +
+
+ ); +} + +const localTabs = [ + ["Overview", rootHref], + ["Funding", `${rootHref}/funding`], + ["Activity", `${rootHref}/activity`], + ["Evidence", `${rootHref}/evidence`], + ["Lineage", `${rootHref}/lineage`], +]; + +function AccordHeader({ onPrepare }: { onPrepare?: () => void }) { + return ( + <> +
+
+
+
+ Next permitted action + + Requires treasury wallet review +
+
+ + + ); +} + +function MetricsBand() { + const metrics = [ + ["Target", facility.targetCapacity, "Facility requirement"], + ["Committed", facility.committedCapacity, "Funded by 3 providers"], + ["Drawn", facility.drawnPrincipal, "Current principal"], + ["Available", facility.availableCapacity, "Capacity restored"], + ]; + return ( +
+ {metrics.map(([label, raw, note], index) =>
{label}{formatToken(raw)} USDT0

{note}

)} +
+ ); +} + +const relationshipSteps: Array<{ label: string; detail: string; state: string; icon: IconType; href: string }> = [ + { label: "Root Accord", detail: "1 facility", state: "Active", icon: RectangleGroupIcon, href: rootHref }, + { label: "Makkari", detail: "3 offers", state: "Finalized", icon: LockClosedIcon, href: `/rounds/${round.id}` }, + { label: "CoFill", detail: "9 USDT0", state: "Verified", icon: Squares2X2Icon, href: `/evidence/${evidence.resultDigest}` }, + { label: "Child Accords", detail: "3 funded", state: "Active", icon: UserGroupIcon, href: `${rootHref}/funding` }, + { label: "Draw", detail: "2 legs", state: "Repaid", icon: ArrowDownTrayIcon, href: `/draws/${draw.id}` }, + { label: "Repayment", detail: "4 USDT0", state: "Restored", icon: ArrowUpTrayIcon, href: `${rootHref}/activity` }, +]; + +function RelationshipSpine() { + return ( +
+
CAUSAL RELATIONSHIP

Why the capital could move

Full lineage
+
    + {relationshipSteps.map(({ label, detail, state, icon: Icon, href }, index) => ( +
  1. + + {String(index + 1).padStart(2, "0")} +
  2. + ))} +
+
+ ); +} + +function ChildRegister({ compact = false }: { compact?: boolean }) { + return ( +
+
CHILD ACCORDS

Independent provider relationships

Selected 9 · funded 9 USDT0
+
+
ProviderStateCommittedDrawnAvailable
+ {children.map((child, index) => ( + + P{index + 1}{providerLabels.get(child.provider.toLowerCase())}{shortId(child.provider)} + + Committed{formatToken(child.committedCapacity)} USDT0 + Drawn{formatToken(child.drawnPrincipal)} USDT0 + Available{formatToken(child.availableCapacity)} USDT0 + + + ))} +
+
+ ); +} + +function StateExplanation() { + return ( +
+
+
CURRENT STATE

Funded, repaid, and ready for another draw.

Three child relationships funded 9 USDT0. The recorded 4 USDT0 draw was fully repaid, so root and child exposure are zero and the complete 9 USDT0 capacity is available again.

+
+
+ ); +} + +function ActivityList({ limit }: { limit?: number }) { + return ( +
    + {activity.slice(0, limit).map((item) => ( +
  1. +
  2. + ))} +
+ ); +} + +function EvidenceSummary() { + return ( +
+
+
Result digest
{shortId(evidence.resultDigest, 10, 8)}
Extension
66188
TEE mode
Simulated development
Disclosure
Metadata only
+ Inspect disclosure boundary +
+ ); +} + +function ActionReview({ open, onClose }: { open: boolean; onClose: () => void }) { + const dialogRef = useRef(null); + useEffect(() => { + if (!open) return; + dialogRef.current?.focus(); + const onKey = (event: KeyboardEvent) => event.key === "Escape" && onClose(); + document.addEventListener("keydown", onKey); + return () => document.removeEventListener("keydown", onKey); + }, [open, onClose]); + if (!open) return null; + return ( +
+
+ UNSIGNED INTENT REVIEW +

Prepare a facility draw

+

This interface is rendering recorded Coston2 evidence. A live intent service and treasury wallet must be connected before Concord can prepare calldata.

+
    +
  1. 1
    What would happen?

    A new root draw would allocate principal across eligible child capacity.

  2. +
  3. 2
    Why is it allowed?

    The root is active, exposure is zero, and 9 USDT0 is available.

  4. +
  5. 3
    What remains required?

    Amount entry, unsigned intent generation, explicit wallet approval, receipt confirmation, and an observed state update.

  6. +
+
+
+
+ + ); +} + +function OverviewPage() { + const [review, setReview] = useState(false); + return ( + + setReview(true)} /> + + + + +
+
ACTIVITY

Recorded lifecycle

All activity
+
EVIDENCE

Verification boundary

+
+ setReview(false)} /> +
+ ); +} + +function FundingPage() { + return ( + + + +
+
SYNDICATION ROUND

Allocation finalized

CoFill selected the lowest eligible fees and filled the 9 USDT0 target deterministically.

+
Target
9 USDT0
Eligible providers
3
Maximum fee
{round.maxFeeBps} bps
Round expiry
{formatDate(round.roundExpiry)}
+ Open round dossier +
+
+ +
+ ); +} + +function ActivityPage() { + return ( + + + +
+
+ ); +} + +function EvidencePage() { + return ( + + + +
+ +
+

Public and observed

+
  • Root and child Accord state
  • USDT0 commitments and current exposure
  • Draw, draw-leg, and repayment receipts
  • Result, round, root, and extension binding
+
+
+

Withheld or not claimed

+
  • Losing quotes and provider constraints
  • Private FXRP or USDT0 transfers
  • Private EVM settlement
  • Production hardware-backed TEE security
+
+
+
+ ); +} + +function LineagePage() { + return ( + + + +
+ {relationshipSteps.map(({ label, detail, state, icon: Icon, href }, index) =>
{String(index + 1).padStart(2, "0")}
)} +
+
DRAW LEGS

Explicit provider supply

+
+ ); +} + +function DrawLegs() { + return
{draw.legs.map((leg, index) => P{index + 1}
{providerLabels.get(leg.provider.toLowerCase())}Child Accord {shortId(leg.childAccordId)}
{formatToken(leg.principal)} USDT0fully repaid
; +} + +function DrawPage() { + const { drawId } = useParams(); + if (drawId !== draw.id) return ; + return ( + + } /> + +
Principal4 USDT0

Settled to treasury

Repaid4 USDT0

Observed on Coston2

Outstanding0 USDT0

Capacity restored

+
DRAW LEGS

Provider composition

2 explicit obligations
+
REPAYMENT

Principal returned to the same obligations.

Each draw-leg principal fell to zero, child exposure fell to zero, root exposure fell to zero, and available capacity returned to 9 USDT0.

+
+ ); +} + +function ChildPage() { + const { childId } = useParams(); + const child = children.find((candidate) => candidate.id === childId); + if (!child) return ; + const providerNumber = children.indexOf(child) + 1; + return ( + + } /> +
{child.id}Parent Root Accord
+
Selected3 USDT0

CoFill allocation

Committed3 USDT0

Funding observed

Drawn now0 USDT0

Exposure repaid

Available3 USDT0

Capacity restored

+

Provider

Participant
Provider {providerNumber}
Address
{shortId(child.provider, 12, 10)}
Accepted fee
{child.feeBps} bps
Valid until
{formatDate(child.validUntil)}

Terms and binding

Allocation
{shortId(child.allocationId, 12, 10)}
Terms commitment
{shortId(child.termsCommitment, 12, 10)}
Root
{shortId(child.rootAccordId, 12, 10)}
+
+ ); +} + +function RoundPage() { + const { roundId } = useParams(); + if (roundId !== round.id) return ; + return ( + + } /> +
{round.id}Funding formation
+

Session objective

Root Accord
{shortId(round.rootAccordId)}
Target
9 USDT0
Maximum fee
{round.maxFeeBps} bps
Eligible providers
{round.eligibleProviderCount}
Expiry
{formatDate(round.roundExpiry)}

Confidentiality boundary

Provider capacity, rates, constraints, expiry, and losing quotes may remain confidential inside the session. Accepted executable capacity, provider addresses, commitments, funding, and settlement are public where the chain requires them.

Private quote data: withheld

No zero values are inferred for unavailable quote fields.

+
+ ); +} + +function EvidenceDetailPage() { + const { digest } = useParams(); + if (digest !== evidence.resultDigest) return ; + return ( + + } /> +
{evidence.resultDigest}
+

Binding

Extension
66188
Root Accord
{shortId(evidence.rootAccordId!)}
Round
{shortId(evidence.roundId!)}
Selected
3 providers · 9 USDT0

Execution truth

Network
Coston2 · chain 114
TEE registry state
Status 2 · one active machine
TEE mode
Simulated development
Disclosure
Metadata only
+
This is real Coston2 FCC development-path evidence.

It is not a claim of private token transfers, private EVM state, production hardware-backed TEE execution, or production institutional readiness.

+
+ ); +} + +function SettingsPage() { + return ( + + +
+ {[ + [ServerStackIcon, "Network", "Coston2 · chain 114", snapshot.deployment.rpcUrl], + [RectangleGroupIcon, "Capital facility", shortId(snapshot.deployment.canonicalFacility.capitalFacility, 12, 10), explorerAddress(snapshot.deployment.canonicalFacility.capitalFacility)], + [BuildingLibraryIcon, "Accord registry", shortId(snapshot.deployment.canonicalFacility.accordRegistry, 12, 10), explorerAddress(snapshot.deployment.canonicalFacility.accordRegistry)], + [BanknotesIcon, "Liquidity asset", `USDT0 · ${snapshot.deployment.assets.usdt0Decimals} decimals`, explorerAddress(snapshot.deployment.assets.usdt0)], + [ShieldCheckIcon, "FCC extension", "66188 · simulated development TEE", snapshot.deployment.extension.activeTeeUrls[0]], + ].map(([Icon, label, value, href]) => )} +
+
+ ); +} + +function NotFound() { + return
NOT OBSERVED

This relationship is not in the recorded evidence.

Concord is not asserting an empty or zero state for this identifier.

Return to facilities
; +} + +function ObservedFacilityRoute({ children: route }: PropsWithChildren) { + const { rootId } = useParams(); + return rootId === facility.id ? route : ; +} + +export function App() { + return ( + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + ); +} diff --git a/frontend/src/data/concord.ts b/frontend/src/data/concord.ts new file mode 100644 index 0000000..8bd3870 --- /dev/null +++ b/frontend/src/data/concord.ts @@ -0,0 +1,278 @@ +import type { + ChildAccord, + Draw, + Evidence, + Facility, + Lineage, + Round, +} from "@concord-protocol/sdk"; +import deployment from "../../../config/coston2/concord-deployment.json"; + +const root = deployment.rootRound; +const lifecycle = root.facilityLifecycle; + +export const providerLabels = new Map( + root.children.map((child, index) => [child.provider.toLowerCase(), `Provider ${index + 1}`]), +); + +export function formatToken(raw: string, decimals = 6, maximumFractionDigits = 2) { + const divisor = 10n ** BigInt(decimals); + const value = BigInt(raw); + const whole = value / divisor; + const fraction = value % divisor; + const formatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }); + const wholeText = formatter.format(whole); + const fractionText = fraction + .toString() + .padStart(decimals, "0") + .slice(0, maximumFractionDigits) + .replace(/0+$/, ""); + if (!fractionText) return wholeText; + const decimal = new Intl.NumberFormat(undefined).formatToParts(1.1) + .find((part) => part.type === "decimal")?.value ?? "."; + return `${wholeText}${decimal}${fractionText}`; +} + +export function shortId(value: string, start = 7, end = 5) { + if (value.length <= start + end + 1) return value; + return `${value.slice(0, start)}…${value.slice(-end)}`; +} + +export function formatDate(value: string) { + return new Intl.DateTimeFormat(undefined, { + day: "numeric", + month: "short", + year: "numeric", + timeZone: "UTC", + }).format(new Date(value)); +} + +const childStateCopy = { + state: "ACTIVE", + label: "Active", + explanation: "The provider funded its selected allocation and participates in the active facility.", +}; + +export const children: ChildAccord[] = root.children.map((child) => ({ + id: child.id, + rootAccordId: root.rootAccordId, + allocationId: child.allocationId, + provider: child.provider, + selectedCapacity: child.selectedCapacity, + committedCapacity: child.committedCapacity, + drawnPrincipal: child.drawnPrincipal, + availableCapacity: (BigInt(child.committedCapacity) - BigInt(child.drawnPrincipal)).toString(), + feeBps: child.feeBps, + validUntil: new Date(Number(child.validUntilUnix) * 1000).toISOString(), + validUntilUnix: child.validUntilUnix, + termsCommitment: child.termsCommitment, + state: "ACTIVE", + stateCopy: childStateCopy, +})); + +export const round: Round = { + id: root.roundId, + rootAccordId: root.rootAccordId, + targetCapacity: root.targetCapacity, + maxFeeBps: root.maxFeeBps, + roundExpiry: root.roundExpiry, + roundExpiryUnix: root.roundExpiryUnix, + status: "FINALIZED", + stateCopy: { + state: "FINALIZED", + label: "Allocation verified", + explanation: "CoFill finalized a deterministic allocation from three confidential provider offers.", + }, + eligibleProviderCount: root.eligibleProviders.length, + privateQuoteData: "withheld", +}; + +export const facility: Facility = { + id: root.rootAccordId, + borrower: root.borrower, + collateralAsset: { + address: deployment.assets.fxrp, + symbol: "FXRP", + decimals: deployment.assets.fxrpDecimals, + }, + liquidityAsset: { + address: deployment.assets.usdt0, + symbol: "USDT0", + decimals: deployment.assets.usdt0Decimals, + }, + targetCapacity: root.targetCapacity, + committedCapacity: root.verifiedOnchain.committedCapacity, + drawnPrincipal: root.verifiedOnchain.drawnPrincipal, + collateralLocked: root.collateralLocked, + availableCapacity: lifecycle.finalAvailableCapacity, + validUntil: root.validUntil, + validUntilUnix: root.validUntilUnix, + policyHash: root.policyHash, + syndicationRoundId: root.roundId, + state: "ACTIVE", + stateCopy: { + state: "ACTIVE", + label: "Active", + explanation: "All three selected providers funded their allocations. The recorded draw was repaid and capacity returned.", + }, + round, + children, + invariants: { + rootDrawWithinCommitment: true, + childDrawWithinCommitment: true, + rootExposureMatchesChildren: true, + committedMatchesFundedChildren: true, + }, +}; + +export const draw: Draw = { + id: lifecycle.drawId, + rootAccordId: root.rootAccordId, + principal: lifecycle.drawAmount, + repaidPrincipal: lifecycle.drawAmount, + outstandingPrincipal: "0", + createdAt: lifecycle.drawCreatedAt, + legs: lifecycle.drawLegs.map((leg) => { + const child = children.find((candidate) => candidate.id === leg.childAccordId)!; + return { + id: leg.id, + drawId: lifecycle.drawId, + childAccordId: leg.childAccordId, + provider: child.provider, + principal: leg.principal, + repaidPrincipal: leg.repaidPrincipal, + outstandingPrincipal: (BigInt(leg.principal) - BigInt(leg.repaidPrincipal)).toString(), + }; + }), +}; + +export const evidence: Evidence = { + resultDigest: root.allocation.resultDigest, + status: "verified", + disclosure: "metadata_only", + extensionId: deployment.extension.id, + roundId: root.roundId, + rootAccordId: root.rootAccordId, + source: "coston2_fcc", + warning: "Coston2 execution used the supported simulated development TEE path; public token transfers remain public.", +}; + +export const lineage: Lineage = { + rootAccordId: root.rootAccordId, + nodes: [ + { + id: root.rootAccordId, + parentId: "", + kind: "ROOT_ACCORD", + createdAt: deployment.observedAt, + children: [root.roundId], + }, + { + id: root.roundId, + parentId: root.rootAccordId, + kind: "MAKKARI_SESSION", + createdAt: deployment.observedAt, + children: [root.allocation.resultDigest], + }, + { + id: root.allocation.resultDigest, + parentId: root.roundId, + kind: "COFILL_ALLOCATION", + createdAt: deployment.observedAt, + children: children.map((child) => child.id), + }, + ...children.map((child) => ({ + id: child.id, + parentId: root.allocation.resultDigest, + kind: "CHILD_ACCORD" as const, + createdAt: deployment.observedAt, + children: draw.legs.filter((leg) => leg.childAccordId === child.id).map((leg) => leg.id), + })), + { + id: draw.id, + parentId: root.rootAccordId, + kind: "DRAW" as const, + createdAt: draw.createdAt, + children: draw.legs.map((leg) => leg.id), + }, + ...draw.legs.map((leg) => ({ + id: leg.id, + parentId: draw.id, + kind: "DRAW_LEG" as const, + createdAt: draw.createdAt, + children: [], + })), + ], +}; + +export type ActivityItem = { + id: string; + title: string; + description: string; + tx?: string; + tone: "observed" | "private" | "restored"; +}; + +export const activity: ActivityItem[] = [ + { + id: "root", + title: "Root Accord created and collateralized", + description: "The treasury created one 9 USDT0 facility and locked 1 FXRP.", + tx: root.transactions.createRoot, + tone: "observed", + }, + { + id: "round", + title: "Makkari syndication opened", + description: "Three eligible providers entered the confidential coordination round.", + tx: root.transactions.openSyndication, + tone: "private", + }, + { + id: "allocation", + title: "CoFill allocation verified", + description: "Three 3 USDT0 child allocations were materialized at 610, 640, and 680 bps.", + tx: root.allocation.materializeTransaction, + tone: "observed", + }, + { + id: "funding", + title: "Selected providers funded", + description: "Three successful USDT0 transfers moved the child relationships from selected to active.", + tx: lifecycle.fundingTransactions[2], + tone: "observed", + }, + { + id: "draw", + title: "4 USDT0 drawn across two children", + description: "Provider 1 supplied 3 USDT0 and Provider 2 supplied 1 USDT0 through explicit draw legs.", + tx: lifecycle.drawTransaction, + tone: "observed", + }, + { + id: "repayment", + title: "Principal repaid; capacity restored", + description: "The 4 USDT0 principal was repaid, child exposure returned to zero, and 9 USDT0 became available again.", + tx: lifecycle.repaymentTransaction, + tone: "restored", + }, +]; + +export const snapshot = { + deployment, + facility, + round, + children, + draw, + evidence, + lineage, + activity, +}; + +export function explorerTx(hash: string) { + return `${deployment.explorerUrl}/tx/${hash}`; +} + +export function explorerAddress(address: string) { + return `${deployment.explorerUrl}/address/${address}`; +} diff --git a/frontend/src/global.d.ts b/frontend/src/global.d.ts new file mode 100644 index 0000000..cbe652d --- /dev/null +++ b/frontend/src/global.d.ts @@ -0,0 +1 @@ +declare module "*.css"; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..606e945 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,26 @@ +import "@fontsource-variable/inter"; +import React from "react"; +import ReactDOM from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter } from "react-router-dom"; +import { App } from "./App"; +import "./styles.css"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + staleTime: 30_000, + }, + }, +}); + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + + + , +); diff --git a/frontend/src/styles.css b/frontend/src/styles.css new file mode 100644 index 0000000..565cf7d --- /dev/null +++ b/frontend/src/styles.css @@ -0,0 +1,474 @@ +:root { + color: #171d25; + background: #f2f1ed; + font-family: "Inter Variable", Inter, ui-sans-serif, system-ui, sans-serif; + font-synthesis: none; + text-rendering: optimizeLegibility; + --ink: #171d25; + --muted: #69717d; + --faint: #9299a3; + --line: #dcdeda; + --line-strong: #cdd1d0; + --paper: #fbfbf8; + --paper-soft: #f7f7f4; + --blue: #205cf3; + --blue-dark: #154ad0; + --blue-pale: #edf3ff; + --green: #24714f; + --green-pale: #e7f4ec; + --amber: #7a5d18; + --amber-pale: #faf1d8; + --radius: 18px; + --shadow: 0 20px 60px rgba(35, 42, 49, 0.08), 0 2px 8px rgba(35, 42, 49, 0.04); +} + +* { box-sizing: border-box; } +html { scroll-behavior: smooth; } +body { margin: 0; min-width: 320px; min-height: 100vh; background: radial-gradient(circle at 50% -10%, #fff 0, #f5f4f0 34%, #efeee9 100%); } +body.drawer-open { overflow: hidden; } +button, input { font: inherit; } +button, a { -webkit-tap-highlight-color: transparent; } +a { color: inherit; text-decoration: none; } +button { color: inherit; } +svg { width: 1.25rem; height: 1.25rem; flex: 0 0 auto; } +code { font: 500 0.77rem/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; overflow-wrap: anywhere; } +.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } + +.button { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: .55rem; padding: .75rem 1.1rem; border: 1px solid transparent; border-radius: 8px; font-size: .82rem; font-weight: 600; cursor: pointer; transition: transform .18s ease, box-shadow .18s ease, background .18s ease, border-color .18s ease; } +.button svg { width: 1rem; height: 1rem; } +.button:hover:not(:disabled) { transform: translateY(-1px); } +.button:focus-visible, .icon-button:focus-visible, a:focus-visible, input:focus-visible { outline: 3px solid rgba(32, 92, 243, .28); outline-offset: 2px; } +.button--primary { color: white; background: var(--blue); box-shadow: 0 7px 18px rgba(32, 92, 243, .2); } +.button--primary:hover:not(:disabled) { background: var(--blue-dark); box-shadow: 0 9px 24px rgba(32, 92, 243, .28); } +.button--secondary { color: #303842; background: rgba(255,255,255,.65); border-color: var(--line); } +.button--secondary:hover:not(:disabled) { border-color: #adb4bd; background: white; } +.button--compact { min-height: 38px; padding: .55rem .9rem; font-size: .74rem; } +.button:disabled { cursor: not-allowed; opacity: .48; box-shadow: none; } +.icon-button { width: 40px; height: 40px; display: inline-grid; place-items: center; border: 1px solid var(--line); border-radius: 9px; background: rgba(255,255,255,.72); cursor: pointer; } +.text-link { display: inline-flex; align-items: center; gap: .4rem; color: var(--blue); font-size: .8rem; font-weight: 600; } +.text-link svg { width: .95rem; } +.eyebrow { display: block; margin: 0 0 .75rem; color: #6d7580; font-size: .66rem; font-weight: 700; letter-spacing: .13em; text-transform: uppercase; } + +.brand { display: inline-flex; align-items: center; gap: .55rem; font-size: .83rem; font-weight: 650; letter-spacing: .04em; } +.brand__mark { width: 19px; height: 19px; display: inline-grid; place-items: center; border: 1.5px solid currentColor; border-radius: 50%; } +.brand__mark > span { width: 7px; height: 7px; border: 1.5px solid currentColor; border-right-color: transparent; border-radius: 50%; } +.brand--inverted { color: white; } +.shell-frame { width: min(1440px, calc(100% - 40px)); margin-inline: auto; border: 1px solid rgba(199, 201, 198, .82); border-radius: var(--radius); background: rgba(251,251,248,.9); box-shadow: var(--shadow); } + +/* Landing */ +.landing { padding: 20px 0 0; overflow: hidden; } +.landing-hero { min-height: min(760px, calc(100vh - 40px)); display: flex; flex-direction: column; overflow: hidden; } +.public-header { height: 82px; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; padding: 0 4.4%; } +.public-header__nav { display: flex; align-items: center; gap: 3rem; } +.public-header__nav a { color: #39414a; font-size: .72rem; font-weight: 560; } +.public-header > .button { justify-self: end; } +.landing-hero__grid { flex: 1; display: grid; grid-template-columns: minmax(360px, .86fr) minmax(520px, 1.14fr); align-items: center; gap: 2rem; padding: 1rem 4.4% 2rem; } +.hero-copy { position: relative; z-index: 2; padding: 2rem 0; } +.hero-copy h1 { max-width: 620px; margin: 0; font-size: clamp(3.6rem, 5.9vw, 6.6rem); line-height: .98; letter-spacing: -.065em; font-weight: 590; } +.accent-dot { color: var(--blue); } +.hero-copy__body { max-width: 565px; margin: 1.8rem 0 0; color: var(--muted); font-size: clamp(.95rem, 1.15vw, 1.08rem); line-height: 1.7; } +.hero-actions { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 2rem; } +.proof-line { display: flex; flex-wrap: wrap; gap: .7rem 1.6rem; margin-top: 2.8rem; color: #7b828b; font-size: .7rem; } +.proof-line span { display: inline-flex; align-items: center; gap: .45rem; } +.proof-line span::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: #41a074; box-shadow: 0 0 0 4px #e6f3eb; } +.hero-evidence { min-height: 62px; display: flex; align-items: center; gap: 2rem; padding: 0 4.4%; border-top: 1px solid var(--line); color: #626a75; font-size: .7rem; } +.hero-evidence > span { color: #949aa2; font-weight: 680; letter-spacing: .1em; } +.hero-evidence div { display: flex; align-items: center; gap: .45rem; } +.hero-evidence svg { width: 1rem; height: 1rem; color: var(--blue); } + +.artifact-stage { position: relative; min-height: 570px; display: grid; place-items: center; perspective: 1300px; } +.orbit { position: absolute; inset: 50% auto auto 50%; border: 1px solid rgba(139, 149, 160, .25); border-radius: 50%; transform: translate(-50%, -50%) rotate(-12deg); } +.orbit--one { width: 95%; aspect-ratio: 1.7; } +.orbit--two { width: 75%; aspect-ratio: 1.45; transform: translate(-50%, -50%) rotate(-28deg); } +.orbit i { position: absolute; width: 8px; height: 8px; border: 1px solid white; border-radius: 50%; background: #d6dbe0; box-shadow: 0 3px 8px rgba(30,40,50,.14); } +.orbit i:nth-child(1) { left: 8%; top: 44%; } +.orbit i:nth-child(2) { right: 16%; top: 14%; } +.orbit i:nth-child(3) { left: 52%; bottom: -4px; } +.facility-artifact { position: relative; z-index: 2; width: min(390px, 58vw); aspect-ratio: .84; padding: 3px; border-radius: 28px; background: conic-gradient(from 145deg, #fafafa, #c7effe, #fff3b7, #e9cfff, #f9ffff, #fafafa); transform: rotateX(55deg) rotateZ(12deg) translateY(-5%); box-shadow: 12px 25px 45px rgba(46,52,60,.16), -8px 10px 35px rgba(154,215,247,.2), 8px 3px 25px rgba(245,182,231,.14); animation: artifact-float 7s ease-in-out infinite; } +.facility-artifact::before { content: ""; position: absolute; inset: 12px -13px -17px 13px; z-index: -1; border-radius: 27px; background: linear-gradient(135deg,#e8faff,#f6e6ff 40%,#fff8ce 66%,#ddf7fc); filter: blur(1px); } +.facility-artifact__face { height: 100%; display: flex; flex-direction: column; justify-content: space-between; padding: 8% 9%; border: 1px solid rgba(255,255,255,.9); border-radius: 25px; background: linear-gradient(145deg, rgba(255,255,255,.96), rgba(241,242,240,.9)); box-shadow: inset 0 1px 0 white; } +.facility-artifact__topline { display: flex; justify-content: space-between; color: #69707a; font-size: .68rem; } +.facility-artifact__topline svg { width: 1.4rem; } +.facility-artifact h2 { margin: .3rem 0 .4rem; font-size: 1.55rem; line-height: 1.15; letter-spacing: -.035em; font-weight: 530; } +.facility-artifact strong { font-size: .86rem; font-weight: 490; color: #6f7680; } +.facility-artifact__rule { height: 1px; margin: 0 0 -1.4rem; background: #bdc1c2; } +.facility-artifact dl { display: grid; gap: .45rem; margin: 0; color: #555e68; font-size: .66rem; } +.facility-artifact dl div { display: flex; justify-content: space-between; gap: 1rem; } +.facility-artifact dt { color: #90969d; } +.facility-artifact dd { margin: 0; font-weight: 580; } +.facility-artifact__signature { width: 38px; height: 38px; display: grid; place-items: center; align-self: end; border: 1px solid #7f8790; border-radius: 13px; color: #666e77; font: italic 500 1.15rem Georgia, serif; } +.artifact-shadow { position: absolute; left: 22%; right: 14%; bottom: 15%; height: 18%; border-radius: 50%; background: radial-gradient(ellipse, rgba(47,53,60,.26), rgba(71,192,238,.09) 37%, rgba(255,255,255,0) 70%); filter: blur(15px); transform: rotate(8deg); } +@keyframes artifact-float { 0%,100% { transform: rotateX(55deg) rotateZ(12deg) translateY(-5%); } 50% { transform: rotateX(55deg) rotateZ(10deg) translateY(-8%); } } + +.landing-section { width: min(1260px, calc(100% - 48px)); margin: 9rem auto; } +.section-intro { max-width: 770px; } +.section-intro h2, .process-section__copy h2, .proof-section__copy h2, .landing-cta h2 { margin: 0; font-size: clamp(2.25rem, 4vw, 4rem); line-height: 1.08; letter-spacing: -.05em; font-weight: 560; } +.section-intro > p:last-child, .process-section__copy > p, .proof-section__copy > p { max-width: 640px; color: var(--muted); line-height: 1.7; } +.capability-grid { display: grid; grid-template-columns: repeat(4, 1fr); margin-top: 4rem; border-top: 1px solid var(--line-strong); border-bottom: 1px solid var(--line-strong); } +.capability { min-height: 285px; padding: 1.8rem 1.6rem 2rem; border-right: 1px solid var(--line); } +.capability:last-child { border-right: 0; } +.capability__index { display: block; margin-bottom: 3.8rem; color: #a0a6ad; font: 500 .68rem ui-monospace, monospace; } +.capability > svg { width: 1.4rem; height: 1.4rem; color: var(--blue); } +.capability h3 { margin: 1rem 0 .65rem; font-size: 1.06rem; font-weight: 610; letter-spacing: -.02em; } +.capability p { margin: 0; color: var(--muted); font-size: .78rem; line-height: 1.65; } +.process-section { display: grid; grid-template-columns: .82fr 1.18fr; gap: 8%; align-items: start; } +.process-section__copy { position: sticky; top: 2rem; } +.process-section__copy .text-link { margin-top: 1rem; } +.process-ledger { margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--line-strong); } +.process-ledger li { min-height: 95px; display: grid; grid-template-columns: 36px 1fr auto; align-items: center; gap: 1rem; border-bottom: 1px solid var(--line); } +.process-ledger > li > span { color: #a0a6ad; font: .7rem ui-monospace, monospace; } +.process-ledger strong, .process-ledger small { display: block; } +.process-ledger strong { font-size: .9rem; } +.process-ledger small { margin-top: .32rem; color: var(--muted); } +.proof-section { min-height: 560px; display: grid; grid-template-columns: 1fr 1fr; gap: 8%; align-items: center; padding: 6%; border: 1px solid var(--line); border-radius: var(--radius); background: var(--paper); box-shadow: var(--shadow); } +.proof-object { position: relative; min-height: 350px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .9rem; overflow: hidden; border: 1px solid var(--line); border-radius: 14px; background: linear-gradient(145deg,#fff,#f3f4f3); } +.proof-object > svg { z-index: 1; width: 5rem; height: 5rem; padding: 1.15rem; color: var(--blue); border: 1px solid #d5def8; border-radius: 28px; background: rgba(255,255,255,.78); box-shadow: 0 15px 35px rgba(32,92,243,.13); } +.proof-object > span, .proof-object > code { z-index: 1; } +.proof-object > span { color: #747c86; font-size: .64rem; font-weight: 700; letter-spacing: .12em; } +.proof-object__halo { position: absolute; width: 350px; height: 350px; border-radius: 50%; background: conic-gradient(from 90deg,rgba(32,92,243,.09),rgba(164,226,244,.22),rgba(242,209,255,.24),rgba(255,241,180,.18),rgba(32,92,243,.09)); filter: blur(25px); } +.proof-facts { display: grid; grid-template-columns: repeat(3,1fr); margin: 2rem 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } +.proof-facts div { padding: 1.2rem 1rem; border-right: 1px solid var(--line); } +.proof-facts div:last-child { border-right: 0; } +.proof-facts strong, .proof-facts span { display: block; } +.proof-facts strong { font-size: 1.6rem; font-weight: 570; } +.proof-facts span { margin-top: .25rem; color: var(--muted); font-size: .68rem; } +.disclosure { display: flex; align-items: flex-start; gap: .6rem; font-size: .74rem; } +.disclosure svg { width: 1rem; margin-top: .25rem; color: var(--blue); } +.landing-cta { min-height: 320px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 3rem; } +.landing-cta .button { margin-top: 2rem; } +.public-footer { width: min(1260px, calc(100% - 48px)); min-height: 150px; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 2rem; margin: 4rem auto 0; border-top: 1px solid var(--line-strong); color: var(--muted); font-size: .72rem; } +.public-footer > div { justify-self: end; display: flex; gap: 1.6rem; } + +/* Shared application shell */ +.app-shell { min-height: 100vh; background: #f5f5f2; } +.app-header { position: sticky; top: 0; z-index: 30; height: 68px; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; padding: 0 max(24px, calc((100vw - 1320px)/2)); border-bottom: 1px solid var(--line); background: rgba(251,251,248,.88); backdrop-filter: blur(18px); } +.app-header__nav { display: flex; align-self: stretch; gap: 2rem; } +.app-header__nav a { position: relative; display: flex; align-items: center; color: #626a74; font-size: .76rem; font-weight: 560; } +.app-header__nav a::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; transform: scaleX(0); background: var(--blue); transition: transform .18s ease; } +.app-header__nav a.active { color: var(--ink); } +.app-header__nav a.active::after { transform: scaleX(1); } +.app-header__context { justify-self: end; display: flex; align-items: center; gap: .8rem; } +.app-header__menu { display: none; } +.network-mark { height: 32px; display: inline-flex; align-items: center; gap: .45rem; padding: 0 .7rem; border: 1px solid var(--line); border-radius: 99px; background: white; color: #626a74; font-size: .68rem; font-weight: 560; } +.network-mark__dot { width: 6px; height: 6px; border-radius: 50%; background: #39a16b; box-shadow: 0 0 0 3px #e4f1e9; } +.account-control { height: 34px; display: flex; align-items: center; gap: .45rem; padding: 0 .75rem; border: 0; border-radius: 8px; background: #eceeea; font-size: .68rem; cursor: pointer; } +.account-control svg { width: .95rem; } +.app-content { width: min(1320px, calc(100% - 48px)); min-height: calc(100vh - 240px); margin: 0 auto; padding: 3.2rem 0 6rem; } +.app-footer { min-height: 110px; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 1rem; padding: 0 max(24px, calc((100vw - 1320px)/2)); border-top: 1px solid var(--line); color: #7a828b; font-size: .66rem; background: var(--paper); } +.app-footer > div { justify-self: end; display: flex; gap: 1.4rem; } +.mobile-bottom-nav { display: none; } + +.status { width: fit-content; min-height: 23px; display: inline-flex; align-items: center; gap: .35rem; padding: .18rem .48rem; border-radius: 99px; font-size: .61rem; font-weight: 650; white-space: nowrap; } +.status svg { width: .78rem; height: .78rem; } +.status--success { color: var(--green); background: var(--green-pale); } +.status--warning { color: var(--amber); background: var(--amber-pale); } +.status--neutral { color: #5d6570; background: #eceeec; } +.status__dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; } +.page-heading { display: flex; justify-content: space-between; align-items: flex-end; gap: 2rem; margin-bottom: 2.4rem; } +.page-heading h1 { margin: 0; font-size: clamp(2.1rem,4vw,3.8rem); line-height: 1.05; letter-spacing: -.05em; font-weight: 570; } +.page-heading > div > p:last-child { max-width: 680px; margin: .9rem 0 0; color: var(--muted); line-height: 1.65; } +.view-tabs, .facility-tabs, .local-tabs { display: flex; gap: 1.6rem; border-bottom: 1px solid var(--line); } +.view-tabs button, .facility-tabs a, .local-tabs span { position: relative; padding: 0 0 .85rem; border: 0; background: none; color: #747c85; font-size: .72rem; font-weight: 560; } +.view-tabs button.active, .facility-tabs a.active, .local-tabs span.active { color: var(--blue); } +.view-tabs button.active::after, .facility-tabs a.active::after, .local-tabs span.active::after { content:""; position:absolute; left:0; right:0; bottom:-1px; height:2px; background:var(--blue); } +.register-toolbar { display: flex; justify-content: space-between; align-items: center; margin: 1rem 0; } +.search-control { width: min(380px, 60%); height: 40px; display: flex; align-items: center; gap: .6rem; padding: 0 .8rem; border: 1px solid var(--line); border-radius: 8px; background: white; } +.search-control svg { width: 1rem; color: #8a9199; } +.search-control input { width: 100%; border: 0; outline: 0; background: transparent; font-size: .75rem; } +.master-detail { display: grid; grid-template-columns: minmax(0, 1.65fr) minmax(320px, .75fr); min-height: 560px; overflow: hidden; border: 1px solid var(--line); border-radius: 13px; background: var(--paper); box-shadow: 0 8px 28px rgba(30,38,45,.05); } +.facility-register { display: flex; flex-direction: column; min-width: 0; } +.register-head, .facility-row { display: grid; grid-template-columns: minmax(210px,1.5fr) repeat(3,minmax(95px,.7fr)) 80px; align-items: center; gap: 1rem; } +.register-head { height: 48px; padding: 0 1.2rem; border-bottom: 1px solid var(--line); color: #78808a; font-size: .62rem; font-weight: 600; } +.facility-row { min-height: 82px; margin: .55rem; padding: .9rem .75rem; border: 1px solid transparent; border-radius: 8px; font-size: .72rem; } +.facility-row.selected { border-color: #6f98ff; background: #f5f8ff; box-shadow: 0 0 0 1px rgba(32,92,243,.05); } +.facility-row__title strong, .facility-row__title small, .facility-row > span > small { display: block; } +.facility-row__title strong { color: var(--blue); } +.facility-row small { color: #808792; font-size: .6rem; margin-bottom: .25rem; } +.register-empty-line { display: flex; justify-content: space-between; margin-top: auto; padding: 1rem 1.2rem; border-top: 1px solid var(--line); color: #92989f; font-size: .62rem; } +.object-inspector { padding: 1.4rem; border-left: 1px solid var(--line); background: #fafaf7; } +.object-inspector__header { display: flex; justify-content: space-between; gap: 1rem; } +.object-inspector h2 { margin: 0; font-size: 1rem; } +.local-tabs { gap: 1.2rem; margin: 1.5rem 0; } +.local-tabs span { font-size: .62rem; } +.inspector-metrics { margin: 0; padding: .85rem; border: 1px solid var(--line); border-radius: 8px; background: white; } +.inspector-metrics div, .dossier-section dl div { display: flex; justify-content: space-between; gap: 1rem; padding: .42rem 0; font-size: .68rem; } +.inspector-metrics dt, .dossier-section dt { color: #777f88; } +.inspector-metrics dd, .dossier-section dd { margin: 0; text-align: right; font-weight: 560; } +.inspector-section { display: grid; gap: .35rem; padding: 1rem 0; border-bottom: 1px solid var(--line); font-size: .68rem; } +.inspector-section span { color: #858c94; } +.inspector-actions { display: grid; grid-template-columns: 1fr 1fr; gap: .65rem; margin-top: 1.2rem; } + +.accord-header { display: flex; justify-content: space-between; align-items: flex-end; gap: 2rem; } +.back-link { display: inline-flex; align-items: center; gap: .4rem; margin-bottom: 1.8rem; color: #6e7680; font-size: .72rem; } +.back-link svg { width: .95rem; } +.accord-header__eyebrow { display: flex; align-items: center; gap: .8rem; margin-bottom: .75rem; } +.accord-header__eyebrow > span { color: #747c85; font-size: .66rem; font-weight: 700; letter-spacing: .13em; } +.accord-header h1 { margin: 0; font-size: clamp(2.2rem,4vw,4rem); line-height: 1.04; letter-spacing: -.055em; font-weight: 570; } +.accord-header > div:first-child > p { max-width: 720px; color: var(--muted); line-height: 1.55; } +.id-line { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; color: #7c848e; font-size: .66rem; } +.id-line code { color: #4d5660; } +.copy-button { width: 27px; height: 27px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 6px; background: white; cursor: pointer; } +.copy-button svg { width: .8rem; } +.accord-header__action { min-width: 210px; display: grid; gap: .45rem; } +.accord-header__action > span, .accord-header__action > small { color: #7b838c; font-size: .62rem; } +.facility-tabs { margin: 2.5rem 0 1.5rem; } +.metrics-band { display: grid; grid-template-columns: repeat(4, 1fr); margin-bottom: 1.4rem; overflow: hidden; border: 1px solid var(--line); border-radius: 12px; background: var(--paper); } +.metrics-band--three { grid-template-columns: repeat(3,1fr); } +.metric { min-height: 140px; padding: 1.3rem 1.45rem; border-right: 1px solid var(--line); } +.metric:last-child { border-right: 0; } +.metric > span { color: #767e87; font-size: .67rem; } +.metric > strong { display: block; margin-top: 1rem; font-size: clamp(1.55rem,2.5vw,2.25rem); letter-spacing: -.04em; font-weight: 570; } +.metric > strong small { font-size: .64rem; letter-spacing: 0; color: #777f87; } +.metric p { margin: .55rem 0 0; color: #8b9299; font-size: .62rem; } +.metric--accent { background: var(--blue-pale); } +.metric--accent > strong { color: var(--blue); } +.state-explanation { display: grid; grid-template-columns: auto minmax(0,1fr) auto; align-items: center; gap: 1.4rem; margin: 1.4rem 0; padding: 1.6rem; border: 1px solid #cedaf6; border-radius: 12px; background: linear-gradient(110deg,#f6f9ff,#fff); } +.state-explanation__icon { width: 46px; height: 46px; display: grid; place-items: center; border-radius: 12px; color: var(--blue); background: #e5edff; } +.state-explanation h2 { margin: 0; font-size: 1.05rem; } +.state-explanation p { max-width: 750px; margin: .45rem 0 0; color: var(--muted); font-size: .74rem; line-height: 1.55; } +.invariant-list { display: grid; gap: .45rem; padding-left: 1.5rem; border-left: 1px solid #d7dfee; color: #56616b; font-size: .63rem; } +.invariant-list span { display: flex; align-items: center; gap: .4rem; } +.invariant-list svg { width: .8rem; color: var(--green); } +.section-block { margin-top: 1.4rem; padding: 1.45rem; border: 1px solid var(--line); border-radius: 12px; background: var(--paper); } +.section-block--compact { margin-top: 0; } +.section-header { display: flex; justify-content: space-between; align-items: flex-end; gap: 1rem; margin-bottom: 1.35rem; } +.section-header h2 { margin: 0; font-size: 1.05rem; font-weight: 600; } +.section-header .eyebrow { margin-bottom: .45rem; } +.section-meta { color: #858c95; font-size: .65rem; } +.relationship-spine { display: grid; grid-template-columns: repeat(6,1fr); margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } +.relationship-spine li { position: relative; min-width: 0; border-right: 1px solid var(--line); } +.relationship-spine li:last-child { border-right: 0; } +.relationship-spine li:not(:last-child)::after { content: ""; position: absolute; z-index: 2; top: 50%; right: -5px; width: 8px; height: 8px; border-top: 1px solid #9ba2aa; border-right: 1px solid #9ba2aa; background: var(--paper); transform: rotate(45deg); } +.relationship-spine a { min-height: 172px; display: flex; flex-direction: column; align-items: flex-start; padding: 1rem; } +.relationship-spine a > svg { margin: 1.1rem 0 .75rem; color: var(--blue); } +.relationship-spine__number { color: #a0a6ad; font: .62rem ui-monospace, monospace; } +.relationship-spine strong { font-size: .75rem; } +.relationship-spine small { margin: .25rem 0 .7rem; color: #858c95; font-size: .6rem; } +.relationship-spine .status { margin-top: auto; } +.child-table { border-top: 1px solid var(--line); } +.child-table__head, .child-row { display: grid; grid-template-columns: minmax(180px,1.5fr) minmax(110px,.8fr) repeat(3,minmax(105px,.75fr)) 26px; align-items: center; gap: 1rem; } +.child-table__head { min-height: 42px; padding: 0 .7rem; color: #858c95; font-size: .62rem; } +.child-row { min-height: 75px; padding: .7rem; border-top: 1px solid var(--line); font-size: .7rem; transition: background .16s ease; } +.child-row:hover { background: #f5f8ff; } +.provider-cell { display: flex; align-items: center; gap: .7rem; } +.provider-avatar { width: 34px; height: 34px; display: grid; place-items: center; flex: 0 0 auto; border: 1px solid #cfd7ec; border-radius: 9px; color: var(--blue); background: #f1f5ff; font-size: .65rem; font-weight: 700; } +.provider-cell strong, .provider-cell small, .child-row > span > small { display: block; } +.provider-cell small, .child-row > span > small { margin-top: .2rem; color: #8b929a; font-size: .58rem; } +.child-row > span:last-child svg { width: .9rem; color: #9299a1; } +.split-section { display: grid; grid-template-columns: 1fr 1fr; gap: 1.4rem; } +.activity-list { margin: 0; padding: 0; list-style: none; } +.activity-list li { position: relative; display: grid; grid-template-columns: 14px 1fr; gap: 1rem; padding: 0 0 1.4rem; } +.activity-list li:not(:last-child)::before { content: ""; position: absolute; left: 4px; top: 14px; bottom: 0; width: 1px; background: var(--line); } +.activity-marker { position: relative; z-index: 1; width: 9px; height: 9px; margin-top: 4px; border: 2px solid white; border-radius: 50%; box-shadow: 0 0 0 1px #aeb5bc; background: #85909a; } +.activity-marker--private { background: #7968cb; } +.activity-marker--restored { background: #39a16b; } +.activity-list strong { font-size: .76rem; } +.activity-list p { margin: .32rem 0; color: var(--muted); font-size: .68rem; line-height: 1.5; } +.activity-list a { display: inline-flex; align-items: center; gap: .35rem; color: var(--blue); font-size: .63rem; } +.activity-list a svg { width: .8rem; } +.evidence-summary { padding: 1rem; border: 1px solid var(--line); border-radius: 9px; background: white; } +.evidence-summary__status { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: .8rem; padding-bottom: 1rem; border-bottom: 1px solid var(--line); } +.evidence-summary__status > svg { width: 1.5rem; color: var(--blue); } +.evidence-summary__status strong { font-size: .78rem; } +.evidence-summary__status .eyebrow { margin-bottom: .25rem; } +.evidence-summary dl { margin: .8rem 0; } +.evidence-summary dl div { display: flex; justify-content: space-between; gap: 1rem; padding: .36rem 0; font-size: .65rem; } +.evidence-summary dt { color: #808790; } +.evidence-summary dd { margin: 0; text-align: right; } +.evidence-summary .button { width: 100%; } + +.round-summary { display: grid; grid-template-columns: 1.25fr 1fr auto; align-items: end; gap: 2rem; padding: 1.6rem; border: 1px solid var(--line); border-radius: 12px; background: var(--paper); } +.round-summary h2 { margin: 0; font-size: 1.2rem; } +.round-summary p { color: var(--muted); font-size: .72rem; line-height: 1.5; } +.round-summary dl { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem 1.3rem; margin: 0; } +.round-summary dl div { display: grid; gap: .25rem; } +.round-summary dt { color: #878e97; font-size: .6rem; } +.round-summary dd { margin: 0; font-size: .72rem; font-weight: 590; } +.privacy-boundary { display: flex; align-items: flex-start; gap: .8rem; margin: 1.4rem 0; padding: 1rem 1.2rem; border: 1px solid #ded8ee; border-radius: 10px; background: #f9f7fc; } +.privacy-boundary > svg { width: 1.15rem; color: #7259a8; } +.privacy-boundary strong { font-size: .72rem; } +.privacy-boundary p { margin: .3rem 0 0; color: #6f6879; font-size: .67rem; line-height: 1.5; } +.privacy-boundary--inline { margin: 1.2rem 0 0; } +.activity-page { max-width: 850px; } +.evidence-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.3rem; } +.evidence-grid .evidence-summary { grid-column: span 2; } +.evidence-dossier, .dossier-section { padding: 1.5rem; border: 1px solid var(--line); border-radius: 12px; background: var(--paper); } +.evidence-dossier h2, .dossier-section h2 { margin: 0 0 1.2rem; font-size: 1rem; } +.evidence-dossier ul { display: grid; gap: .8rem; margin: 0; padding: 0; list-style: none; color: #59626c; font-size: .72rem; } +.evidence-dossier li { display: flex; align-items: center; gap: .6rem; } +.evidence-dossier svg { width: 1rem; color: var(--blue); } +.lineage-map { position: relative; display: grid; gap: .8rem; max-width: 840px; margin: 0 auto; } +.lineage-map::before { content: ""; position: absolute; left: 35px; top: 50px; bottom: 50px; width: 1px; background: #bbc4d7; } +.lineage-node { position: relative; display: grid; grid-template-columns: 35px 38px 1fr auto 30px; align-items: center; gap: .8rem; min-height: 82px; padding: .8rem 1rem; border: 1px solid var(--line); border-radius: 11px; background: var(--paper); } +.lineage-node > span { z-index: 1; width: 35px; height: 35px; display: grid; place-items: center; border: 1px solid #cbd3e5; border-radius: 50%; background: white; color: #77808a; font: .61rem ui-monospace,monospace; } +.lineage-node > svg { color: var(--blue); } +.lineage-node strong, .lineage-node small { display: block; } +.lineage-node strong { font-size: .77rem; } +.lineage-node small { margin-top: .25rem; color: #888f97; font-size: .62rem; } +.lineage-node > a svg { width: .9rem; } +.draw-legs { border-top: 1px solid var(--line); } +.draw-legs > a { display: grid; grid-template-columns: auto minmax(0,1fr) auto auto; align-items: center; gap: .8rem; padding: .9rem .4rem; border-bottom: 1px solid var(--line); font-size: .72rem; } +.draw-legs strong, .draw-legs small { display: block; } +.draw-legs small { margin-top: .25rem; color: #868e97; font-size: .6rem; } +.draw-legs > a > span:nth-last-child(2) { text-align: right; } +.draw-legs > a > svg { color: var(--green); } +.dossier-id { display: flex; justify-content: space-between; gap: 1rem; margin: -1rem 0 1.5rem; padding: .75rem 1rem; border: 1px solid var(--line); border-radius: 8px; background: #f0f1ee; color: #626a74; font-size: .68rem; } +.dossier-id a { display: inline-flex; align-items: center; gap: .4rem; color: var(--blue); font-weight: 600; } +.dossier-id svg { width: .85rem; } +.state-explanation--compact { grid-template-columns: auto 1fr; } +.dossier-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.4rem; } +.dossier-section dl { margin: 0; } +.dossier-section p { color: var(--muted); font-size: .74rem; line-height: 1.65; } +.proof-detail { min-height: 180px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1rem; margin-bottom: 1.4rem; padding: 1.5rem; border: 1px solid #d5def7; border-radius: 13px; background: radial-gradient(circle at 50% 30%,#fff,#eef3ff); } +.proof-detail svg { width: 2.2rem; height: 2.2rem; color: var(--blue); } +.proof-detail code { text-align: center; } +.settings-list { overflow: hidden; border: 1px solid var(--line); border-radius: 12px; background: var(--paper); } +.settings-row { min-height: 82px; display: grid; grid-template-columns: 38px 1fr auto; align-items: center; gap: 1rem; padding: .8rem 1.2rem; border-bottom: 1px solid var(--line); } +.settings-row:last-child { border-bottom: 0; } +.settings-row > svg:first-child { color: var(--blue); } +.settings-row > svg:last-child { width: .9rem; color: #90979f; } +.settings-row span, .settings-row strong { display: block; } +.settings-row span { color: #858c94; font-size: .62rem; } +.settings-row strong { margin-top: .25rem; font-size: .75rem; } +.not-found { min-height: 550px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; } +.not-found > svg { width: 3rem; height: 3rem; margin-bottom: 1.5rem; color: var(--blue); } +.not-found h1 { max-width: 640px; margin: 0; font-size: 2rem; } +.not-found p { color: var(--muted); } +.not-found .button { margin-top: 1rem; } + +/* Overlays */ +.drawer-layer, .modal-layer { position: fixed; inset: 0; z-index: 100; } +.drawer-scrim, .modal-scrim { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: rgba(19,24,30,.35); backdrop-filter: blur(2px); } +.mobile-drawer { position: absolute; top: 0; left: 0; width: min(88vw, 360px); height: 100%; padding: 1.2rem; background: var(--paper); box-shadow: 15px 0 45px rgba(20,25,30,.16); animation: drawer-in .2s ease-out; } +@keyframes drawer-in { from { transform: translateX(-100%); } } +.mobile-drawer__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 2rem; } +.drawer-label { margin: 1.3rem .7rem .5rem; color: #989fa7; font-size: .6rem; font-weight: 700; letter-spacing: .12em; } +.mobile-drawer nav { display: grid; gap: .25rem; } +.mobile-drawer nav a { display: flex; align-items: center; gap: .75rem; min-height: 44px; padding: 0 .7rem; border-radius: 8px; color: #5d6670; font-size: .78rem; } +.mobile-drawer nav a.active { color: var(--blue); background: var(--blue-pale); } +.drawer-disclosure { position: absolute; left: 1.2rem; right: 1.2rem; bottom: 1.2rem; display: flex; gap: .7rem; padding: .9rem; border: 1px solid var(--line); border-radius: 9px; background: white; } +.drawer-disclosure svg { color: var(--blue); } +.drawer-disclosure p { margin: 0; } +.drawer-disclosure strong, .drawer-disclosure span { display: block; font-size: .65rem; } +.drawer-disclosure span { margin-top: .3rem; color: #7a828a; } +.action-review { position: absolute; top: 50%; left: 50%; width: min(560px, calc(100% - 32px)); max-height: calc(100vh - 32px); overflow-y: auto; padding: 1.6rem; border: 1px solid var(--line); border-radius: 14px; background: var(--paper); box-shadow: 0 30px 80px rgba(16,23,31,.2); transform: translate(-50%,-50%); } +.action-review__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.2rem; } +.action-review__icon { width: 42px; height: 42px; display: grid; place-items: center; border-radius: 10px; color: var(--blue); background: var(--blue-pale); } +.action-review h2 { margin: 0; font-size: 1.55rem; } +.action-review > p { color: var(--muted); font-size: .75rem; line-height: 1.6; } +.review-steps { margin: 1.2rem 0; padding: 0; list-style: none; border-top: 1px solid var(--line); } +.review-steps li { display: grid; grid-template-columns: 25px 1fr; gap: .8rem; padding: 1rem 0; border-bottom: 1px solid var(--line); } +.review-steps li > span { width: 23px; height: 23px; display: grid; place-items: center; border-radius: 50%; color: var(--blue); background: var(--blue-pale); font-size: .63rem; font-weight: 700; } +.review-steps strong { font-size: .72rem; } +.review-steps p { margin: .3rem 0 0; color: var(--muted); font-size: .67rem; line-height: 1.5; } +.review-boundary { display: flex; gap: .8rem; padding: .9rem; border-radius: 9px; background: #f0f1ee; } +.review-boundary svg { color: #6c747d; } +.review-boundary strong, .review-boundary small { display: block; } +.review-boundary strong { font-size: .68rem; } +.review-boundary small { margin-top: .25rem; color: #737b84; font-size: .61rem; } +.action-review__actions { display: flex; justify-content: flex-end; gap: .7rem; margin-top: 1.2rem; } + +@media (max-width: 1040px) { + .landing-hero__grid { grid-template-columns: 1fr 1fr; } + .artifact-stage { min-height: 480px; } + .facility-artifact { width: min(330px, 62vw); } + .capability-grid { grid-template-columns: 1fr 1fr; } + .capability:nth-child(2) { border-right: 0; } + .capability:nth-child(-n+2) { border-bottom: 1px solid var(--line); } + .master-detail { grid-template-columns: 1fr; } + .object-inspector { border-top: 1px solid var(--line); border-left: 0; } + .relationship-spine { grid-template-columns: repeat(3,1fr); } + .relationship-spine li:nth-child(3) { border-right: 0; } + .relationship-spine li:nth-child(-n+3) { border-bottom: 1px solid var(--line); } + .child-table__head, .child-row { grid-template-columns: minmax(170px,1.4fr) minmax(100px,.8fr) repeat(3,minmax(85px,.7fr)) 20px; gap: .65rem; } +} + +@media (max-width: 760px) { + .shell-frame { width: calc(100% - 20px); } + .landing { padding-top: 10px; } + .landing-hero { min-height: auto; } + .public-header { height: 66px; grid-template-columns: 1fr auto; padding: 0 5%; } + .public-header__nav { display: none; } + .landing-hero__grid { grid-template-columns: 1fr; padding: 2.5rem 5% 1rem; } + .hero-copy { padding: 0; } + .hero-copy h1 { font-size: clamp(3rem,15vw,4.8rem); } + .hero-copy__body { font-size: .92rem; } + .hero-actions { display: grid; grid-template-columns: 1fr; } + .proof-line { margin-top: 2rem; } + .artifact-stage { min-height: 420px; margin: -1rem 0; } + .facility-artifact { width: 270px; } + .hero-evidence { align-items: flex-start; flex-direction: column; gap: .55rem; padding: 1rem 5%; } + .landing-section { width: calc(100% - 32px); margin: 6rem auto; } + .capability-grid { grid-template-columns: 1fr; margin-top: 2.5rem; } + .capability, .capability:nth-child(2) { min-height: 230px; border-right: 0; border-bottom: 1px solid var(--line); } + .capability:last-child { border-bottom: 0; } + .capability__index { margin-bottom: 2rem; } + .process-section, .proof-section { grid-template-columns: 1fr; } + .process-section__copy { position: static; margin-bottom: 2.5rem; } + .proof-section { padding: 1rem; } + .proof-object { min-height: 280px; } + .proof-section__copy { padding: .8rem; } + .public-footer { width: calc(100% - 32px); grid-template-columns: 1fr; padding: 2rem 0; } + .public-footer > div { justify-self: start; } + + .app-header { grid-template-columns: auto 1fr auto; gap: .7rem; height: 60px; padding: 0 12px; } + .app-header__menu { display: grid; } + .app-header__nav, .account-control { display: none; } + .app-header .brand { justify-self: start; } + .network-mark { padding: 0 .55rem; } + .network-mark > span:not(.network-mark__dot):not(.sr-only) { display: none; } + .app-content { width: calc(100% - 24px); padding: 2rem 0 7rem; } + .app-footer { display: none; } + .mobile-bottom-nav { position: fixed; left: 0; right: 0; bottom: 0; z-index: 40; min-height: 64px; display: grid; grid-template-columns: repeat(4,1fr); padding: 6px 4px max(6px, env(safe-area-inset-bottom)); border-top: 1px solid var(--line); background: rgba(251,251,248,.94); backdrop-filter: blur(18px); } + .mobile-bottom-nav a { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .2rem; color: #767e87; font-size: .56rem; } + .mobile-bottom-nav a.active { color: var(--blue); } + .mobile-bottom-nav svg { width: 1.15rem; height: 1.15rem; } + .page-heading { align-items: flex-start; flex-direction: column; } + .page-heading h1 { font-size: 2.4rem; } + .register-toolbar { gap: .6rem; } + .search-control { width: 100%; } + .master-detail { min-height: auto; } + .register-head { display: none; } + .facility-row { grid-template-columns: 1fr auto; gap: .8rem; margin: .5rem; } + .facility-row > span:not(.facility-row__title):not(.status) { display: none; } + .register-empty-line { flex-direction: column; gap: .3rem; } + .object-inspector { padding: 1rem; } + .accord-header { align-items: flex-start; flex-direction: column; } + .accord-header h1 { font-size: 2.6rem; } + .accord-header__action { width: 100%; } + .facility-tabs { overflow-x: auto; gap: 1.3rem; scrollbar-width: none; } + .facility-tabs a { white-space: nowrap; } + .metrics-band, .metrics-band--three, .metrics-band--four { grid-template-columns: 1fr 1fr; } + .metric { min-height: 120px; padding: 1.1rem; border-bottom: 1px solid var(--line); } + .metric:nth-child(2n) { border-right: 0; } + .metric:nth-last-child(-n+2) { border-bottom: 0; } + .metrics-band--three .metric:last-child { grid-column: span 2; border-top: 1px solid var(--line); } + .state-explanation { grid-template-columns: auto 1fr; align-items: start; } + .invariant-list { grid-column: 1 / -1; padding: 1rem 0 0; border-top: 1px solid #d7dfee; border-left: 0; } + .relationship-spine { display: grid; grid-template-columns: 1fr; } + .relationship-spine li, .relationship-spine li:nth-child(3) { border-right: 0; border-bottom: 1px solid var(--line); } + .relationship-spine li:last-child { border-bottom: 0; } + .relationship-spine li:not(:last-child)::after { top: auto; right: 50%; bottom: -5px; transform: rotate(135deg); } + .relationship-spine a { min-height: 76px; display: grid; grid-template-columns: 26px 30px 1fr auto; grid-template-rows: auto auto; align-items: center; gap: .2rem .7rem; } + .relationship-spine a > svg { grid-row: span 2; margin: 0; } + .relationship-spine strong { align-self: end; } + .relationship-spine small { align-self: start; margin: 0; } + .relationship-spine .status { grid-column: 4; grid-row: 1 / span 2; } + .child-table__head { display: none; } + .child-row { grid-template-columns: 1fr auto; padding: .9rem .2rem; } + .child-row > span:nth-child(n+3):not(:last-child) { display: none; } + .child-row > span:last-child { display: none; } + .split-section, .round-summary, .evidence-grid, .dossier-grid { grid-template-columns: 1fr; } + .round-summary { align-items: start; } + .evidence-grid .evidence-summary { grid-column: auto; } + .lineage-node { grid-template-columns: 32px 30px 1fr auto; } + .lineage-node .status { display: none; } + .draw-legs > a { grid-template-columns: auto 1fr auto; } + .draw-legs > a > svg { display: none; } + .dossier-id { align-items: flex-start; flex-direction: column; } + .proof-detail code { font-size: .65rem; } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; } +} diff --git a/frontend/src/test/setup.ts b/frontend/src/test/setup.ts new file mode 100644 index 0000000..bb32bff --- /dev/null +++ b/frontend/src/test/setup.ts @@ -0,0 +1,5 @@ +import "@testing-library/jest-dom/vitest"; +import { cleanup } from "@testing-library/react"; +import { afterEach } from "vitest"; + +afterEach(cleanup); diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..bb71af9 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["vitest/globals"], + "paths": { + "@concord-protocol/sdk": ["../sdk/typescript/src/index.ts"] + } + }, + "include": ["src", "../sdk/typescript/src/index.ts", "../config/coston2/concord-deployment.json"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..fc2337c --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@concord-protocol/sdk": new URL("../sdk/typescript/src/index.ts", import.meta.url).pathname, + }, + }, + server: { + fs: { + allow: [new URL("..", import.meta.url).pathname], + }, + }, + test: { + environment: "jsdom", + setupFiles: "./src/test/setup.ts", + css: true, + }, +}); diff --git a/infra/railway/proxy.Dockerfile b/infra/railway/proxy.Dockerfile new file mode 100644 index 0000000..2bc212a --- /dev/null +++ b/infra/railway/proxy.Dockerfile @@ -0,0 +1,22 @@ +ARG TEE_PROXY_VERSION=v0.0.18 + +FROM golang:1.25.1-alpine AS build +ARG TEE_PROXY_VERSION +RUN apk add --no-cache git +WORKDIR /src +RUN git clone --depth 1 --branch "${TEE_PROXY_VERSION}" https://github.com/flare-foundation/tee-proxy.git +WORKDIR /src/tee-proxy +RUN go mod download && go mod verify +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOFLAGS="-buildvcs=false" \ + go build -trimpath -ldflags="-buildid= -s -w" -o /out/tee-proxy ./cmd/proxy + +FROM alpine:3.21 +RUN apk add --no-cache ca-certificates +WORKDIR /app +COPY --from=build --chmod=755 /out/tee-proxy /app/tee-proxy +COPY --chmod=755 infra/railway/start-proxy.sh /app/start-proxy.sh +RUN addgroup -g 1001 -S concord && adduser -u 1001 -S concord -G concord && chown -R concord:concord /app +USER concord +ENV PORT=6664 +EXPOSE 6663 6664 +ENTRYPOINT ["/app/start-proxy.sh"] diff --git a/infra/railway/proxy.railway.toml b/infra/railway/proxy.railway.toml new file mode 100644 index 0000000..c496ac7 --- /dev/null +++ b/infra/railway/proxy.railway.toml @@ -0,0 +1,8 @@ +[build] +builder = "DOCKERFILE" +dockerfilePath = "infra/railway/proxy.Dockerfile" + +[deploy] +healthcheckPath = "/info" +healthcheckTimeout = 300 +restartPolicyType = "ALWAYS" diff --git a/infra/railway/start-proxy.sh b/infra/railway/start-proxy.sh new file mode 100755 index 0000000..64bd079 --- /dev/null +++ b/infra/railway/start-proxy.sh @@ -0,0 +1,51 @@ +#!/bin/sh +set -eu + +: "${PROXY_PRIVATE_KEY:?PROXY_PRIVATE_KEY is required}" +: "${REDIS_ENDPOINT:?REDIS_ENDPOINT is required, for example concord-redis.railway.internal:6379}" +: "${INDEXER_DB_HOST:?INDEXER_DB_HOST is required}" +: "${INDEXER_DB_NAME:?INDEXER_DB_NAME is required}" +: "${INDEXER_DB_USER:?INDEXER_DB_USER is required}" +: "${INDEXER_DB_PASSWORD:?INDEXER_DB_PASSWORD is required}" + +toml_escape() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +mkdir -p /app/config + +cat > /app/config/config.toml < [extension-id]" >&2; exit 2; } +command -v curl >/dev/null || { echo "curl is required" >&2; exit 2; } +command -v jq >/dev/null || { echo "jq is required" >&2; exit 2; } + +INFO_JSON="$(curl --fail --silent --show-error --max-time 20 "${PROXY_URL%/}/info")" +OBSERVED_EXTENSION="$(jq -r '.extensionId // empty' <<<"$INFO_JSON")" +[[ -n "$OBSERVED_EXTENSION" ]] || { echo "proxy /info omitted extensionId" >&2; exit 1; } + +normalize_extension() { + local raw="${1#0x}" + raw="$(sed 's/^0*//' <<<"$raw")" + [[ -n "$raw" ]] || raw=0 + if [[ "$1" == 0x* ]]; then printf '%d' "0x$raw"; else printf '%d' "$raw"; fi +} + +EXPECTED_DECIMAL="$(normalize_extension "$EXTENSION_ID")" +OBSERVED_DECIMAL="$(normalize_extension "$OBSERVED_EXTENSION")" +[[ "$OBSERVED_DECIMAL" == "$EXPECTED_DECIMAL" ]] || { + echo "proxy extension mismatch: expected $EXTENSION_ID, observed $OBSERVED_EXTENSION" >&2 + exit 1 +} + +jq '{extensionId, codeHash, platform}' <<<"$INFO_JSON" +echo "proxy binding confirmed for extension $EXPECTED_DECIMAL" + +if command -v go >/dev/null && [[ -f tools/go.mod ]]; then + ( + cd tools + go run ./cmd/query-tee -rpc "$RPC_URL" -ext "$EXPECTED_DECIMAL" + ) +else + echo "Go toolchain unavailable; skipped the read-only onchain active-machine query" +fi + +echo "Development-path note: SIMULATED_TEE=true is not production hardware-backed execution." From 78e8f804505c4f6a0b55ff771ef017b0ab13318b Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 14 Aug 2026 08:26:45 +0200 Subject: [PATCH 085/107] feat: refine product frontend and Northflank FCC hosting --- README.md | 4 +- docs/CONCORD_STATUS.md | 25 +- docs/fcc-always-on-hosting.md | 64 ++-- docs/frontend-art-direction.md | 13 + docs/frontend-map.md | 10 +- docs/source-snapshot-2026-08-14.yaml | 13 +- frontend/README.md | 26 +- frontend/index.html | 2 +- frontend/package-lock.json | 395 ++++++++++++++++++++-- frontend/package.json | 4 +- frontend/src/App.test.tsx | 21 +- frontend/src/App.tsx | 222 ++++++++---- frontend/src/components/WalletControl.tsx | 118 +++++++ frontend/src/main.tsx | 14 +- frontend/src/styles.css | 122 ++++++- frontend/src/web3.ts | 16 + infra/northflank/README.md | 104 ++++++ infra/northflank/proxy.variables.example | 19 ++ infra/northflank/tee.variables.example | 14 + 19 files changed, 1034 insertions(+), 172 deletions(-) create mode 100644 frontend/src/components/WalletControl.tsx create mode 100644 frontend/src/web3.ts create mode 100644 infra/northflank/README.md create mode 100644 infra/northflank/proxy.variables.example create mode 100644 infra/northflank/tee.variables.example diff --git a/README.md b/README.md index 2bfc459..aeb163a 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,9 @@ repayments are explicit nodes in its lineage. production handler into a plaintext path. - The React frontend renders the recorded Coston2 Root Accord, Makkari/CoFill formation, three funded Child Accords, explicit draw legs, repayment, - restored capacity, and evidence boundary through one shared SDK model. + restored capacity, and evidence boundary through one shared SDK model. It + also connects injected wallets, switches to Coston2, and reads the live + native balance without retaining keys or auto-submitting transactions. ## Truth boundary diff --git a/docs/CONCORD_STATUS.md b/docs/CONCORD_STATUS.md index 581f47f..9febdee 100644 --- a/docs/CONCORD_STATUS.md +++ b/docs/CONCORD_STATUS.md @@ -4,7 +4,7 @@ Updated 2026-08-14 after the first frontend implementation and always-on FCC hosting package. The complete observed Coston2 lifecycle remains the canonical deployment proof. -- The fresh source snapshot used for this checkpoint is [source-snapshot-2026-08-14.yaml](source-snapshot-2026-08-14.yaml). It records the current official Coston2 FCC development path and the Railway/Northflank availability decision. +- The fresh source snapshot used for this checkpoint is [source-snapshot-2026-08-14.yaml](source-snapshot-2026-08-14.yaml). It records the current official Coston2 FCC development path and the Northflank/Railway availability decision. ## Works in the durable checkpoint @@ -30,13 +30,17 @@ deployment proof. - `scripts/coston2-preflight.sh` provides a read-only offline/live operator gate, and the adversarial cases are recorded in `docs/experiments/coston2-failure-matrix.md`. -- The React/Vite frontend now implements the institutional landing page, +- The React/Vite frontend now implements the outcome-led institutional landing page, facility register, Root Accord workspace, funding, activity, evidence, lineage, child, and draw views from the recorded Coston2 deployment and the - shared TypeScript SDK types. Its build and four semantic tests pass locally. -- `infra/railway/` and `docs/fcc-always-on-hosting.md` package the official - three-service FCC topology for a stable Railway deployment, with Northflank - documented as the fallback. No new hosted machine has been registered yet. + shared TypeScript SDK types. Injected-wallet connection, Coston2 switching, + native balance reads, and the official faucet link are live. Its build and + six semantic tests pass locally. +- `infra/northflank/`, `infra/railway/`, and + `docs/fcc-always-on-hosting.md` package the official two-service plus Redis + FCC topology. Northflank is the primary continuously running hackathon + development host; paid Railway is the fallback. No new hosted machine has + been registered yet. ## Observed Coston2 deployment @@ -127,10 +131,11 @@ bound to the current token, and its current root round is recorded below. the documented Coston2 simulated development TEE path. - A named Cloudflare Tunnel or custom-domain ingress has not been provisioned; the stable Workers.dev relay remains the current development ingress. -- The institutional read frontend is implemented, but browser-wallet intent - generation and transaction submission remain disconnected. The evidence - above still comes from the contract/FCC runners and explorer receipts. -- The Railway/Northflank runtime bundle is reproducible but not yet deployed; +- The institutional frontend can connect and identify an injected Coston2 + wallet, but lifecycle intent generation and transaction submission remain + disconnected. The evidence above still comes from the contract/FCC runners + and explorer receipts. +- The Northflank/Railway runtime bundle is reproducible but not yet deployed; a fresh hosted simulated TEE identity must be registered and the stale machine paused before the endpoint can be called continuously available. diff --git a/docs/fcc-always-on-hosting.md b/docs/fcc-always-on-hosting.md index 17bab44..ea94836 100644 --- a/docs/fcc-always-on-hosting.md +++ b/docs/fcc-always-on-hosting.md @@ -4,22 +4,23 @@ This runbook moves Concord's current Coston2 FCC development runtime from the disposable Codespace relay to a stable, long-running host. It does not alter Accord, Makkari, CoFill, Lineage, or the completed facility evidence. -## Recommended topology: Railway +## Recommended topology: Northflank -Railway is the primary target because it supports Dockerfile builds, -configuration-as-code, private service DNS, stable public domains, and an -`ALWAYS` restart policy. `ALWAYS` requires a paid plan; free/trial deployments -must not be described as continuously available. +Northflank is the primary target for the public Coston2 test because its +long-running services do not sleep, it supports private service DNS, stable +public HTTP domains, health checks, and a private Redis addon. The Northflank +sandbox is useful for the hackathon development runtime, but Northflank does +not describe the sandbox as production. -Create one Railway project with three services: +Create one Northflank project with two combined services and one addon: 1. `concord-fcc-proxy` from this repository, using - `infra/railway/proxy.railway.toml`. Give port `6664` a stable public HTTPS - domain and set `PORT=6664`. + `infra/railway/proxy.Dockerfile`. Give port `6664` a stable public HTTPS + domain; keep port `6663` private. 2. `concord-fcc-tee` from this repository, using - `infra/railway/tee.railway.toml`. Keep it private. -3. A private Redis service. Persistence is not required for the current - ephemeral development queue; do not expose Redis publicly. + `go/Dockerfile`. Keep ports `5501`, `7701`, and `7702` private. +3. A private Redis addon. Persistence is not an economic source of truth; do + not expose Redis publicly. The provider-facing path is direct: @@ -27,17 +28,17 @@ The provider-facing path is direct: Flare providers -> https://stable-proxy-host/instruction :6664 -> tee-proxy - -> private Railway DNS :6663 + -> private Northflank DNS :6663 -> Concord Go extension TEE ``` ### Proxy variables -Store these as Railway secrets/variables. Never commit their values. +Store these as Northflank secret-group variables. Never commit their values. ```text PROXY_PRIVATE_KEY -REDIS_ENDPOINT=concord-redis.railway.internal:6379 +REDIS_ENDPOINT=: INDEXER_DB_HOST INDEXER_DB_PORT=3306 INDEXER_DB_NAME @@ -62,16 +63,19 @@ EXTENSION_ID=0x000000000000000000000000000000000000000000000000000000000001028c INITIAL_OWNER= GOVERNANCE_SIGNERS= GOVERNANCE_THRESHOLD=1 -PROXY_URL=http://concord-fcc-proxy.railway.internal:6663 +PROXY_URL=http://concord-fcc-proxy:6663 CONFIG_PORT=5501 SIGN_PORT=7701 EXTENSION_PORT=7702 LOG_LEVEL=INFO ``` -Service names determine Railway private DNS. If the proxy service has another +Service names determine Northflank private DNS. If the proxy service has another name, update `PROXY_URL` accordingly. +The exact setup contract and environment templates are in +`infra/northflank/README.md`. + ## Identity and cutover rule The stable hostname survives a restart; the simulated TEE identity does not. @@ -98,15 +102,15 @@ The currently recorded machine is Workers.dev-to-Codespace development relay. It remains historical evidence; it is not evidence that the old upstream is continuously available. -## Northflank fallback +## Railway fallback -Northflank can run the same two Dockerfiles as long-running combined services. -Expose only proxy port `6664` with TLS, keep proxy port `6663`, the TEE ports, -and Redis private, configure an HTTP `/info` health check, and keep the service -scale at one or greater. Scale zero is unavailable and is not always-on. +Railway can run the same two Dockerfiles with a private Redis service. Use the +checked-in Railway TOML files, a paid plan with `ALWAYS` restart, one replica, +and a stable public proxy domain. Free or trial Railway deployments must not be +described as continuously available. -Use the same variables and the same identity cutover. Northflank changes the -service DNS name only; it does not change Concord or FCC semantics. +Use the same variables and the same identity cutover. The hosting provider +changes service DNS only; it does not change Concord or FCC semantics. ## Reproducibility and operations @@ -116,8 +120,11 @@ service DNS name only; it does not change Concord or FCC semantics. registry check against any stable deployment. - `proxy.Dockerfile` pins `tee-proxy` to the same `v0.0.18` line as the current Concord extension and does not mix Flare dependency versions. -- Railway configuration declares `ALWAYS` restart. Use a paid plan and alerts; - restart policy alone is not a service-level guarantee. +- Northflank configuration is recorded as an exact service checklist and + environment contract because platform secrets and generated addon endpoints + must remain outside Git. Import it into a GitOps template after the first + project is created and export Northflank's generated specification back to + `infra/northflank` for drift control. ## Truth boundary @@ -130,8 +137,7 @@ Official platform references used for this decision: - Flare FCE scaffold: https://github.com/flare-foundation/fce-extension-scaffold - Flare FCC getting started: https://dev.flare.network/fcc/guides/getting-started -- Railway config-as-code: https://docs.railway.com/config-as-code/reference -- Railway private networking: https://docs.railway.com/networking/private-networking -- Railway restart policy: https://docs.railway.com/deployments/restart-policy -- Northflank services: https://northflank.com/features/application-layer +- Northflank infrastructure as code: https://northflank.com/docs/v1/application/infrastructure-as-code/infrastructure-as-code +- Northflank private ports: https://northflank.com/docs/v1/application/network/configure-ports - Northflank health checks: https://northflank.com/docs/v1/application/observe/configure-health-checks +- Railway config-as-code: https://docs.railway.com/config-as-code/reference diff --git a/docs/frontend-art-direction.md b/docs/frontend-art-direction.md index 955e13f..1c8e716 100644 --- a/docs/frontend-art-direction.md +++ b/docs/frontend-art-direction.md @@ -21,6 +21,19 @@ This gives Concord institutional seriousness without making the first-use experience feel like a terminal. The default interface is bright, calm, and highly legible. Dark mode is not a separate brand or data model. +The implemented public narrative follows progressive disclosure: + +1. outcome: one facility composed from multiple accountable providers; +2. participant value: treasury, provider, and shared understanding; +3. topology: one Root Accord and independently governed Child Accords; +4. journey: creation, private coordination, composition, funding, draw, and + restored capacity; +5. privacy boundary; and +6. recorded Coston2 proof. + +Protocol primitive names never have to disappear, but they arrive after the +human outcome they explain. + The visual territory board is a reference study, not a production asset: `docs/assets/concord-visual-territories-v1.png`. diff --git a/docs/frontend-map.md b/docs/frontend-map.md index e711ede..580018e 100644 --- a/docs/frontend-map.md +++ b/docs/frontend-map.md @@ -37,14 +37,15 @@ and cannot be the only way to understand causality. | Route | Purpose | Required contract data | |---|---|---| -| `/` | Role-aware entry and facility list | health, accessible facilities, role context | +| `/` | Outcome-first product story, wallet entry, and public proof | recorded evidence, network identity | +| `/facilities` | Facility register | accessible/recorded facilities and current state | | `/facilities/:rootAccordId` | Primary Root Accord workspace | facility, round, children, invariants | | `/facilities/:rootAccordId/lineage` | Full causal relationship trail | lineage, linked public actions | | `/rounds/:roundId` | Makkari/CoFill status | round, authorization-scoped evidence | | `/children/:childAccordId` | Provider relationship view | child, root summary, funding/exposure | | `/draws/:drawId` | One draw and its explicit legs | draw, child/provider links, settlements | | `/evidence/:resultDigest` | Verification and disclosure boundary | evidence metadata, source, warning | -| `/settings/network` | Network/assets and disclosure settings | network config, asset decimals | +| `/settings` | Network/assets and disclosure settings | network config, asset decimals | ## Components @@ -159,3 +160,8 @@ never call contract methods directly from arbitrary components. Wallet calls are made from an action boundary after the API/SDK returns an unsigned intent. This keeps the same semantics across web, CLI, REST, MCP, and institution integrations. + +The current wallet foundation intentionally stops before that write boundary: +it connects an injected wallet, switches to Coston2, and reads the native +balance. It does not infer a treasury/provider role from an address and does +not submit facility transactions. diff --git a/docs/source-snapshot-2026-08-14.yaml b/docs/source-snapshot-2026-08-14.yaml index 1a1f68a..fcf7b23 100644 --- a/docs/source-snapshot-2026-08-14.yaml +++ b/docs/source-snapshot-2026-08-14.yaml @@ -33,9 +33,11 @@ snapshot: - SIMULATED_TEE=true remains a supported Coston2 hackathon development path and is not production hardware-backed execution. - A simulated TEE restart creates a new identity; the new machine must be registered and promoted and the stale identity paused. - Exactly one active machine per endpoint avoids random dispatch to stale machines. + - Northflank private services resolve as service-name:port inside one project, and public HTTP ports receive managed HTTPS endpoints. + - Northflank sandbox services do not sleep, but Northflank does not position the sandbox as production infrastructure. - Railway supports Dockerfile configuration-as-code, private service DNS, and stable public domains. - Railway ALWAYS restart is unavailable on free/trial plans; an always-on claim requires a paid plan and no scale-to-zero behavior. - - Northflank long-running services are a viable fallback when scale remains at one or greater and only proxy port 6664 is public. + - Northflank supports health checks, GitOps templates, private services, and Redis addons for the primary long-running hackathon development topology. current_recorded_machine: extension_id_decimal: 66188 tee_id: 0xeE39d5e7d1C5043232282e3CC884B41a9Db22c85 @@ -44,12 +46,13 @@ snapshot: registered_url: https://concord-fcc-ingress.microcosm.workers.dev availability_now: unavailable_because_disposable_codespace_upstream_is_offline decision: - primary_host: Railway paid long-running services - fallback_host: Northflank long-running combined services + primary_host: Northflank long-running combined services plus private Redis addon + fallback_host: Railway paid long-running services cutover_required: true - reason: A stable host removes the disposable Codespace ingress dependency, but the fresh simulated TEE identity must still be deliberately registered and the stale machine paused. + reason: Northflank removes the disposable Codespace ingress dependency without scale-to-zero, but a fresh simulated TEE identity must still be deliberately registered and the stale machine paused after any TEE restart. product_semantics: - contradiction_found: false + contradiction_found: true + contradiction: The current official Developer Hub supports SIMULATED_TEE=true for a live Coston2 development path, while production deployment guidance states that simulated attestation is rejected for production. Concord resolves this by using simulated mode only for the Coston2 development proof and making no hardware-backed or production TEE claim. mvp_changed: false fdc_added: false privacy_claim_expanded: false diff --git a/frontend/README.md b/frontend/README.md index 20f6b58..a368009 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -36,10 +36,30 @@ The semantic map remains in `docs/frontend-map.md`. Visual decisions are in Heroicons is the single interface icon family. Inter Variable is self-hosted through the npm package; no runtime font CDN is required. +The public landing page now leads with the user outcome—one accountable +facility composed from independent providers—before progressively disclosing +Accord, Makkari, CoFill, FCC, and Lineage. Its relationship composition, +prismatic facility object, pointer response, and ambient motion all respect the +user's reduced-motion preference. + +## Wallet connection + +The app supports injected EVM wallets through Wagmi and Viem. A user can: + +- connect an injected wallet such as MetaMask or Rabby; +- add or switch to Flare Testnet Coston2 through the wallet; +- see the connected address and live C2FLR balance; +- open the official Coston2 faucet for C2FLR, FXRP, and USDT0; and +- disconnect without Concord retaining a private key. + +No WalletConnect project credential is bundled, so the UI does not claim +WalletConnect support. Public evidence remains browsable without connecting. + ## Current interaction boundary -Read views use recorded Coston2 evidence. The “Prepare draw” review explains -the unsigned intent flow but does not fabricate calldata or transaction -success: a live intent service and wallet approval path are still required. +Read views use recorded Coston2 evidence. Wallet connection establishes live +network identity and balance only. The “Prepare draw” review explains the +unsigned intent flow but does not fabricate calldata or transaction success: a +live intent service and transaction-action boundary are still required. The FCC label is intentionally “simulated development TEE”; token settlement and public EVM state are not represented as private. diff --git a/frontend/index.html b/frontend/index.html index ca5fbf9..d43ea59 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,7 +8,7 @@ name="description" content="Concord coordinates persistent capital relationships on Flare." /> - Concord — Private capital, coordinated. + Concord — One facility. Many providers. Always accountable.
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 55dc27a..34bb4fd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,7 +13,9 @@ "@tanstack/react-query": "5.101.4", "react": "19.2.8", "react-dom": "19.2.8", - "react-router-dom": "7.18.2" + "react-router-dom": "7.18.2", + "viem": "2.55.15", + "wagmi": "3.7.6" }, "devDependencies": { "@testing-library/jest-dom": "7.0.1", @@ -34,6 +36,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, "node_modules/@asamuzakjp/css-color": { "version": "6.0.7", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", @@ -300,6 +308,45 @@ "dev": true, "license": "MIT" }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@oxc-project/types": { "version": "0.144.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", @@ -555,6 +602,42 @@ "dev": true, "license": "MIT" }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -710,7 +793,7 @@ "version": "19.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -733,7 +816,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -750,7 +832,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -767,7 +848,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -784,7 +864,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -801,7 +880,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -818,7 +896,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -835,7 +912,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -852,7 +928,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -869,7 +944,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -886,7 +960,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -903,7 +976,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -920,7 +992,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -937,7 +1008,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -954,7 +1024,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -971,7 +1040,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -988,7 +1056,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1005,7 +1072,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1022,7 +1088,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1039,7 +1104,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1056,7 +1120,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1205,6 +1268,27 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1315,7 +1399,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/data-urls": { @@ -1412,6 +1496,12 @@ "@types/estree": "^1.0.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/expect-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", @@ -1485,6 +1575,21 @@ "dev": true, "license": "MIT" }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1843,6 +1948,26 @@ "node": ">=4" } }, + "node_modules/mipd": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mipd/-/mipd-0.0.7.tgz", + "integrity": "sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wagmi-dev" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/nanoid": { "version": "3.3.18", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", @@ -1876,6 +2001,36 @@ "node": ">=12.20.0" } }, + "node_modules/ox": { + "version": "0.14.33", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.33.tgz", + "integrity": "sha512-rooA/4o7bBof4Ge2VH/eovfNPb/AEEYyrNj03wggc55g5HZD8Pjs/OeWhttgjic3dDcqn0r29bDuvQEdTiUemQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -2265,7 +2420,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc" @@ -2306,6 +2461,45 @@ "node": ">=22.19.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/viem": { + "version": "2.55.15", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.55.15.tgz", + "integrity": "sha512-ka9SfSJ3ZfhuUEzTGmufwALRvEPVKM068tF0AwfdRZagqA3yuFa/QoXIvALzr9y47m4Wiisl3yEoYWuFQso6Ng==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.14.33", + "ws": "8.21.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/vite": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", @@ -2487,6 +2681,138 @@ "node": ">=18" } }, + "node_modules/wagmi": { + "version": "3.7.6", + "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-3.7.6.tgz", + "integrity": "sha512-Zm/ppGHV6GYX9by3ERs1KVcNqpMZ4Vdno48Avdz9U+3QCjC5iFatOwruA9IMOl8poL7n/pJGQOcR1xj8N5c69w==", + "license": "MIT", + "dependencies": { + "@wagmi/connectors": "8.1.0", + "@wagmi/core": "3.6.4", + "use-sync-external-store": "1.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "@tanstack/react-query": ">=5.0.0", + "react": ">=18", + "typescript": ">=5.9.3", + "viem": "2.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/wagmi/node_modules/@wagmi/connectors": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-8.1.0.tgz", + "integrity": "sha512-ASlHXq9NC4pp617lalZdlPuPnZUhFeWNEKQfm3ngVSQKd4S2is5XXG4v3EOAhcxdLjSk1AMfGXlHgk5tX2WCZg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "@base-org/account": "^2.5.1", + "@coinbase/wallet-sdk": "^4.3.6", + "@metamask/connect-evm": "^2.1.0", + "@safe-global/safe-apps-provider": "~0.18.6", + "@safe-global/safe-apps-sdk": "^9.1.0", + "@wagmi/core": "3.6.4", + "@walletconnect/ethereum-provider": "^2.21.1", + "accounts": "~0.14", + "typescript": ">=5.9.3", + "viem": "2.x" + }, + "peerDependenciesMeta": { + "@base-org/account": { + "optional": true + }, + "@coinbase/wallet-sdk": { + "optional": true + }, + "@metamask/connect-evm": { + "optional": true + }, + "@safe-global/safe-apps-provider": { + "optional": true + }, + "@safe-global/safe-apps-sdk": { + "optional": true + }, + "@walletconnect/ethereum-provider": { + "optional": true + }, + "accounts": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/wagmi/node_modules/@wagmi/core": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-3.6.4.tgz", + "integrity": "sha512-YERJ+vbFZw3UI6p4KoFU5DkxkqmtzBq5lddpgVAvcu5Tz7kSicKVA3DMCWvjVAFsikNdZYIs39FIkMGWfzpSMQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "5.0.1", + "mipd": "0.0.7", + "zustand": "5.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "@tanstack/query-core": ">=5.0.0", + "accounts": "~0.14", + "typescript": ">=5.9.3", + "viem": "2.x" + }, + "peerDependenciesMeta": { + "@tanstack/query-core": { + "optional": true + }, + "accounts": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/wagmi/node_modules/@wagmi/core/node_modules/zustand": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0.tgz", + "integrity": "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -2539,6 +2865,27 @@ "node": ">=8" } }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index a18c1cb..8fb9c87 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,9 @@ "@tanstack/react-query": "5.101.4", "react": "19.2.8", "react-dom": "19.2.8", - "react-router-dom": "7.18.2" + "react-router-dom": "7.18.2", + "viem": "2.55.15", + "wagmi": "3.7.6" }, "devDependencies": { "@testing-library/jest-dom": "7.0.1", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 71bfaad..ed376e2 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -1,23 +1,32 @@ import { render, screen } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { MemoryRouter } from "react-router-dom"; import { describe, expect, it } from "vitest"; +import { WagmiProvider } from "wagmi"; import { App } from "./App"; import { children, draw, facility, formatToken } from "./data/concord"; +import { wagmiConfig } from "./web3"; function renderRoute(path: string) { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); return render( - - - , + + + + + + + , ); } describe("Concord frontend product semantics", () => { it("presents the relationship as the product on the landing page", () => { renderRoute("/"); - expect(screen.getByRole("heading", { name: /private capital,\s*coordinated/i })).toBeInTheDocument(); - expect(screen.getByText(/the relationship is the product/i)).toBeInTheDocument(); - expect(screen.getByText(/simulated development tee/i)).toBeInTheDocument(); + expect(screen.getByRole("heading", { name: /one facility\.\s*many providers\.\s*always accountable/i })).toBeInTheDocument(); + expect(screen.getByText(/built around the relationship/i)).toBeInTheDocument(); + expect(screen.getByText(/confidential where coordination needs it/i)).toBeInTheDocument(); + expect(screen.getAllByRole("link", { name: /install wallet/i }).length).toBeGreaterThan(0); }); it("renders the observed Root Accord and distinguishes current exposure from restored capacity", () => { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 96fcd17..7090a25 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,6 +4,7 @@ import { ArrowRightIcon, ArrowTopRightOnSquareIcon, ArrowUpTrayIcon, + ArrowsRightLeftIcon, BanknotesIcon, Bars3Icon, BuildingLibraryIcon, @@ -14,6 +15,7 @@ import { DocumentTextIcon, EyeSlashIcon, FunnelIcon, + GlobeAltIcon, InformationCircleIcon, LockClosedIcon, MagnifyingGlassIcon, @@ -21,6 +23,7 @@ import { ServerStackIcon, ShareIcon, ShieldCheckIcon, + SparklesIcon, Squares2X2Icon, UserGroupIcon, WalletIcon, @@ -28,6 +31,7 @@ import { } from "@heroicons/react/24/outline"; import { CheckCircleIcon as CheckCircleSolid } from "@heroicons/react/20/solid"; import { + type CSSProperties, type ComponentType, type PropsWithChildren, type SVGProps, @@ -43,6 +47,7 @@ import { useLocation, useParams, } from "react-router-dom"; +import { WalletControl } from "./components/WalletControl"; import { activity, children, @@ -97,37 +102,64 @@ function PublicHeader() { - Open facility +
+ + Open app +
); } function FacilityArtifact() { + const [tilt, setTilt] = useState({ x: 0, y: 0 }); + const style = { + "--pointer-x": `${tilt.x}deg`, + "--pointer-y": `${tilt.y}deg`, + } as CSSProperties; + return ( -
+
{ + if (event.pointerType === "touch") return; + const bounds = event.currentTarget.getBoundingClientRect(); + setTilt({ + x: ((event.clientY - bounds.top) / bounds.height - 0.5) * -5, + y: ((event.clientX - bounds.left) / bounds.width - 0.5) * 7, + }); + }} + onPointerLeave={() => setTilt({ x: 0, y: 0 })} + style={style} + > +
+ + + Open facility +

y{+G@3`ltHvIN(P5vnVIdqSLs$q_6111$~v=) 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