Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@ node2
config.yaml
.vscode
.vagrant
.chain_code
.chain_code
# DKLs23 build output and local artifacts
/mpcium
/mpcium-cli
*.test
benchmark/output.txt
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[submodule "third_party/dkls23"]
path = third_party/dkls23
url = git@github.com:fystack/DKLs23.git
branch = dev
32 changes: 32 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,38 @@ else
@echo "Successfully installed mpcium and mpcium-cli to $(INSTALL_DIR)/"
endif

# Build the DKLs23 Rust cgo library required by the (opt-in, build-tag "dkls") DKLs23 backend
dkls-lib:
cd third_party/dkls23/wrapper/go-ll && cargo build --release

# Run DKLs23 backend tests (requires dkls-lib built first)
test-dkls: dkls-lib
CGO_ENABLED=1 go test -tags dkls ./pkg/mpc/dkls/...

# Run DKLs23 tests against a real (throwaway, dockerized) NATS server instead
# of the in-process MemoryTransport used by test-dkls.
test-dkls-nats: dkls-lib
docker rm -f mpcium-dkls-test-nats >/dev/null 2>&1 || true
docker run -d --name mpcium-dkls-test-nats -p 14222:4222 nats:latest -js
CGO_ENABLED=1 go test -tags 'dkls dklsnats' ./pkg/mpc/dkls/... ; \
status=$$?; \
docker rm -f mpcium-dkls-test-nats >/dev/null 2>&1; \
exit $$status

# Run the DKLs23 vs tss-lib in-process benchmark (requires dkls-lib built first)
bench-dkls: dkls-lib
CGO_ENABLED=1 go test -tags dklsbench -bench . -benchtime=5x -run '^$$' ./benchmark/dklsvstss/...

# Run the DKLs23 e2e benchmark (real NATS, real ed25519 message signing) instead
# of the pure library-level bench-dkls above.
bench-dkls-nats: dkls-lib
docker rm -f mpcium-dkls-bench-nats >/dev/null 2>&1 || true
docker run -d --name mpcium-dkls-bench-nats -p 14222:4222 nats:latest -js
CGO_ENABLED=1 go test -tags 'dkls dklsnats' -bench . -benchtime=3x -run '^$$' ./pkg/mpc/dkls/... ; \
status=$$?; \
docker rm -f mpcium-dkls-bench-nats >/dev/null 2>&1; \
exit $$status

# Run all tests
test:
go test ./...
Expand Down
84 changes: 84 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,72 @@ mpcium generate-authorizer \
- `--encrypt` stores the private key as `<name>.authorizer.key.age`; omit it to write the plain hex key.
- The identity JSON is saved as `<name>.authorizer.identity.json`; copy the `public_key` into `authorization.authorizer_public_keys`.

## DKLs23 (optional ECDSA backend)

