This guide walks you through signing Mina transactions using FROST threshold signatures and broadcasting them to the network.
This repository has not undergone a security audit. It may contain bugs and security vulnerabilities. Use it at your own risk. The authors and contributors take no responsibility for any loss or damage resulting from the use of this code.
| Task | Command |
|---|---|
| Initialize config | mina-frost-client init -c <CONFIG_PATH> |
| Export contact | mina-frost-client export -n <NAME> -c <CONFIG_PATH> |
| Import contact | mina-frost-client import <CONTACT_STRING> -c <CONFIG_PATH> |
| Start DKG (coordinator) | mina-frost-client dkg -d <DESC> -s <SERVER_URL> -t <THRESHOLD> -S <PUBKEYS> -c <CONFIG_PATH> |
| Join DKG (participant) | mina-frost-client dkg -d <DESC> -s <SERVER_URL> -t <THRESHOLD> -c <CONFIG_PATH> |
| List groups | mina-frost-client groups -c <CONFIG_PATH> |
| Coordinate signing | mina-frost-client coordinator -g <GROUP_PUBKEY> -S <SIGNER_PUBKEYS> -m <TX_FILE> -o <SIG_OUT> -n <NETWORK> -c <CONFIG_PATH> |
| Join signing | mina-frost-client participant -g <GROUP_PUBKEY> -c <CONFIG_PATH> |
| Build GraphQL | mina-frost-client graphql-build -i <INPUT_JSON> -o <OUTPUT_FILE> |
| Broadcast | mina-frost-client graphql-broadcast -g <GRAPHQL_FILE> -e <ENDPOINT_URL> |
| Software | Version | Install Command |
|---|---|---|
| Rust | 1.87.0+ | See the Rust book |
| mina-frost-client | latest | cargo install --git https://github.com/Raspberry-Devs/mina-multi-sig.git --locked mina-frost-client |
| frostd | latest | cargo install --git https://github.com/ZcashFoundation/frost-zcash-demo.git --locked frostd |
| mkcert | latest | apt install mkcert |
# Install mina-frost-client
cargo install --git https://github.com/Raspberry-Devs/mina-multi-sig.git --locked mina-frost-client
# Install frostd server
cargo install --git https://github.com/ZcashFoundation/frost-zcash-demo.git --locked frostdEach participant must initialize their own configuration file:
mina-frost-client init -c <CONFIG_PATH>Example:
mina-frost-client init -c ~/.frost/alice.tomlNote: The config file contains private FROST shares in clear text. Keep it safe and never share it with anyone.
Participants must exchange contact information before DKG or signing sessions.
Export your contact:
mina-frost-client export -n <YOUR_NAME> -c <CONFIG_PATH>Import another participant's contact:
mina-frost-client import <CONTACT_STRING> -c <CONFIG_PATH>List all contacts:
mina-frost-client contacts -c <CONFIG_PATH>The FROST tool does not support a way for participants to share contact information, instead we recommend using your favourite messenger application (e.g. WhatsApp, Signal, Telegram) to share contact strings.
The FROST protocol requires a coordination server (frostd) for participants to communicate.
mkcert localhost 127.0.0.1 ::1 2>/dev/nullThis creates localhost+2.pem and localhost+2-key.pem in the current directory, this will be installed to be trusted by your local system trust store. This is not recommended for production systems.
We recommend setting up a DNS name which points to your frostd instance and generating trusted certificates through a certificate authority such as Let's Encrypt. This is out of the scope of this document, and we recommend looking at Let's Encrypt's documentation.
frostd --tls-cert <CERT.pem> --tls-key <KEY.pem>The server runs on localhost:2744 by default.
For production systems, we recommend using a reverse proxy (such as nginx) with a domain name. Additionally, you need a central authority to sign your certificates as explained above.
For more information, see the frostd documentation.
Choose one of the following methods to generate group keys.
DKG distributes key generation across all participants, with no single party ever knowing the complete private key.
Coordinator (one participant initiates with -S flag listing other participants' public keys):
mina-frost-client dkg \
-c <CONFIG_PATH> \
-d "<GROUP_DESCRIPTION>" \
-s <SERVER_URL> \
-t <THRESHOLD> \
-S <PARTICIPANT_PUBKEY_1>,<PARTICIPANT_PUBKEY_2>Other Participants (join without -S flag):
mina-frost-client dkg \
-c <CONFIG_PATH> \
-d "<GROUP_DESCRIPTION>" \
-s <SERVER_URL> \
-t <THRESHOLD>Example (2-of-3 threshold):
# Alice (coordinator)
mina-frost-client dkg \
-c ~/.frost/alice.toml \
-d "Alice, Bob and Eve's group" \
-s localhost:2744 \
-t 2 \
-S <BOB_PUBKEY>,<EVE_PUBKEY>
# Bob (participant)
mina-frost-client dkg \
-c ~/.frost/bob.toml \
-d "Alice, Bob and Eve's group" \
-s localhost:2744 \
-t 2
# Eve (participant)
mina-frost-client dkg \
-c ~/.frost/eve.toml \
-d "Alice, Bob and Eve's group" \
-s localhost:2744 \
-t 2Important: All participants must use the same description and threshold values.
mina-frost-client trusted-dealer \
-c <CONFIG_PATH_1> -c <CONFIG_PATH_2> -c <CONFIG_PATH_3> \
-d "<GROUP_DESCRIPTION>" \
-N <NAME_1>,<NAME_2>,<NAME_3> \
-t <THRESHOLD>Example:
mina-frost-client trusted-dealer \
-c alice.toml -c bob.toml -c eve.toml \
-d "Test group" \
-N Alice,Bob,Eve \
-t 2After key generation, verify the group was created:
mina-frost-client groups -c <CONFIG_PATH>Note the group public key — you'll need it for signing sessions.
Before signing, you need an unsigned transaction in JSON format. This section shows how to generate transactions using o1js. See the O1JS workflow document for full project setup.
Create a new Node.js project for transaction generation:
mkdir mina-tx-generator && cd mina-tx-generator
npm init -y
npm install o1js
npm install -D typescript ts-node @types/nodetsconfig.json:
{
"compilerOptions": {
"target": "es2021",
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"outDir": "./dist"
},
"include": ["src/**/*"]
}Create an output directory:
mkdir -p tx-jsonZKApp transactions are the primary use case for FROST multi-sig on Mina. The key pattern is using Permissions.signature() and this.requireSignature() to enable signature-based authorization.
This contract stores on-chain state and requires a signature to modify it:
src/update_state.ts:
import {
SmartContract,
State,
state,
method,
Field,
Permissions,
Mina,
AccountUpdate,
PublicKey,
} from 'o1js';
import * as fs from 'fs';
import { UpdateFullCommitment } from './commit';
const MINA_TESTNET_URL = 'https://api.minascan.io/node/devnet/v1/graphql';
const FEE = 100_000_000; // 0.1 MINA
const DEPLOYER_KEY = '<PUBLIC_KEY>';
class StateContract extends SmartContract {
@state(Field) counter = State<Field>();
init() {
super.init();
this.counter.set(Field(0));
this.account.permissions.set({
...Permissions.default(),
editState: Permissions.signature(),
});
}
@method async incrementCounter() {
this.requireSignature();
const currentValue = this.counter.get();
this.counter.requireEquals(currentValue);
this.counter.set(currentValue.add(1));
}
}
async function generateUpdateStateTx() {
// 1. Setup Mina testnet
const network = Mina.Network(MINA_TESTNET_URL);
Mina.setActiveInstance(network);
// 2. Get accounts
const deployer = PublicKey.fromBase58(DEPLOYER_KEY);
const contractAccount = PublicKey.fromBase58(DEPLOYER_KEY);
const contract = new StateContract(contractAccount);
// 3. Compile (needed for verification key)
await StateContract.compile();
// 4. Create deploy transaction (unsigned)
const deployTx = await Mina.transaction(
{ sender: deployer, fee: FEE },
async () => {
await contract.deploy();
}
);
// 5. Create update transaction (unsigned)
const tx = await Mina.transaction(
{ sender: deployer, fee: FEE },
async () => {
await contract.incrementCounter();
}
);
UpdateFullCommitment(deployTx, tx);
fs.writeFileSync('./tx-json/deploy-state-contract.json', deployTx.toJSON());
console.log('Deploy transaction saved to ./tx-json/deploy-state-contract.json');
fs.writeFileSync('./tx-json/update-state-transaction.json', tx.toJSON());
console.log('Update transaction saved to ./tx-json/update-state-transaction.json');
}
generateUpdateStateTx();Run with:
npx ts-node src/update_state.tsThis contract allows updating its verification key (useful after Mina hard forks):
import {
SmartContract,
VerificationKey,
method,
Permissions,
Mina,
AccountUpdate,
PublicKey,
} from 'o1js';
import * as fs from 'fs';
import { UpdateFullCommitment } from './commit';
const MINA_TESTNET_URL = 'https://api.minascan.io/node/devnet/v1/graphql';
const FEE = 100_000_000; // 0.1 MINA
const DEPLOYER_KEY = '<PUBLIC_KEY>';
class UpdatableContract extends SmartContract {
init() {
super.init();
this.account.permissions.set({
...Permissions.default(),
setVerificationKey: Permissions.VerificationKey.signature(),
});
}
@method async updateVerificationKey(verificationKey: VerificationKey) {
this.requireSignature();
this.account.verificationKey.set(verificationKey);
}
}
class NewContract extends SmartContract {
@method async dummy() {
// Different contract = different verification key
}
}
async function generateUpdateVerificationKeyTx() {
// 1. Setup Mina testnet
const network = Mina.Network(MINA_TESTNET_URL);
Mina.setActiveInstance(network);
// 2. Get accounts
const deployer = PublicKey.fromBase58(DEPLOYER_KEY);
const contractAccount = PublicKey.fromBase58(DEPLOYER_KEY);
const contract = new UpdatableContract(contractAccount);
// 3. Compile original contract (needed for verification key)
await UpdatableContract.compile();
// 4. Create deploy transaction (unsigned)
const deployTx = await Mina.transaction(
{ sender: deployer, fee: FEE },
async () => {
await contract.deploy();
}
);
// 5. Compile new contract for different verification key
const { verificationKey: newVerificationKey } = await NewContract.compile();
// 6. Create update transaction (unsigned)
const tx = await Mina.transaction(
{ sender: deployer, fee: FEE },
async () => {
await contract.updateVerificationKey(newVerificationKey);
}
);
UpdateFullCommitment(deployTx, tx);
fs.writeFileSync('./tx-json/deploy-updatable-contract.json', deployTx.toJSON());
console.log('Deploy transaction saved to ./tx-json/deploy-updatable-contract.json');
fs.writeFileSync('./tx-json/update-verification-key-transaction.json', tx.toJSON());
console.log('Update transaction saved to ./tx-json/update-verification-key-transaction.json');
}
generateUpdateVerificationKeyTx();The generated JSON has this structure:
{
"feePayer": {
"body": {
"publicKey": "<FEE_PAYER_PUBLIC_KEY>",
"fee": "100000000",
"nonce": "0",
"validUntil": null
},
"authorization": ""
},
"accountUpdates": [
{
"body": {
"publicKey": "<CONTRACT_PUBLIC_KEY>",
"balanceChange": { "magnitude": "0", "sgn": "Positive" },
...
},
"authorization": {
"signature": null
}
}
],
"memo": ""
}FROST signatures are injected into the transaction at:
- Fee Payer —
feePayer.authorization(always required) - Account Updates —
accountUpdates[*].authorization.signature(whenrequireSignature()is called)
| Transaction Type | Signatures Required |
|---|---|
| Deploy | Fee payer + Contract account |
| State update | Fee payer + Contract account (if requireSignature() used) |
| Permissions update | Fee payer + Contract account (if requireSignature() used) |
| Verification key update | Fee payer + Contract account (if requireSignature() used) |
Note that the FROST multi-sig does not sign partial account updates (updates whose use_full_commitment field is false).
Query the current nonce for your FROST group account using GraphQL:
query {
accounts(publicKey: "<GROUP_PUBLIC_KEY_ADDR>") {
nonce
}
}Or use the Mina CLI:
mina account get --public-key <GROUP_PUBLIC_KEY_ADDR>When generating transactions, set the correct nonce:
const tx = await Mina.transaction(
{ sender: frostGroupPubKey, fee: 1e8, nonce: 5 }, // Set nonce explicitly
async () => {
// ... transaction logic
}
);For simple payment transactions without zkApp functionality, create a JSON file manually:
tx-json/payment.json:
{
"to": "<RECEIVER_ADDRESS>",
"from": "<GROUP_PUBLIC_KEY>",
"fee": "100000000",
"amount": "1000000000",
"nonce": "0",
"memo": "FROST payment",
"valid_until": "4294967295",
"tag": [false, false, false]
}| Field | Description |
|---|---|
to |
Recipient's Mina address (B62...) |
from |
Sender's Mina address (your FROST group public key) |
fee |
Transaction fee in nanomina (100000000 = 0.1 MINA) |
amount |
Transfer amount in nanomina (1000000000 = 1 MINA) |
nonce |
Sender's current account nonce |
memo |
Optional memo (max 32 characters) |
valid_until |
Slot number until which transaction is valid (max = 4294967295) |
tag |
Transaction type flags [false, false, false] for payments |
proofsEnabled: false— Transactions use signatures instead of zero-knowledge proofsPermissions.signature()— Allows authorization via signature instead of proofthis.requireSignature()— Method call that requires contract account signature- Compilation is still required — Generates verification key needed for deployment
- Fee payer must have funds — Ensure your FROST group account is funded before signing
The coordinator initiates the signing session with the transaction file:
mina-frost-client coordinator \
-c <CONFIG_PATH> \
-s <SERVER_URL> \
-g <GROUP_PUBLIC_KEY> \
-S <SIGNER_PUBKEY_1>,<SIGNER_PUBKEY_2> \
-m <TRANSACTION_FILE> \
-o <SIGNATURE_OUTPUT> \
-n <NETWORK>Parameters:
| Flag | Description |
|---|---|
-c |
Path to coordinator's config file |
-s |
Server URL (optional if stored in group config) |
-g |
Group public key (from groups command) |
-S |
Comma-separated public keys of signers to include |
-m |
Path to unsigned transaction JSON file |
-o |
Output path for signature (use - for stdout) |
-n |
Network: mainnet or testnet (default: testnet) |
Example:
mina-frost-client coordinator \
-c ~/.frost/alice.toml \
-s localhost:2744 \
-g <GROUP_PUBLIC_KEY> \
-S <BOB_PUBKEY>,<EVE_PUBKEY> \
-m ./tx-json/update-state-transaction.json \
-o ./signed-tx.json \
-n testnetEach selected signer joins the session:
mina-frost-client participant \
-c <CONFIG_PATH> \
-s <SERVER_URL> \
-g <GROUP_PUBLIC_KEY> \
-S <SESSION_ID> \
-yParameters:
| Flag | Description |
|---|---|
-c |
Path to participant's config file |
-s |
Server URL (optional if stored in group config) |
-g |
Group public key |
-S |
Session ID (optional if only one active session) |
-y |
Auto-approve signing (skip confirmation prompt) |
Example:
# Bob joins
mina-frost-client participant \
-c ~/.frost/bob.toml \
-s localhost:2744 \
-g <GROUP_PUBLIC_KEY> \
-y
# Eve joins
mina-frost-client participant \
-c ~/.frost/eve.toml \
-s localhost:2744 \
-g <GROUP_PUBLIC_KEY> \
-yList active sessions:
mina-frost-client sessions \
-c <CONFIG_PATH> \
-s <SERVER_URL> \
-g <GROUP_PUBLIC_KEY>Close all sessions (cleanup):
mina-frost-client sessions \
-c <CONFIG_PATH> \
-s <SERVER_URL> \
-g <GROUP_PUBLIC_KEY> \
--close-allNote: All users must be online during FROST signing for successful participation, if a user loses connection, the session must be restarted.
Convert the signed transaction to a GraphQL mutation:
mina-frost-client graphql-build \
-i <SIGNED_TRANSACTION_JSON> \
-o <GRAPHQL_OUTPUT_FILE>Example:
mina-frost-client graphql-build \
-i ./signed-tx.json \
-o ./broadcast.graphqlSubmit the GraphQL mutation to a Mina node:
mina-frost-client graphql-broadcast \
-g <GRAPHQL_FILE> \
-e <ENDPOINT_URL>Example:
mina-frost-client graphql-broadcast \
-g ./broadcast.graphql \
-e https://api.minascan.io/node/devnet/v1/graphqlThese are an example of GraphQL endpoints, we highly recommending users to use their own node's URLs if they have one.
| Network | Endpoint |
|---|---|
| Mainnet | https://api.minascan.io/node/mainnet/v1/graphql |
| Devnet | https://api.minascan.io/node/devnet/v1/graphql |
| Berkeley | https://api.minascan.io/node/berkeley/v1/graphql |
| Command | Description | Key Flags |
|---|---|---|
init |
Initialize participant config | -c <config> |
export |
Export contact string | -n <name> -c <config> |
import |
Import a contact | <contact> -c <config> |
contacts |
List contacts | -c <config> |
remove-contact |
Remove a contact | -c <config> -p <pubkey> |
trusted-dealer |
Test-only key generation | -c <configs...> -d <desc> -N <names> -t <threshold> |
dkg |
Distributed key generation | -c <config> -d <desc> -s <server> -t <threshold> -S <participants> |
groups |
List groups | -c <config> |
remove-group |
Remove a group | -c <config> -g <group> |
sessions |
List/manage sessions | -c <config> -s <server> -g <group> --close-all |
coordinator |
Start signing session | -c <config> -s <server> -g <group> -S <signers> -m <message> -o <signature> -n <network> |
participant |
Join signing session | -c <config> -s <server> -g <group> -S <session> -y |
graphql-build |
Build GraphQL mutation | -i <input> -o <output> |
graphql-broadcast |
Broadcast transaction | -g <graphql> -e <endpoint> |
| Issue | Possible Cause | Solution |
|---|---|---|
| Connection refused | Server not running | Start frostd server |
| Certificate error | Invalid TLS cert | Regenerate with mkcert or look at your certificate setup |
| Group not found | Wrong group key | Run groups to list valid keys |
| Session timeout | Participants too slow | Setup a new session |
| Issue | Possible Cause | Solution |
|---|---|---|
| Invalid Fee Excess | Fee Payer does not have any funds | Ensure that the FROST account you generate has funds or use an external FeePayer |
- O1JS-WORKFLOW.md — Generating unsigned Mina transactions with o1js
- README.md — Project overview and repository layout
- frostd documentation — FROST server setup guide
- Mina Protocol Documentation — Official Mina documentation