8000 GitHub - zksecurity/mina-guard: MultiSig Wallet for the Mina Network · GitHub
[go: up one dir, main page]

Skip to content

About

MultiSig Wallet for the Mina Network

Resources

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

MinaGuard Monorepo

BETA: MinaGuard is in beta. Bugs could cause loss of funds. Use only amounts you can afford to lose. Beta describes the product's maturity, not the network: this applies on both Testnet and Mainnet. Mainnet funds are real. Read User guide → Beta product in the app (/guide#beta) before using a vault. You can dismiss the banner for the current tab; the warning appears again when you open the proposal-creation page or the SubVault proposal wizard.

Online creation checks whether the exact proposal already exists before requesting a signature, and again when assessing a stale transaction. The recovery panel offers View proposal so another owner can approve the existing proposal. Propose and approve recheck their indexed witnesses against fresh vault state after the proposal signature, before building the transaction.

Initial verified-store mismatches show “Vault data isn’t up to date” with Retry / Cancel. Retry restarts online preparation or offline request export from current state and indexed stores; it never bypasses root validation or retries automatically. Network and unrelated errors are not classified as store mismatches.

Prepared transactions are checked against current on-chain state before broadcast (before wallet handoff for Auro). The UI shows “Checking latest vault state…” before Auro handoff, then “Waiting for wallet confirmation…”. Stale transactions stop with an explicit recovery action. See transaction preflight.

MinaGuard is a multisig wallet zkApp for Mina built with o1js, plus a Next.js UI and an Express indexer API.

The user guide (/guide) explains wallet connection, on-chain proposal stages, the nine-recipient contract limit, parent/SubVault funding and reclaim, offline batch review, and the absence of incoming-payment alerts. It includes GitHub source and issue links. Contract compilation failures now reach the operation banner with their cause and can be retried.

Packages

  • contracts/ - MinaGuard smart contract, stores, and tests.
  • backend/ - Express + Prisma + PostgreSQL read API and chain indexer.
  • ui/ - Next.js app with Auro wallet integration and on-chain actions.

Key Features

  • Propose -> approve -> execute lifecycle using proposal hash keyed approvals.
  • Optional proposal deadlines apply to proposing, approving, and executing. Zero means no expiry; other values must fit in 32 bits, and inclusion at the deadline is allowed.
  • Transfer, add/remove owner, threshold change, and delegate execution support.
  • For transfers to new recipient accounts, the executor pays the account-creation cost and transaction fee; see the accepted risk.
  • Indexed read API for contracts, owners, proposals, approvals, and raw events.
  • Proposal submission progress uses one card for network confirmation and the Vault update, with each pending transaction shown once. Network inclusion alone does not unlock further submissions.
  • Deploy + setup UI flow with a creation-time local deploy-key download. The wizard presents the risks of saving or not saving the key and requires acknowledgment; detailed recovery guidance can be expanded. Existing vaults keep this guidance in Settings → Security. The dashboard shows a short attention banner when the node reports an older stored transaction version. The browser does not persist the key; the creator may retain the downloaded copy. A Mina transaction-version upgrade can make that key sufficient to replace the vault verification key without owner approval. If a fork breaks old proofs and no backup exists, funds may become inaccessible. See the accepted risk.
  • Vault creation displays the network fixed by the deployed UI build. Before deploying a Vault or proposing a SubVault, the UI checks that the configured Mina node reports the matching network. Mainnet creation requires explicit HTTPS Mina and archive endpoints and a mainnet verification-key hash.
  • For an older-version vault whose VK still matches this release, Settings → Security guides owners to submit an ordinary proved action and check that Mina refreshed the stored version. This works only if Mina accepts the existing proof. Any fee payer can instead submit a no-op vault update that ends the deploy-key fallback, so a proved action is the safer compatibility check. If the installed VK differs from the reviewed replacement, Settings → Security can export a version 3 VK migration request. The MinaGuard offline CLI compiles and checks the replacement VK, signs with the saved deploy key and an offline fee payer key, and returns a transaction for the UI to validate and broadcast. If proofs break, that migration must land before any other vault update, including a third-party no-op. A future fork still needs a reviewed compatible release and per-network VK hash before migration is attempted.
  • Child reservations validate governance bounds and initialized root-parent state; successful child setup clears the consumed reservation hash.
  • Child allocations require initialized recipients bound to the sending parent. Complete child setup before funding; ordinary transfers and external deposits to uninitialized children remain unrecoverable until setup succeeds. See safe child funding.

