Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c41870e
Implement coordinator runtime for MPC, including request handling, se…
vietddude Apr 16, 2026
11b11f7
Add initial implementation of cosigner runtime with configuration, id…
vietddude Apr 16, 2026
d36a786
Implement configuration management for coordinator and cosigner runti…
vietddude Apr 16, 2026
b166d80
Refactor cosigner runtime to enhance configuration management and ide…
vietddude Apr 17, 2026
d00f0d8
Enhance logger tests to verify output includes test file name and exc…
vietddude Apr 17, 2026
f8fb621
Refactor cosigner to support multiple relay providers (NATS and MQTT)…
vietddude Apr 17, 2026
5d1d03a
Add initial implementation of MQTT relay with NATS integration. Intro…
vietddude Apr 17, 2026
ac85dea
Refactor NATS configuration in cosigner to use structured natsConfig …
vietddude Apr 17, 2026
033c2ee
Refactor cosigner configuration to enhance MQTT relay management and …
vietddude Apr 17, 2026
e6980cf
Enhance coordinator functionality by enabling detailed logging during…
vietddude Apr 17, 2026
666c8a9
Add coordinator client for signing requests with EdDSA protocol. Impl…
vietddude Apr 17, 2026
97c8d32
Update dependencies in go.mod and go.sum; refactor key generation and…
vietddude Apr 18, 2026
2027943
Add documentation for local mixed transport setup with NATS and MQTT …
vietddude Apr 18, 2026
6693f03
Update SDK dependency references from vietddude to fystack in go.mod …
vietddude Apr 18, 2026
10a2c9b
Add local SDK replacement instructions for development. Include detai…
vietddude Apr 18, 2026
c40b6a4
Refactor coordinator runtimes and add gRPC orchestration.
vietddude Apr 22, 2026
18af503
Add gRPC transport support to coordinator client.
vietddude Apr 22, 2026
e630eeb
Update coordinator docs for mixed gRPC and NATS flow.
vietddude Apr 22, 2026
2373530
Add ECDSA and EdDSA public key handling in gRPC session results
vietddude Apr 22, 2026
636db71
Improve dual-keygen orchestration with seeded key reuse.
vietddude Apr 22, 2026
20b1e57
Serialize cosigner session operations and queue early peer packets.
vietddude Apr 22, 2026
194f6f2
Normalize keygen protocol handling across coordinator clients.
vietddude Apr 22, 2026
b7b12be
Refactor keygen result handling in coordinator to use unified Result …
vietddude Apr 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ node2
config.yaml
.vscode
.vagrant
.chain_code
.chain_code
.gomodcache
.codex
coordinator-snapshots/
relay.credentials
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: all build clean mpcium mpc install reset test test-verbose test-coverage e2e-test e2e-clean cleanup-test-env
.PHONY: all build clean mpcium mpc install reset test test-verbose test-coverage e2e-test e2e-clean cleanup-test-env proto proto-tools

BIN_DIR := bin

Expand Down Expand Up @@ -83,6 +83,13 @@ endif
test:
go test ./...

proto-tools:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

proto:
$(MAKE) -C ../sdk proto

# Run tests with verbose output
test-verbose:
go test -v ./...
Expand Down
64 changes: 64 additions & 0 deletions cmd/mpcium-coordinator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Mpcium Coordinator MVP

This runtime implements the v1 control-plane coordinator from `docs/architecture/external-cosigner-runtime.md`.

It owns:

- NATS request-reply intake on `mpc.v1.request.keygen`, `mpc.v1.request.sign`, and `mpc.v1.request.reshare`
- optional plaintext gRPC client orchestration API for `Keygen`, `Sign`, and `WaitSessionResult`
- pinned participant validation
- session lifecycle state
- signed control fan-out to `mpc.v1.peer.<peerId>.control`
- participant event intake from `mpc.v1.session.<sessionId>.event`
- terminal result publishing to `mpc.v1.session.<sessionId>.result`

It does not implement relay, MQTT mailboxing, p2p MPC packet routing, gRPC participant transport, or legacy `mpc.*` subjects. NATS is still required for cosigner presence, control fan-out, participant session events, and result publishing.

## Run

```sh
go run ./cmd/mpcium-coordinator/main.go -c coordinator.config.yaml
```

The runtime config includes:

- `nats.url`: NATS server used for participant transport.
- `grpc.enabled`: enables the client orchestration API.
- `grpc.listen_addr`: plaintext gRPC listen address.
- `grpc.poll_interval`: result wait polling interval.
- `coordinator.id`, `coordinator.private_key_hex`, and `coordinator.snapshot_dir`.

