Standalone threshold MPC node for distributed ECDSA (Secp256k1) and EdDSA (Ed25519) signatures. Nodes form a P2P mesh, run distributed key generation, and produce threshold signatures without any single party holding the complete key.
This project is extracted from the NEAR MPC repository, with all NEAR blockchain dependencies removed. The core cryptographic protocols (threshold-signatures crate) and TLS transport (mpc-tls crate) are imported as git dependencies from the upstream repo, pinned to a specific commit. The node and cli crates are rewritten to run as a standalone P2P network driven by HTTP API calls instead of an on-chain contract.
Built on FROST for EdDSA and an OT-based protocol (derived from Cait-Sith) for ECDSA.
+-------------------+ +-------------------+ +-------------------+
| Node 1 | | Node 2 | | Node 3 |
| (HTTP API :3001) |<--->| (HTTP API :3002) |<--->| (HTTP API :3003) |
| (P2P TLS :10001) | | (P2P TLS :10002) | | (P2P TLS :10003) |
+-------------------+ +-------------------+ +-------------------+
| | |
+---------+---------------+---------+---------------+
| |
+--------+--------+ +--------+--------+
| threshold-sigs | | tls |
| (crypto library)| | (mTLS transport)|
+-----------------+ +-----------------+
Two local workspace crates, plus two upstream git dependencies:
| Crate | Type | Source | Purpose |
|---|---|---|---|
node |
Binary | Local | MPC node: P2P mesh, orchestrator, HTTP API. Wires crypto protocols to network transport. |
cli |
Binary | Local | Command-line client for interacting with running MPC nodes. |
threshold-signatures |
Library | near/mpc | Pure cryptographic protocols: ECDSA, EdDSA (FROST), CKD (BLS12-381). No networking or I/O. |
mpc-tls |
Library | near/mpc | Mutual TLS (mTLS) using Ed25519 identity keys for P2P authentication. |
-
Build the CLI (needed by the setup script to derive Ed25519 public keys):
cargo build -p mpc-cli
-
Set up nodes for a 3-node network with threshold 2 (docker-compose is harcoded to that set up. Directly run the binary to use different ones):
./scripts/setup-nodes.sh 3 2
This creates
config/node{1,2,3}.yaml,config/node{1,2,3}.key, andconfig/aes.key. -
Set the AES key and start the network:
export MPC_AES_KEY=$(cat config/aes.key) cd docker && docker compose up --build
-
Wait for
"All peers connected!"in the logs. -
Interact via CLI:
cargo run -p mpc-cli -- dkg --scheme eddsa --node http://localhost:3001
Launch all nodes as local processes:
./scripts/launch-localnet.sh 3 2This builds the binaries, generates configs with --local, starts 3 nodes, and waits for them to connect. Use the CLI against http://localhost:3001.
Flags:
--no-build-- skip thecargo buildstep
Install: cargo build --release -p mpc-cli
All commands accept --node <URL> (default: http://localhost:3001).
| Command | Description | Example |
|---|---|---|
dkg |
Run distributed key generation | mpc-cli dkg --scheme eddsa |
sign |
Create a threshold signature | mpc-cli sign --scheme eddsa --payload 68656c6c6f |
verify |
Verify a signature locally | See below |
generate |
Generate ECDSA triples or presignatures | mpc-cli generate --asset triples |
status |
Query node state and peer info | mpc-cli status |
derive-pubkey |
Derive Ed25519 public key from secret | mpc-cli derive-pubkey --secret-key <hex> |
EdDSA:
mpc-cli verify --scheme eddsa \
--payload <hex> \
--public-key <hex> \
--signature <hex>ECDSA:
mpc-cli verify --scheme ecdsa \
--payload <32-byte-hash-hex> \
--public-key <sec1-hex> \
--signature-r <hex> \
--signature-s <hex>The --signature-r value is the hex encoding of the JSON-serialized AffinePoint returned by the sign endpoint. The CLI decodes this to extract the x-coordinate as the ECDSA r scalar.
# 1. Distributed key generation
mpc-cli dkg --scheme eddsa --node http://localhost:3001
# Output: Public key: <hex>
# 2. Sign a message (arbitrary bytes as hex)
PAYLOAD=$(echo -n "hello" | xxd -p)
mpc-cli sign --scheme eddsa --payload $PAYLOAD --node http://localhost:3001
# Output: { "signature": "<hex>", "public_key": "<hex>" }
# 3. Verify
mpc-cli verify --scheme eddsa \
--payload $PAYLOAD \
--public-key <public_key from sign> \
--signature <signature from sign># 1. Distributed key generation
mpc-cli dkg --scheme ecdsa --node http://localhost:3001
# 2. Generate Beaver triples (offline preprocessing)
mpc-cli generate --asset triples --node http://localhost:3001
# 3. Generate presignatures (consumes 2 triples each)
mpc-cli generate --asset presignatures --node http://localhost:3001
# 4. Sign a 32-byte hash
HASH=$(echo -n "hello" | shasum -a 256 | cut -d' ' -f1)
mpc-cli sign --scheme ecdsa --payload $HASH --node http://localhost:3001
# Output: { "r": "<hex>", "s": "<hex>", "public_key": "<hex>" }
# 5. Verify
mpc-cli verify --scheme ecdsa \
--payload $HASH \
--public-key <public_key> \
--signature-r <r> \
--signature-s <s>End-to-end tests (builds, spawns nodes, runs DKG + sign + verify for both schemes):
./scripts/test-e2e.shUnit and integration tests:
cargo testNode configs are YAML files generated by scripts/setup-nodes.sh. Key fields:
node_name: node1 # Must match a participant name
p2p_port: 10001 # TLS P2P port
api_port: 3001 # HTTP API port
data_dir: /data/node1 # RocksDB storage directory
triple: # ECDSA triple generation tuning
concurrency: 2
desired_triples_to_buffer: 128
timeout_sec: 120
presignature: # ECDSA presignature tuning
concurrency: 4
desired_presignatures_to_buffer: 64
timeout_sec: 120
signature:
timeout_sec: 60
keygen:
timeout_sec: 120
participants:
threshold: 2 # Min participants needed for signing
participants:
- id: 0
name: node1
address: node1 # Hostname or IP
port: 10001
p2p_public_key: <hex> # Ed25519 public keyEnvironment variables:
MPC_AES_KEY-- hex-encoded AES-128 key for encrypting local RocksDB storageRUST_LOG-- tracing filter (default:info)
Threshold MPC requires all nodes to know each other before starting. Every node needs the full list of participants — their IDs, addresses, ports, and Ed25519 public keys — because:
- TLS authentication — nodes use mutual TLS with Ed25519 identity keys embedded in certificates. After the TLS handshake, each side extracts the peer's public key from its certificate and looks it up in the participant list. Unknown keys are rejected.
- Protocol correctness — distributed key generation (DKG) binds key shares to a specific set of participants and threshold. You can't add or remove participants without resharing.
In the upstream NEAR MPC repository, the participant list is stored on-chain in a NEAR smart contract. The contract's init() function receives a ThresholdParameters object containing all participants' account IDs, addresses, ports, and public keys. Each node runs an indexer that polls the contract every second to read the participant list. Changing membership requires threshold-many participants to vote via vote_new_parameters(), which triggers a full key resharing protocol.
This standalone version replaces the on-chain contract with static YAML configuration files. The setup-nodes.sh script takes the role of the contract's init() — it generates all Ed25519 keypairs, derives public keys, and writes the complete participant list into every node's config file:
NEAR MPC (upstream) Standalone (this repo)
─────────────────── ──────────────────────
Contract init() with setup-nodes.sh generates
ThresholdParameters → config/node{1..N}.yaml
Indexer polls contract Node reads YAML at startup
every 1 second for state
vote_new_parameters() + Regenerate configs +
resharing protocol restart all nodes
The trust model is identical: a closed, pre-configured cluster where the participant set is fixed at deployment time. There is no peer discovery, gossip protocol, or dynamic join mechanism. To add a node, you must regenerate all configs and restart the entire cluster.
threshold-signer/
crates/
node/ -- MPC node binary: P2P mesh, orchestrator, HTTP API
cli/ -- CLI client for interacting with nodes
scripts/
setup-nodes.sh -- Generate P2P keys, AES key, and YAML configs (requires mpc-cli)
launch-localnet.sh -- Launch local multi-node network
test-e2e.sh -- End-to-end test suite
docker/
Dockerfile -- Multi-stage build for node + CLI
docker-compose.yml -- 3-node Docker Compose setup
config/ -- Generated node configs (gitignored)
data/ -- Runtime data directories (gitignored)
The threshold-signatures and mpc-tls crates are imported from upstream near/mpc as git dependencies.