o1js dependency

Packages that import o1js pin stable o1js@3.0.0 directly for proving, decoding, or tests. MinaGuard uses 13 on-chain state slots, so it requires the Mesa 32-slot protocol. 3.0.0-mesa.final was pinned before stable 3.0.0 was published; the stable package compiles the same testnet and mainnet verification keys for this circuit. The UI, backend lightnet helper, and offline CLI pin mina-signer@4.1.0 for signing and transaction commitments. Signing and proving compatibility is covered by the cross-network tests; the signer is a separate package from the prover.

When changing o1js, compile and compare both network verification-key hashes with the current release, run a genuine-proof test and signing checks, then publish the generated VK manifest for the new build. If either hash changes, update deployment and offline CLI fingerprints together before users sign or broadcast transactions against the new circuit. Run the complete offline CLI end-to-end suite with real proofs before merging a prover or signer dependency change; a SKIP_PROOFS=1 pass does not satisfy this gate.

Development

First-time setup

bun install

# Build contracts (required by backend and UI)
bun run --filter contracts build

# Set up the backend environment
cp backend/.env.example backend/.env

# Generate Prisma client
cd backend && bunx prisma generate && cd ..

Lightnet helpers

dev-helpers/lightnet-up.sh starts a standalone Mesa lightnet on the host (ports 8080 GraphQL, 8282 archive, 8181 account manager, 5432 Postgres). It first frees those host ports by stopping any colliding containers (zkao-postgres-dev, local-lightnet-1) and records what it stopped:

./dev-helpers/lightnet-up.sh

dev-helpers/lightnet-down.sh stops lightnet and restarts whatever the up script paused, so you're back where you started:

./dev-helpers/lightnet-down.sh

Once lightnet is running, accounts must have funds before they can submit transactions. Two ways:

  • From the UI — once a wallet is connected on a testnet network, the header shows a "Fund" button that calls the backend's /api/fund route (which in turn drips from the lightnet account manager).
  • From the CLI — add public keys to dev-helpers/.env and run:
    cd dev-helpers && bun run cli.ts lightnet-fund

NOTE: To test with a Ledger device, its public key (corresponding to the account index used) must be funded similarly. Only the public key is needed.

Lightnet resets: lightnet sometimes stops or wipes its chain state without warning. When that happens the backend's indexed view (contracts, proposals, etc.) no longer matches the live chain, so wipe the DB to start clean:

cd backend && bunx prisma db push --force-reset --skip-generate

Then restart lightnet (./dev-helpers/lightnet-up.sh) and the backend.

Running

To run in browser:

# Run UI (from ui/ directory)
cd ui && bun run dev

# Run backend API/indexer (from backend/ directory)
cd backend && bun run dev

# Run contract tests (choose the circuit domain explicitly)
MINA_NETWORK_DOMAIN=testnet bun run --filter contracts test

To run the Electron app:

# Build UI (from ui/ directory; use the target network's domain)
cd ui && NEXT_PUBLIC_MINA_NETWORK=testnet bun run build

# Run backend API/indexer (from backend/ directory)
cd backend && bun run dev

# Run the Electron desktop app
cd desktop && bun run dev

E2E Testing

Full end-to-end tests live in e2e/ and exercise the deploy → propose → approve → execute lifecycle against a real Mina network. See e2e/README.md for setup details.

# Quick start with local lightnet (default)
bun run test:e2e

# Against Mina devnet (requires funded accounts in e2e/.env.devnet)
NETWORK=devnet bun run test:e2e

# Fast UI bundle-export and desktop network checks (also run in CI)
bun run --filter ui test
bun run --filter desktop test

Build

bun run --filter contracts build
bun run --filter backend build
NEXT_PUBLIC_MINA_NETWORK=testnet bun run --filter ui build

CI computes a fingerprint of the tracked circuit inputs. It compiles testnet and mainnet in parallel when their cached hashes are missing, then publishes a minaguard-vk-manifest artifact for the exact source commit. It contains contracts/.vk-hash with testnet, mainnet, and devnet entries; devnet uses the testnet circuit. Release and deploy jobs verify the artifact's commit before using its network hash for the backend, UI, or desktop bundle. Local builds do not need to compile the circuit just to update a committed hash file. The hash compiler selects the same o1js network as the circuit domain, as the browser and offline signer do; mainnet hashes generated without this setting must be regenerated. The testnet runner proves parent-state paths and, when present, proposal-signing paths in separate steps so each has its own timeout. The mainnet runner proves child reservation and setup, plus the proposal expiry lifecycle. The offline CLI propose regression allows 15 minutes for a cold compile plus proof, and its platform CI step allows 25 minutes for the selected tests. These proof tests still run on every PR check. A VK cache miss recompiles the circuit, including after cache expiry or a merge to a branch that cannot access the PR's cache.