Each operation has its own request shape. The operation comes from the NATS subject, so a sign request to `mpc.v1.request.sign` looks like:

```json
{
"request_id": "req_123",
"ttl_sec": 120,
"threshold": 2,
"participants": [
{ "peer_id": "peer-node-01", "transport": "nats" },
{ "peer_id": "peer-node-02", "transport": "nats" }
],
"wallet_id": "wallet_123",
"key_type": "secp256k1",
"tx_id": "tx_456",
"tx_hash": "0xabc"
}
```

For keygen, send `wallet_id`, `threshold`, and the full keygen participant set to `mpc.v1.request.keygen`. `key_type` is optional; when omitted, participants should generate both `secp256k1` and `ed25519` for that wallet/session. For sign, send exactly the participants selected for this signing session; MVP validation requires `len(participants) == threshold`.

Internal `nats` participants must publish online presence before requests are accepted:

```json
{
"v": 1,
"type": "peer.presence",
"peer_id": "peer-node-01",
"status": "online",
"transport": "nats",
"last_seen_at": "2026-04-16T10:00:00Z"
}
```

Publish it to `mpc.v1.peer.peer-node-01.presence`.
130 changes: 130 additions & 0 deletions cmd/mpcium-coordinator/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package main

import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"

"github.com/fystack/mpcium/internal/coordinator"
"github.com/fystack/mpcium/pkg/config"
"github.com/fystack/mpcium/pkg/logger"
"github.com/nats-io/nats.go"
"github.com/urfave/cli/v3"
)

const coordinatorConfigPath = "coordinator.config.yaml"

func main() {
logger.Init(os.Getenv("ENVIRONMENT"), false)

cmd := &cli.Command{
Name: "mpcium-coordinator",
Usage: "Run MPC coordinator runtime",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
Usage: "Path to coordinator config file",
Value: coordinatorConfigPath,
},
},
Action: run,
}

if err := cmd.Run(context.Background(), os.Args); err != nil {
logger.Error("coordinator exited with error", err)
os.Exit(1)
}
}

func run(ctx context.Context, c *cli.Command) error {
configPath := c.String("config")
config.InitViperConfig(configPath)

cfg, err := coordinator.LoadRuntimeConfig()
if err != nil {
return err
}
if err := cfg.Validate(); err != nil {
return err
}

signer, err := coordinator.NewEd25519SignerFromHex(cfg.PrivateKeyHex)
if err != nil {
return err
}

nc, err := nats.Connect(cfg.NATSURL)
if err != nil {
return fmt.Errorf("connect to NATS: %w", err)
}
defer nc.Close()

ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer stop()

snapshotStore := coordinator.NewAtomicFileSnapshotStore(cfg.SnapshotDir)
sessionStore, err := coordinator.NewMemorySessionStore(ctx, snapshotStore)
if err != nil {
return fmt.Errorf("restore coordinator state: %w", err)
}
keyInfoStore := coordinator.NewMemoryKeyInfoStore()
if err := coordinator.RestoreKeyInfoFromSnapshotStore(ctx, snapshotStore, keyInfoStore); err != nil {
return fmt.Errorf("restore key info: %w", err)
}

presence := coordinator.NewInMemoryPresenceView()
coord, err := coordinator.NewCoordinator(coordinator.CoordinatorConfig{
CoordinatorID: cfg.ID,
Signer: signer,
EventVerifier: coordinator.Ed25519SessionEventVerifier{},
Store: sessionStore,
KeyInfoStore: keyInfoStore,
Presence: presence,
Controls: coordinator.NewNATSControlPublisher(nc),
Results: coordinator.NewNATSResultPublisher(nc),
DefaultSessionTTL: cfg.DefaultSessionTTL,
})
if err != nil {
return err
}

natsRuntime := coordinator.NewNATSRuntime(nc, coord, presence)
composite := coordinator.NewCompositeRuntime(natsRuntime)
if cfg.GRPCEnabled {
composite = coordinator.NewCompositeRuntime(
natsRuntime,
coordinator.NewGRPCRuntime(cfg.GRPCListenAddr, coord, cfg.GRPCPollInterval),
)
}

if err := composite.Start(ctx); err != nil {
return err
}
defer func() {
if err := composite.Stop(); err != nil {
logger.Error("stop coordinator runtime failed", err)
}
}()

return runTickLoop(ctx, coord, cfg.TickInterval)
}

func runTickLoop(ctx context.Context, coord *coordinator.Coordinator, interval time.Duration) error {
ticker := time.NewTicker(interval)
defer ticker.Stop()

for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
if _, err := coord.Tick(ctx); err != nil {
logger.Error("coordinator tick error", err)
}
}
}
}
61 changes: 61 additions & 0 deletions cmd/mpcium-cosigner/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"context"
"os"
"os/signal"
"syscall"