Besides tss-lib, Mpcium can sign with [DKLs23](https://eprint.iacr.org/2023/765) threshold ECDSA (secp256k1 only). It uses far less CPU per signature and is much faster at scale; see [benchmark results](./benchmark/results/dkls-vs-tss.md).

![DKLs23 vs tss-lib signing throughput](./benchmark/results/dkls-vs-tss.svg)

It is opt-in: the Rust library is built through cgo behind the `dkls` build tag, and the default build does not include it.

### Build

```bash
git submodule update --init third_party/dkls23 # needs read access to fystack/DKLs23
make dkls-lib # cargo build --release (Rust toolchain required)
go build -tags dkls -o mpcium ./cmd/mpcium
```

- Clone with `--recurse-submodules` to fetch the submodule in one step.
- The binary loads the library built in `third_party/dkls23/target/release`, so run it on the machine that built it.
- All nodes must run a `-tags dkls` build of the same version; upgrade them together.
- `mpcium-cli` and Go clients need no build tag.

### Use

Nodes start as usual (`mpcium start -n node0`). Clients opt in per request with `types.ProtocolDkls23`:

```go
// Keygen also creates the tss-lib ECDSA/EdDSA keys; the DKLs23 result carries DKLSPubKey and DKLSChainCode.
mpcClient.CreateWalletWithProtocol(walletID, types.ProtocolDkls23, nil)

mpcClient.SignTransaction(&types.SignTxMessage{
KeyType: types.KeyTypeSecp256k1,
Protocol: types.ProtocolDkls23,
WalletID: walletID,
TxID: txID,
Tx: hash32[:], // DKLs23 signs a 32-byte hash
DerivationPath: []uint32{1, 5}, // optional, see below
})
```

- The signing result has `R`, `S`, `SignatureRecovery`, and `Signature` (`r || s`).
- `dkls_pub_key` is the 64-byte `X||Y` public key, the same format as tss-lib ECDSA keys.
- **HD derivation:** set `DerivationPath` to sign with a BIP-32 child key. Only non-hardened indices (below 2^31) work, because no party holds the full key. The chain code comes from the DKG, not from `chain_code` in the config. With the public key and `dkls_chain_code`, clients derive child public keys offline.
- **Refresh:** `mpcClient.RefreshDkls(&types.RefreshMessage{WalletID: id, SessionID: uuid})` rotates every share and keeps the public key and chain code. All participants must be online, and the result arrives through `OnResharingResult`.
- Not supported for DKLs23 wallets: EdDSA and resharing (changing the committee or threshold).

A runnable end-to-end example (keygen, sign with the master and a child key, verify both signatures):

```bash
go run ./examples/dkls --path 1,5
```

### Tests

```bash
make test-dkls # in-memory and in-process tests
make test-dkls-nats # same, plus real NATS (starts a Docker container)
```

End-to-end test on 3 real nodes (keygen, sign, child-key sign, refresh, and failure cases). It needs `mpcium` and `mpcium-cli` in `PATH`, with `mpcium` built using `-tags dkls`:

```bash
cd e2e && CGO_ENABLED=1 E2E_DKLS=1 go test -run TestDklsSigning -timeout 10m -v .
```

Without `E2E_DKLS=1` the test is skipped.

### Testing

## 1. Unit tests
Expand Down Expand Up @@ -515,3 +581,21 @@ mpcium-cli benchmark --config config.yaml --output signing-results.txt \
```

Use `--prompt-password` for secure key password input and `--help` for all options.

### DKLs23 Benchmark

```bash
# Create a DKLs23 wallet; the wallet ID is printed as "Wallet created: <id>"
mpcium-cli benchmark --client-id run1 keygen-dkls 1

# Sign 1000 requests and write the summary to a file
mpcium-cli benchmark --client-id run1 --output dkls.txt sign-dkls23 1000 <wallet-id> --batch-size 8

# Library-level comparison (tss-lib vs DKLs23, no network) and a real-NATS run
make bench-dkls
make bench-dkls-nats
```

- Give every run its own `--client-id`. Without it, all clients share one results consumer and another consumer on the same NATS can take your results, so the benchmark waits until it times out.
- Keep `max_concurrent_signing` at its default for tss-lib: its consumer handles one request at a time, and a large value makes requests wait past the ack time and get redelivered.
- Saved results and a comparison with tss-lib are in [`benchmark/results`](./benchmark/results).
124 changes: 124 additions & 0 deletions benchmark/dklsvstss/bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//go:build dklsbench

// Package dklsvstss benchmarks mpcium's tss-lib ECDSA backend against the DKLs23
// implementation (github.com/fystack/DKLs23, vendored under third_party/dkls23),
// running both in-process with local message routing (no network).
//
// tss-lib's ECDSA keygen needs Paillier "safe prime" preparams per party; generating
// them is slow (tens of seconds each) but, exactly as mpcium's Node does in production
// (see pkg/mpc/node.go generatePreParams), they are precomputed once and reused across
// keys — so that cost is paid once here too, outside the timed loop.
//
// Run with:
//
// CGO_ENABLED=1 go test -tags dklsbench -bench . -benchtime=5x -run '^$' -timeout 30m ./benchmark/dklsvstss/...
package dklsvstss

import (
"crypto/sha256"
"math/big"
"sync"
"testing"
"time"

"github.com/bnb-chain/tss-lib/v3/ecdsa/keygen"
)

const (
numParties = 2
threshold = 1 // 2-of-2
)

var (
preParamsOnce sync.Once
preParams []*keygen.LocalPreParams
)

// sharedPreParams mirrors mpcium's Node: preparams are generated once and reused,
// so their (slow) generation cost is never part of a keygen benchmark.
func sharedPreParams(b *testing.B) []*keygen.LocalPreParams {
b.Helper()
preParamsOnce.Do(func() {
preParams = make([]*keygen.LocalPreParams, numParties)
errs := make([]error, numParties)
var wg sync.WaitGroup
for i := 0; i < numParties; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
preParams[i], errs[i] = keygen.GeneratePreParams(3 * time.Minute)
}(i)
}
wg.Wait()
for _, err := range errs {
if err != nil {
b.Fatalf("generate preparams: %v", err)
}
}
})
return preParams
}

func BenchmarkTssLibKeygen(b *testing.B) {
pp := sharedPreParams(b)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := runTssKeygen(numParties, threshold, pp); err != nil {
b.Fatalf("tss-lib keygen failed: %v", err)
}
}
}

func BenchmarkDklsKeygen(b *testing.B) {
for i := 0; i < b.N; i++ {
shares, err := runDklsDKG(uint8(numParties), uint8(threshold+1))
if err != nil {
b.Fatalf("dkls keygen failed: %v", err)
}
for _, s := range shares {
s.Free()
}
}
}

func BenchmarkTssLibSign(b *testing.B) {
pp := sharedPreParams(b)
keys, err := runTssKeygen(numParties, threshold, pp)
if err != nil {
b.Fatalf("tss-lib keygen (setup) failed: %v", err)
}
partyIDs := genTestPartyIDs(numParties)
hashBytes := sha256.Sum256([]byte("dkls-vs-tss-bench"))
msgHash := new(big.Int).SetBytes(hashBytes[:])

signers := keys[:threshold+1]
signerIDs := partyIDs[:threshold+1]

b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := runTssSign(signers, signerIDs, threshold, msgHash); err != nil {
b.Fatalf("tss-lib sign failed: %v", err)
}
}
}

func BenchmarkDklsSign(b *testing.B) {
shares, err := runDklsDKG(uint8(numParties), uint8(threshold+1))
if err != nil {
b.Fatalf("dkls keygen (setup) failed: %v", err)
}
defer func() {
for _, s := range shares {
s.Free()
}
}()
msgHash := sha256.Sum256([]byte("dkls-vs-tss-bench"))
signers := shares[:threshold+1]

b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := runDklsSign(signers, msgHash[:]); err != nil {
b.Fatalf("dkls sign failed: %v", err)
}
}
}
Loading
Loading