Mainnet image preparation

A reviewed PR into production containing the current main code selects the release. Once merged, .github/workflows/mainnet-image-build.yml builds that exact production commit on GitHub-hosted runners and publishes the frontend and backend images to GHCR. The workflow signs the image digests and frontend content baseline. It runs a separate publishing job to preserve the signed release evidence in a GitHub Release; that job can write repository release metadata. It runs only when MAINNET_BUILD_ENABLED=true; the three public MAINNET_* URL variables must also be configured. The separate ops workflow verifies the images and deploys them only when automatic rollout has been enabled after first-launch acceptance.

deploy/mainnet-content-integrity.py compares the public site with the approved bytes. The GitHub-hosted watch workflow can run it every 15 minutes after the ops App actor ID, watch gate, and alert webhook are configured. These checks do not deploy the app or start monitoring by themselves; see the deployment audit guide for setup and limits. The watcher avoids naming itself in its User-Agent, but selective delivery to particular visitors can still evade its checks.

These settings have di 8000 stinct jobs: the Mina and archive URLs choose which chain services the app talks to; the network name selects the wallet network; the contract domain is part of the proposal hash and circuit; and the VK hash checks the compiled contract. Keep all four aligned when configuring the AWS runtime. The browser checks the Mina node's reported network again before Vault creation. See the deployment audit guide for the release boundary and remaining AWS setup.

Local full stack with Docker

You can run the full stack (lightnet, PostgreSQL, backend, frontend, block explorer and a Caddy reverse proxy) locally:

./preview-env/local-preview.sh up 1

Access at https://localhost:10001/preview/1/. In Auro Wallet, set the network URL to https://localhost:10001/preview/1/graphql.

Caddy serves this over HTTPS with a self-signed cert (tls internal) and sets the COOP/COEP headers o1js needs; accept the cert warning on first visit. The CA persists in the caddy-local-data volume so the cert stays stable across restarts.

The helper builds backend, frontend, and explorer sequentially before starting the stack. This avoids the RAM spike from docker compose up -d --build, which can try to build all three images at once.

The first uncached frontend build can take a few minutes because Next.js has to compile the heavy o1js worker bundle. local-preview.sh now prints a periodic heartbeat during long builds so this does not look like a freeze.

Frontend changes do not hot-reload in this Docker preview flow. The container runs a built next start app, so after changing files under ui/, rebuild the frontend image and restart only that service:

PR_NUMBER=1 PREVIEW_PORT=10001 docker compose \
  -f preview-env/docker-compose.preview.yml \
  -f preview-env/docker-compose.local.yml \
  -p local \
  build frontend

PR_NUMBER=1 PREVIEW_PORT=10001 docker compose \
  -f preview-env/docker-compose.preview.yml \
  -f preview-env/docker-compose.local.yml \
  -p local \
  up -d --no-deps frontend

build frontend picks up your latest UI code. up -d --no-deps frontend recreates just the frontend container without restarting backend, lightnet, explorer, or Caddy.

To seed coherent test data, prefer the real on-chain fixture helper over direct DB inserts:

MINA_NETWORK_DOMAIN=testnet bun run dev-helpers/cli.ts lightnet-fixture --main-address <YOUR_WALLET_ADDRESS>

By default this uses the quick-test minimal scenario: 2 vaults, 2 executed proposals per vault, and both vaults ending with your wallet as the sole owner at threshold 1. For broader coverage, you can also use --scenario full.

That command acquires a funded lightnet deployer, generates helper signers, deploys real MinaGuard contracts that include your wallet address as an owner, submits real propose / approve / execute transactions with proofs disabled, and waits for the stack's indexer to ingest them. The result is fixture data that stays consistent across chain, backend, and UI.

# Logs
docker compose -p local logs -f            # all services
docker compose -p local logs -f frontend   # frontend only
docker compose -p local logs -f backend    # backend/indexer
docker compose -p local logs -f lightnet   # mina node + archive

# Tear down
./preview-env/local-preview.sh down 1

Develop on the remote server via SSH tunnel

When you want the heavy services (lightnet, backend, frontend dev server) to run on the server but iterate from your laptop's browser + Auro wallet.