"github.com/fystack/mpcium/internal/cosigner"
"github.com/fystack/mpcium/pkg/config"
"github.com/fystack/mpcium/pkg/logger"
"github.com/urfave/cli/v3"
)

const cosignerConfigPath = "cosigner.config.yaml"

func main() {
logger.Init(os.Getenv("ENVIRONMENT"), false)

cmd := &cli.Command{
Name: "mpcium-cosigner",
Usage: "Run MPC cosigner runtime",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
Usage: "Path to cosigner config file",
Value: cosignerConfigPath,
},
},
Action: run,
}

if err := cmd.Run(context.Background(), os.Args); err != nil {
logger.Error("cosigner exited with error", err)
os.Exit(1)
}
}

func run(ctx context.Context, c *cli.Command) error {
configPath := c.String("config")
config.InitViperConfig(configPath)
cfg, err := cosigner.LoadConfig()
if err != nil {
return err
}

return runCosigner(ctx, cfg)
}

func runCosigner(ctx context.Context, cfg cosigner.Config) error {
runtime, err := cosigner.NewRuntime(cfg)
if err != nil {
return err
}
defer runtime.Close()

ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer stop()
return runtime.Run(ctx)
}
57 changes: 57 additions & 0 deletions cmd/mpcium-relay/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package main

import (
"context"
"os"
"os/signal"
"syscall"

"github.com/fystack/mpcium/internal/relay"
"github.com/fystack/mpcium/pkg/config"
"github.com/fystack/mpcium/pkg/logger"
"github.com/urfave/cli/v3"
)

const relayConfigPath = "relay.config.yaml"

func main() {
logger.Init(os.Getenv("ENVIRONMENT"), false)

cmd := &cli.Command{
Name: "mpcium-relay",
Usage: "Run MQTT relay runtime",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
Usage: "Path to relay config file",
Value: relayConfigPath,
},
},
Action: run,
}

if err := cmd.Run(context.Background(), os.Args); err != nil {
logger.Error("relay exited with error", err)
os.Exit(1)
}
}

func run(ctx context.Context, c *cli.Command) error {
configPath := c.String("config")
config.InitViperConfig(configPath)
cfg, err := relay.LoadConfig()
if err != nil {
return err
}

runtime, err := relay.NewRuntime(cfg)
if err != nil {
return err
}
defer runtime.Close()

ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer stop()
return runtime.Run(ctx)
}
12 changes: 12 additions & 0 deletions coordinator.config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
nats:
url: nats://127.0.0.1:4222

grpc:
enabled: true
listen_addr: 127.0.0.1:50051
poll_interval: 200ms

coordinator:
id: coordinator-01
private_key_hex: "86ed171146e6003841f1686c0958b68ae84f9992974c2c6febfb9df7f424b3adb64ca8ec459081a299aecc2b2b5d555265b15ddfd29e792ddd08bedb418bdd0d"
snapshot_dir: coordinator-snapshots
21 changes: 21 additions & 0 deletions cosigner.config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
relay_provider: nats
nats:
url: "nats://127.0.0.1:4222"
# username: ""
# password: ""
# tls:
# client_cert: ""
# client_key: ""
# ca_cert: ""
# mqtt:
# broker: "tcp://localhost:1883"
# client_id: "cosigner-1"
# username: ""
# password: ""

node_id: peer-node-01
data_dir: node-v1-data
identity_private_key_hex: "b14d168636008a9c766a6c231c182446e4b636cd2116817a89d068ffb5cc49e456a47a1103b610d6c85bf23ddb1f78ff6404f7c6f170d46441a268e105873cc4"

coordinator_id: coordinator-01
coordinator_public_key_hex: "b64ca8ec459081a299aecc2b2b5d555265b15ddfd29e792ddd08bedb418bdd0d"
16 changes: 16 additions & 0 deletions cosigner2.config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
relay_provider: mqtt
mqtt:
broker: tcp://127.0.0.1:1883
client_id: peer-node-02
username: peer-node-02
password: peer-node-02

# nats:
# url: nats://127.0.0.1:4222

node_id: peer-node-02
data_dir: node-v1-data-02
identity_private_key_hex: "a96d8c0de1b5682740f6487b13dc7477aaa739b900c6f5c3db737ca019163efad9034dd84e0dd10a57d6a09a8267b217051d5f121ff52fca66c2b485be16ae02"

coordinator_id: coordinator-01
coordinator_public_key_hex: "b64ca8ec459081a299aecc2b2b5d555265b15ddfd29e792ddd08bedb418bdd0d"
Loading
Loading