{"id":51890223,"url":"https://github.com/peacefulstudio/canton-localnet","last_synced_at":"2026-07-26T03:01:55.257Z","repository":{"id":363889981,"uuid":"1262149352","full_name":"peacefulstudio/canton-localnet","owner":"peacefulstudio","description":"Reusable Canton Network LocalNet — Docker Compose stack, Go CLI, Go \u0026 C# test fixtures, and Terraform (vendors Splice).","archived":false,"fork":false,"pushed_at":"2026-07-21T15:04:31.000Z","size":653,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"dev","last_synced_at":"2026-07-21T17:03:57.828Z","etag":null,"topics":["canton","daml","docker-compose","golang","localnet","splice","terraform","testing"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/peacefulstudio.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-07T16:29:42.000Z","updated_at":"2026-07-21T15:00:56.000Z","dependencies_parsed_at":null,"dependency_job_id":"f9839804-2bf4-4cdd-8ab1-402e9afd8103","html_url":"https://github.com/peacefulstudio/canton-localnet","commit_stats":null,"previous_names":["peacefulstudio/canton-localnet"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/peacefulstudio/canton-localnet","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peacefulstudio%2Fcanton-localnet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peacefulstudio%2Fcanton-localnet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peacefulstudio%2Fcanton-localnet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peacefulstudio%2Fcanton-localnet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/peacefulstudio","download_url":"https://codeload.github.com/peacefulstudio/canton-localnet/tar.gz/refs/heads/dev","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/peacefulstudio%2Fcanton-localnet/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35898942,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-26T02:00:06.503Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["canton","daml","docker-compose","golang","localnet","splice","terraform","testing"],"created_at":"2026-07-26T03:01:55.167Z","updated_at":"2026-07-26T03:01:55.246Z","avatar_url":"https://github.com/peacefulstudio.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# canton-localnet\n\n[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n\nA shared, reusable, flexibly-topological Canton LocalNet that Peaceful Studio's\nrepositories build their integration tests on — compose + xUnit/Go fixtures +\nCLI + AWS terraform.\n\n## Capabilities\n\n| Capability | What ships today |\n|---|---|\n| Declarative config | Single-file `canton-localnet.yaml` (topology, slots, parties, auth) |\n| CLI lifecycle | `up` / `down` / `wait-ready` / `auth token` / `info` / `vm` |\n| Flexible topology | 1 SV + N validators across 5 named slots; SV-only → all-five |\n| Programmatic fixtures | C# `LocalnetFixture` + Go `fixture`: query + mutate a live ledger |\n| Observability \u0026 PQS | Grafana/Prometheus/Loki/Tempo + per-slot Participant Query Store |\n| Multi-synchronizer | Second synchronizer profile for cross-domain scenarios |\n| Infra | AWS spot + part-time Hetzner Terraform, scheduled up/down |\n| Release artifacts | NuGet + Go module + CLI binaries + OCI compose artifact, in lockstep |\n\n## Use cases\n\n- Local multi-participant app development against a real Canton ledger.\n- Integration and end-to-end testing.\n- CI/CD pipelines.\n- Privacy-boundary / witness testing (the reserved `d-validator-1` slot).\n- Multi-synchronizer / cross-domain scenarios.\n- Demos and runtime samples.\n\nIt underpins our own repositories' integration suites — see the\n[integration-testing guide](docs/public/integration-testing.md) — and the\n[topologies guide](docs/public/topologies.md) shows how far you can push the\ntopology.\n\n## Quickstart\n\nBoot a complete Canton LocalNet (Splice 0.6.13) on your machine:\n\n```bash\nmake up           # docker compose up -d, OAuth2 mode by default\nmake wait-ready   # poll the JSON Ledger API until participant accepts requests\nmake down         # stop the stack and remove containers\n```\n\nJSON Ledger API endpoints once ready (see `compose/modules/localnet/env/common.env`):\n\n| Slot             | JSON Ledger API |\n|------------------|-----------------|\n| `sv-validator-1` | 10975           |\n| `a-validator-1`  | 11975           |\n| `b-validator-1`  | 12975           |\n| `c-validator-1`  | 13975           |\n| `d-validator-1`  | 14975           |\n\nSlot ports follow the 5-digit two-digit-prefix scheme (`\u003cprefix\u003e\u003csuffix\u003e`):\neach slot's participant ledger / admin / JSON / Splice validator admin ports\nshare the same two-digit prefix as the JSON Ledger API port shown above.\n\nOptional layers via Make flags (shortcuts — the canonical config layer is\n`canton-localnet.yaml`, see **Configuration** below):\n\n```bash\nmake up PQS=true            # opt in to PQS a-validator-1 profile\nmake up OBS=true            # add Grafana (http://localhost:3030) + Prometheus / Loki / Tempo / cAdvisor\nmake up RES=false           # remove the default mem_limit / JVM heap caps\nmake up AUTH_MODE=secret    # shared-secret JWT (escape hatch; not CI-tested)\n```\n\nWhen iterating with observability on, `make stop-app` (and `make clean-app`)\nrestart the application stack while leaving Grafana / Prometheus / Loki / Tempo\nrunning so dashboards stay populated.\n\n### Remote VM (AWS)\n\nFor consumers that prefer running LocalNet on a shared EC2 instance, the\n`vm` subcommand wraps the `terraform/` stack and an ssh tunnel:\n\n```bash\ncanton-localnet vm provision        # terraform apply, prints public IP + ssh command\ncanton-localnet vm tunnel           # ssh -L 11901, 7575, 8082 to the VM (Ctrl-C to close)\ncanton-localnet vm destroy --yes    # terraform destroy (interactive prompt without --yes)\n```\n\nThe tunnel forwards the same port set the legacy `tunnel.sh` scripts in\ndownstream consumers and `terraform-provider-canton` open: `11901` (a-validator-1\nparticipant gRPC ledger API), `7575` (legacy in-container Splice JSON\nLedger API), and `8082` (Keycloak). `vm provision` is idempotent —\nre-running on an already-applied state is a no-op refresh.\n\n\u003e The 5-digit port renumbering applies to the host-exposed\n\u003e port set on the local stack (`10975`/`11975`/… for JSON Ledger API).\n\u003e The `vm tunnel` default port set is unchanged for backwards\n\u003e compatibility with the legacy tunnel scripts and is documented as\n\u003e such in [`cli/internal/tunnel`](cli/internal/tunnel/tunnel.go). If\n\u003e you connect to JSON Ledger API on the VM, forward the corresponding\n\u003e host port (`11975` for `a-validator-1`, etc.) explicitly.\n\nPrerequisites: Docker ≥ 27, Docker Compose ≥ 2.27. The compose stack is\nvendored into `compose/modules/` from `hyperledger-labs/splice` at the SHA\npinned in `compose/splice.sha`. Re-vendor with `make vendor`.\n\n### Multi-synchronizer profile\n\nBring up a second synchronizer (`app-synchronizer`) that the `a`/`b`/`d`\nvalidators connect to, for cross-synchronizer tests:\n\n```bash\ncanton-localnet up --multi-sync\ncanton-localnet wait-ready --synchronizers 2   # blocks until a-validator-1 sees both\n# or, via make:\nMULTI_SYNC=true make up\n```\n\nDiscover the second synchronizer's id from a test (C#):\n\n```csharp\nvar party = await fixture.AllocatePartyAsync(\"lapi\");\nvar appSyncId = await fixture.GetAppSynchronizerIdAsync(party.PartyId);\n// pin a submission: WithSynchronizerId(new SynchronizerId(appSyncId))\n```\n\n`multi-sync` is local/CI only — it is not enabled on the shared VM.\n\n## Driving a slot from a script\n\nDownstream apps that allocate parties, upload DARs, or otherwise drive\na slot's JSON Ledger API can stop hardcoding realm names, client\nsecrets, and the `\u003cprefix\u003e975` port scheme by going through two CLI\nprimitives:\n\n```bash\n# Mint a participant-admin bearer for the slot.\nTOKEN=$(canton-localnet auth token --slot a)\n\n# Get the slot's endpoints + live participant id as JSON.\ncanton-localnet info --slot a --json\n```\n\n`auth token` does an OAuth2 `client_credentials` exchange against the\nslot's Keycloak realm for a/b/c/d, and mints a self-signed HS256 JWT\nfor sv-validator-1 (which doesn't run Keycloak). Resolution layers, from\nhighest to lowest priority:\n\n1. `CANTON_LOCALNET_\u003cSLOT\u003e_CLIENT_ID` / `_CLIENT_SECRET` env vars\n2. `canton-localnet.yaml` `validators.\u003cslot\u003e.auth.clientId` / `clientSecret` (the same fields `up` reads)\n3. `compose/modules/keycloak/env/\u003cslot\u003e/on/oauth2.env` — so secret rotations in this repo land automatically\n4. Built-in default\n\nFor sv-validator-1, the HS256 signing secret defaults to the LocalNet\nshared `\"unsafe\"` string; override via\n`CANTON_LOCALNET_SV_VALIDATOR_1_HS256_SECRET` if you have customised the\nSV participant's auth config. The CLI does not validate the sv secret\nlocally — a wrong secret surfaces as a downstream 401 from the JSON\nLedger API. `validators.sv-validator-1.auth.clientId` / `clientSecret`\nin `canton-localnet.yaml` are silently ignored because the HS256 path\ndoesn't have a client id.\n\n`info --json` returns:\n\n```json\n{\n  \"slot\": \"a-validator-1\",\n  \"json_api\": \"http://localhost:11975\",\n  \"ledger_grpc\": \"localhost:11901\",\n  \"admin_grpc\": \"localhost:11902\",\n  \"validator_admin\": \"localhost:11903\",\n  \"realm\": \"AValidator1\",\n  \"token_url_host\": \"http://localhost:8082/realms/AValidator1/protocol/openid-connect/token\",\n  \"token_url_internal\": \"http://nginx-keycloak:8082/realms/AValidator1/protocol/openid-connect/token\",\n  \"audience\": \"https://canton.network.global\",\n  \"auth_kind\": \"oauth2\",\n  \"party_hint\": \"a-validator-1\",\n  \"participant_id\": \"a-validator-1::1220...\",\n  \"participant_namespace\": \"1220...\",\n  \"validator_primary_party\": \"a-validator-1::1220...\"\n}\n```\n\nPass `--offline` to skip the live participant-id lookup when you only\nneed the static endpoint mapping (e.g. before the stack is up). Short\nslot names (`a`, `b`, `c`, `d`, `sv`) and canonical names\n(`a-validator-1`, …) are both accepted.\n\n## Configuration\n\nThe CLI reads an optional `canton-localnet.yaml` from the working\ndirectory or any ancestor (walk-up discovery, matching `git` / `docker\ncompose` / `kubectl`), overridable with `--config \u003cpath\u003e`. With no\nfile found anywhere, the CLI uses built-in defaults (all five\nvalidator slots enabled, observability + PQS on, party hints equal to\nslot names). Partial configs merge over the defaults.\n\n```yaml\nschemaVersion: preview-1\nmodules:\n  obs: true\n  pqs: true\nvalidators:\n  a-validator-1:\n    partyHint: featuredapp-validator-1\n    auth: { clientId: a-validator-1-validator, clientSecret: ${FEATUREDAPP_VALIDATOR_SECRET} }\n  c-validator-1: { enabled: false }\n  d-validator-1: { enabled: false }\n```\n\nThe schema is **preview / unstable** until compose codegen lands —\nsee [`docs/public/canton-localnet-yaml-schema.md`](docs/public/canton-localnet-yaml-schema.md)\nfor the full reference.\n\n## Onboarding clients\n\nEach user-facing validator realm ships a confidential OAuth client whose\nservice account holds just enough `realm-management` roles\n(`manage-users`, `view-users`, `query-users`) to manage users in that\nrealm only. Downstream apps that need to create users (sign-up flows,\nintegration test seeders) should use **this** client instead of the\nmaster realm `admin/admin` admin — master-realm admin is a footgun (one\nbug wipes the wrong realm) and breaks over plain HTTP via the\n`nginx-keycloak` proxy because Keycloak's master realm defaults to\n`sslRequired=external`.\n\n| Realm          | Client ID                  | Dev secret env var                              |\n|----------------|----------------------------|-------------------------------------------------|\n| `AValidator1`  | `a-validator-1-onboarding` | `AUTH_A_VALIDATOR_1_ONBOARDING_CLIENT_SECRET`   |\n| `BValidator1`  | `b-validator-1-onboarding` | `AUTH_B_VALIDATOR_1_ONBOARDING_CLIENT_SECRET`   |\n| `CValidator1`  | `c-validator-1-onboarding` | `AUTH_C_VALIDATOR_1_ONBOARDING_CLIENT_SECRET`   |\n| `DValidator1`  | `d-validator-1-onboarding` | `AUTH_D_VALIDATOR_1_ONBOARDING_CLIENT_SECRET`   |\n\nSecrets are committed in `compose/modules/keycloak/env/{slot}/on/oauth2.env`\nand re-surfaced in `compose/modules/localnet/env/{slot}-auth-on.env`. They\nare **dev-only** — for production deployments, rotate them.\n\nQuick check: mint a token, create a user, and verify cross-realm access\nis denied. Substitute the slot's realm + client + secret env var.\n\n```bash\nTOKEN=$(curl -sf -X POST http://localhost:8082/realms/AValidator1/protocol/openid-connect/token \\\n  -H 'Content-Type: application/x-www-form-urlencoded' \\\n  -d 'client_id=a-validator-1-onboarding' \\\n  -d \"client_secret=${AUTH_A_VALIDATOR_1_ONBOARDING_CLIENT_SECRET}\" \\\n  -d 'grant_type=client_credentials' \\\n  -d 'scope=openid' | jq -r .access_token)\n\ncurl -sf -X POST http://localhost:8082/admin/realms/AValidator1/users \\\n  -H \"Authorization: Bearer $TOKEN\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"username\":\"alice\",\"enabled\":true,\"credentials\":[{\"type\":\"password\",\"value\":\"abc123\",\"temporary\":false}]}' \\\n  -w '%{http_code}\\n' -o /dev/null\n# expect: 201\n\ncurl -s -X GET http://localhost:8082/admin/realms/BValidator1/users \\\n  -H \"Authorization: Bearer $TOKEN\" -w '%{http_code}\\n' -o /dev/null\n# expect: 403 (token is scoped to AValidator1 only)\n```\n\n## Project stewardship\n\n`canton-localnet` is currently developed and maintained by **Peaceful Studio\nOÜ** (Estonia, VAT EE102232996). The project is licensed under Apache-2.0\nwith the explicit intent of community ownership: if and when adoption\nwarrants neutral governance, Peaceful Studio commits to transferring this\nrepository to a community-led organisation under the same license terms.\nContributions welcome from anywhere in the Canton, Daml, and Splice\necosystem; no CLA required.\n\n## Contributing\n\nContributions are welcome from anyone in the Canton, Daml, and Splice\ncommunity. See [CONTRIBUTING.md](CONTRIBUTING.md) for the dev setup,\nthe red-green TDD requirement, and the branch model. The per-PR\nchecklist itself lives in the PR template and is filled in when you\nopen a PR. By participating you agree to abide by the\n[Code of Conduct](CODE_OF_CONDUCT.md).\n\nFor security-sensitive bugs, please follow [SECURITY.md](SECURITY.md) instead\nof opening a public issue.\n\n## License\n\nApache-2.0. © 2026 Peaceful Studio OÜ. See [LICENSE](LICENSE) and\n[NOTICE](NOTICE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeacefulstudio%2Fcanton-localnet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpeacefulstudio%2Fcanton-localnet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpeacefulstudio%2Fcanton-localnet/lists"}