On the server, follow First-time setup and Running so lightnet, backend, and the UI dev server are listening on the default localhost:* ports. backend/.env and ui/.env.local need no changes.

On the laptop, open one SSH tunnel that forwards every port the bundle references:

ssh -L 3000:localhost:3000 \
    -L 3001:localhost:3001 \
    -L 8080:localhost:8080 \
    -L 8282:localhost:8282 \
    user@server

Then open http://localhost:3000 in the browser. The frontend bundle has localhost:* baked in for the backend / Mina / archive URLs; both the server-side dev server and the laptop-side browser resolve localhost to themselves, so the URLs work on both ends as long as the tunnel is up.

Notes:

  • Add a custom network in Auro Wallet pointing at http://localhost:8080/graphql and switch to it before connecting.
  • Tunnel dies → the page errors. Run the SSH command inside tmux/screen if you want it sticky.

Gotchas

  • SharedArrayBuffer: o1js WASM requires crossOriginIsolated, which needs COOP + COEP headers over HTTPS. Do not add Cross-Origin-Resource-Policy: same-origin — it blocks o1js blob URL sub-workers.
  • Bun workspaces: keep the exact o1js and mina-signer pins in bun.lock when building UI, backend, and offline CLI artifacts.
  • Minification disabled: SWC/terser mangle BigInt ops used by o1js.

Lightnet (working image before update 2026/04/13)

# Pull previous working lightnet (non-MESA) docker image
docker pull 'o1labs/mina-local-network@sha256:746190ff2f556f252b7f50215ae60d4a5e786c8adc16f27986e3e35ce6105949' 

# Verify it was pulled
docker inspect 'o1labs/mina-local-network@sha256:746190ff2f556f252b7f50215ae60d4a5e786c8adc16f27986e3e35ce6105949' --format '{{.Id}} {{.RepoTags}}'

# Tag it as a distinct image
docker tag 'o1labs/mina-local-network@sha256:746190ff2f556f252b7f50215ae60d4a5e786c8adc16f27986e3e35ce6105949' o1labs/mina-local-network:known-good

# Add a second tag, the one zk will look for
docker tag 'o1labs/mina-local-network@sha256:746190ff2f556f252b7f50215ae60d4a5e786c8adc16f27986e3e35ce6105949' o1labs/mina-local-network:compatible-latest-lightnet

# Confirm no stale state
zk lightnet stop --clean-up

# Start lightnet WITHOUT pulling latest
zk lightnet start --pull=false

Incremental signing state and offline compatibility

The signing worker saves public owner/approval/nullifier checkpoints locally and fetches only newer event blocks on later operations using the existing backend block filters and offset pagination. It checks the resulting roots against the Mina node and falls back to one full replay on mismatch. Initial sync still needs complete history; cold restoration rehashes saved leaves. Clearing browser storage loses only this optimization, not signing keys. Ranges exceeding the existing pagination cap fail closed; cursor pagination is tracked in issue #143.

Offline requests exported by this UI use version 3 with a complete public store snapshot. Proposals use application-tagged hashes, distinct propose/approve signing messages, and length-prefixed memo commitments (including empty memos). Owner-chain links, vote-nullifier keys, and child configuration hashes also have separate tags. Use matching CLI/UI/backend/desktop builds and the current per-network VKs. Owner requests include a bounded, mempool-derived fee in nanomina; the offline CLI shows that exact fee before signing it. A successful query confirming an empty mempool uses 0.01 MINA. A failed query, missing pool, or pool with no usable fee samples falls back to the previous fixed 0.1 MINA fee. This fallback is a conservative policy, not evidence of congestion or an inclusion guarantee. VK migration keeps its fixed 0.1 MINA fee. Auro keeps its existing fixed fee and online Ledger uses the same bounded estimate as offline export. On owner response upload, the UI shows the signed command's fee and requires a separate broadcast click. It rejects fees outside 0.01–0.1 MINA or different from the latest same-action request exported in the page session. On a proposal's page the tab keeps approve and execute exports across reloads until they are broadcast or the user clicks Start over; with no export, compare the displayed fee with the saved request and CLI summary. Version 1 and 2 requests must be re-exported with a matching UI and CLI; owner signed responses stay at version 1 and VK migration responses stay at version 2. This fee change does not alter proposal hashes, circuits, or verification keys and does not require new vaults or proposals. See the offline audit guide for migration and trust boundaries. Desktop packages the same worker and exporter.

About

MultiSig Wallet for the Mina Network

Resources

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

0