How to Migrate Existing Wallets to a New Threshold Configuration via Resharing in mpcium
Migrate existing wallets to a new threshold configuration via resharing by constructing a ResharingMessage with the new committee and threshold, invoking MPCClient.Resharing to publish the signed request, and subscribing to OnResharingResult to receive the updated public key and key metadata.
The mpcium library provides a secure Multi-Party Computation (MPC) infrastructure for threshold signature schemes. When you need to migrate existing wallets to a new threshold configuration via resharing, the library coordinates a distributed protocol that transfers signing authority from an old committee to a new committee without ever reconstructing the private key.
Prepare the ResharingMessage
Begin by constructing a ResharingMessage that defines the migration parameters. This struct is defined in [pkg/types/initiator_msg.go](https://github.com/fystack/mpcium/blob/master/pkg/types/initiator_msg.go#L54-L62) and requires the following fields:
SessionID: A unique UUID for this resharing run.WalletID: The identifier of the existing wallet being migrated.NodeIDs: The complete list of peer IDs forming the new committee.NewThreshold: The desired threshold value (must satisfyNewThreshold + 1 ≤ len(NodeIDs)).KeyType: The signature algorithm, eithertypes.KeyTypeEd25519ortypes.KeyTypeSecp256k1.
resharingMsg := &types.ResharingMessage{
SessionID: uuid.NewString(),
WalletID: "c2a5c9d4-...",
NodeIDs: []string{"node-A-uuid", "node-B-uuid", "node-C-uuid"},
NewThreshold: 2,
KeyType: types.KeyTypeEd25519,
}
Before transmission, the message must be canonically serialized for signing. The Raw() method (lines 106-110 in initiator_msg.go) strips the Signature and AuthorizerSignatures fields to produce a deterministic payload. The MPCClient.Resharing method automatically invokes Raw() and signs the result using the configured Signer instance.
Invoke the Resharing Protocol
Publish the resharing request through the MPCClient.Resharing API located in [pkg/client/client.go](https://github.com/fystack/mpcium/blob/master/pkg/client/client.go#L200-L212). This method serializes the message, attaches the initiator's signature, and publishes it to the mpc.mpc_reshare NATS topic.
if err := mpcClient.Resharing(resharingMsg); err != nil {
logger.Fatal("Resharing failed", err)
}
Upon receiving the event in [pkg/eventconsumer/event_consumer.go](https://github.com/fystack/mpcium/blob/master/pkg/eventconsumer/event_consumer.go#L582), the server unmarshals the message and initiates the appropriate TSS resharing session:
- Ed25519: Uses
NewEDDSAReshareSessionin [pkg/mpc/eddsa_resharing_session.go](https://github.com/fystack/mpcium/blob/master/pkg/mpc/eddsa_resharing_session.go). - Secp256k1: Uses
NewECDSAReshareSessionin [pkg/mpc/ecdsa_resharing_session.go](https://github.com/fystack/mpcium/blob/master/pkg/mpc/ecdsa_resharing_session.go).
Both constructors build a tss.ReSharingParameters object containing the old committee (current share holders), the new committee (from NodeIDs), and both the old and new thresholds. The session determines whether the local node is part of the old committee, the new committee, or both, and initializes share data accordingly—existing shares are loaded from the KV store for old parties, while new parties begin with empty shares.
Handle the ResharingResultEvent
Subscribe to the completion event using OnResharingResult, which listens on the mpc.mpc_reshare_result.* queue (see lines 224-246 in pkg/client/client.go). The server publishes the result via handleReshareResult (lines 630-665 in [pkg/eventconsumer/event_consumer.go](https://github.com/fystack/mpcium/blob/master/pkg/eventconsumer/event_consumer.go#L630-L665)) after successfully storing the new share and updating the key metadata.
err := mpcClient.OnResharingResult(func(evt event.ResharingResultEvent) {
logger.Info("Resharing completed",
"walletID", evt.WalletID,
"pubKey", fmt.Sprintf("%x", evt.PubKey),
"newThreshold", evt.NewThreshold,
"version", evt.Version)
})
The ResharingResultEvent (defined in pkg/event/reshare.go) contains:
WalletID: The migrated wallet identifier.PubKey: The new public key in compressed byte format.NewThreshold: The updated signing threshold.Version: The incremented key-info version (atomic counter).ParticipantPeerIDs: The definitive list of new committee members.
After the resharing session completes, the server persists the new share in the KV store under a versioned key (walletID:version) and updates the KeyInfo record via keyinfo.Store.Save, ensuring subsequent signing operations use the refreshed committee and threshold.
Complete Migration Example
The following example demonstrates the full client-side flow, from configuration to result handling. This mirrors the official [examples/reshare/main.go](https://github.com/fystack/mpcium/blob/master/examples/reshare/main.go):
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"github.com/fystack/mpcium/pkg/client"
"github.com/fystack/mpcium/pkg/config"
"github.com/fystack/mpcium/pkg/event"
"github.com/fystack/mpcium/pkg/logger"
"github.com/fystack/mpcium/pkg/types"
"github.com/google/uuid"
"github.com/nats-io/nats.go"
"github.com/spf13/viper"
)
func main() {
// Initialise configuration and logger
config.InitViperConfig("")
logger.Init("dev", true)
// Connect to NATS
natsURL := viper.GetString("nats.url")
nc, err := nats.Connect(natsURL)
if err != nil {
logger.Fatal("NATS connection failed", err)
}
defer nc.Drain()
// Create local signer for the event initiator
localSigner, err := client.NewLocalSigner(
types.EventInitiatorKeyTypeEd25519,
client.LocalSignerOptions{KeyPath: "./event_initiator.key"},
)
if err != nil {
logger.Fatal("Failed to initialise signer", err)
}
// Build MPC client
mpc := client.NewMPCClient(client.Options{
NatsConn: nc,
Signer: localSigner,
})
// Subscribe to resharing results
if err := mpc.OnResharingResult(func(evt event.ResharingResultEvent) {
logger.Info("Resharing completed",
"walletID", evt.WalletID,
"pubKey", fmt.Sprintf("%x", evt.PubKey),
"newThreshold", evt.NewThreshold,
"version", evt.Version)
}); err != nil {
logger.Fatal("Failed to subscribe to resharing result", err)
}
// Create and send resharing request
resharingMsg := &types.ResharingMessage{
SessionID: uuid.NewString(),
WalletID: "c2a5c9d4-...",
NodeIDs: []string{"node-A-uuid", "node-B-uuid", "node-C-uuid"},
NewThreshold: 2,
KeyType: types.KeyTypeEd25519,
}
if err := mpc.Resharing(resharingMsg); err != nil {
logger.Fatal("Resharing failed", err)
}
fmt.Printf("Resharing request sent for wallet %s\n", resharingMsg.WalletID)
// Keep alive until interrupted
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
}
Summary
- Construct a
ResharingMessagewithSessionID,WalletID,NodeIDs,NewThreshold, andKeyTypeto define the migration target. - Sign and publish via
MPCClient.Resharing, which serializes the canonical payload and broadcasts to thempc.mpc_reshareNATS topic. - Listen for
ResharingResultEventthroughOnResharingResultto obtain the new public key, updated threshold, and incremented key version. - The server automatically manages TSS resharing sessions, versioning the new shares in the KV store and updating
KeyInfometadata atomically.
Frequently Asked Questions
What happens to the old shares after resharing completes?
The old shares remain in the KV store under their previous version keys but are no longer used for signing operations. The KeyInfo record is atomically updated to point to the new version, and the server begins using the new committee for all subsequent signature requests. Old shares can be garbage collected based on your retention policy.
Can a node be part of both the old and new committees during resharing?
Yes. The resharing sessions in eddsa_resharing_session.go and ecdsa_resharing_session.go determine committee membership using the isNewParty and isOldParty flags. A node can hold an existing share (old committee) while simultaneously receiving a new share (new committee), ensuring continuity of service during the migration.
How is the initiator signature validated during the resharing request?
The ResharingMessage includes a Signature field populated by MPCClient.Resharing using the configured Signer. The server validates this signature against the canonical payload produced by msg.Raw() to ensure the request originated from an authorized event initiator and was not tampered with during transit.
What is the relationship between threshold and the number of nodes?
The threshold t (stored as NewThreshold) defines the minimum number of shares required to produce a signature, where t+1 parties must cooperate. This value must satisfy t+1 ≤ len(NodeIDs) for the new committee. The server enforces this constraint when constructing tss.ReSharingParameters in the session constructors.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →