部署

  1. 编译部署

git clone https://github.com/fystack/mpcium.git
cd mpcium
source /etc/profile
go env -w GOPRIVATE=github.com/bcskill/*
go mod tidy
make

可用命令

  • mpcium:启动 MPCium 节点
  • mpcium-cli:用于对等方、身份和发起方配置的 CLI 实用程序
go build ./cmd/mpcium
go build ./cmd/mpcium-cli
  1. 启动NATS 和 Consul

cd mpcium
docker compose up -d

NATS:高性能、轻量级的 消息系统(消息队列 / 消息总线),主要用于服务之间的异步通信

Service A ───► NATS ───► Service B

​ ▲

​ └─► Service C(也可同时收到消息)

Consul:HashiCorp 开源的一个用于服务网络的工具,主要用于 服务发现、配置管理、健康检查,也支持 分布式 Key-Value 存储服务网格功能

[Service A] <---> [Consul Agent] <---> [Consul Server] <---> [Service B info]

  1. 部署节点

3.1 创建节点配置

cd ../
mkdir test-node
cd test-node
mpcium-cli generate-peers -n 3
cp ../mpcium/config.yaml.template ./config.yaml

编辑badger_password 为16,24,32 位密码

3.2 注册链节点

mpcium-cli register-peers

3.3 生成启动器

mpcium-cli generate-initiator

从生成的event_initiator.identity.json 中复制public_key, 更新到 config.yaml中的event_initiator_pubkey

3.4 创建目录

mkdir node{0..2}
for dir in node{0..2}; do cp config.yaml peers.json "$dir/"; done

3.5 创建身份

cd node0
mpcium-cli generate-identity --node node0
cd ../node1
mpcium-cli generate-identity --node node1
cd ../node2
mpcium-cli generate-identity --node node2
cd ../

cp node0/identity/node0_identity.json node1/identity/node0_identity.json
cp node0/identity/node0_identity.json node2/identity/node0_identity.json
cp node1/identity/node1_identity.json node0/identity/node1_identity.json
cp node1/identity/node1_identity.json node2/identity/node1_identity.json
cp node2/identity/node2_identity.json node0/identity/node2_identity.json
cp node2/identity/node2_identity.json node1/identity/node2_identity.json

3.6 启动节点

cd node0
mpcium start -n node0
cd node1
mpcium start -n node1
cd node2
mpcium start -n node2

实际测试

https://github.com/fystack/mpcium-client-ts

event_initiator.key

f81851f905abc51de2618ddb7f75dde2a422dd0f6f3abb0d51c850287ecac900

创建钱包

import { connect } from "nats";
import { KeygenSuccessEvent, MpciumClient } from "../src";
import { computeAddress, hexlify } from "ethers";
import base58 from "bs58";
import * as fs from "fs";
import * as path from "path";
import { v4 } from "uuid";

async function main() {
  const args = process.argv.slice(2);
  const nIndex = args.indexOf("-n");
  const walletCount = nIndex !== -1 ? parseInt(args[nIndex + 1]) || 1 : 1;

  const nc = await connect({ servers: "nats://127.0.0.1:4222" }).catch(
    (err) => {
      console.error(`Failed to connect to NATS: ${err.message}`);
      process.exit(1);
    }
  );
  console.log(`Connected to NATS at ${nc.getServer()}`);

  const mpcClient = await MpciumClient.create({
    nc: nc,
    keyPath: "./event_initiator.key",
    // password: "your-password-here",
  });

  const walletsPath = path.resolve("./wallets.json");
  let wallets: Record<string, KeygenSuccessEvent> = {};
  if (fs.existsSync(walletsPath)) {
    try {
      wallets = JSON.parse(fs.readFileSync(walletsPath, "utf8"));
    } catch (error) {
      console.warn(`Could not read wallets file: ${error.message}`);
    }
  }

  let remaining = walletCount;

  mpcClient.onWalletCreationResult((event: KeygenSuccessEvent) => {
    const timestamp = new Date().toISOString();
    console.log(`${timestamp} Received wallet creation result:`, event);

    if (event.eddsa_pub_key) {
      const pubKeyBytes = Buffer.from(event.eddsa_pub_key, "base64");
      const solanaAddress = base58.encode(pubKeyBytes);
      console.log(`Solana wallet address: ${solanaAddress}`);
    }

    if (event.ecdsa_pub_key) {
      const pubKeyBytes = Buffer.from(event.ecdsa_pub_key, "base64");
      const uncompressedKey =
        pubKeyBytes.length === 65
          ? pubKeyBytes
          : Buffer.concat([Buffer.from([0x04]), pubKeyBytes]);
      const ethAddress = computeAddress(hexlify(uncompressedKey));
      console.log(`Ethereum wallet address: ${ethAddress}`);
    }

    wallets[event.wallet_id] = event;
    fs.writeFileSync(walletsPath, JSON.stringify(wallets, null, 2));
    console.log(`Wallet saved to wallets.json with ID: ${event.wallet_id}`);

    remaining -= 1;
    if (remaining === 0) {
      console.log("All wallets generated.");
    }
  });

  try {
    for (let i = 0; i < walletCount; i++) {
      const timestamp = new Date().toISOString();
      const walletID = await mpcClient.createWallet(`${v4()}:${i}`);
      console.log(
        `${timestamp} CreateWallet sent #${
          i + 1
        }, awaiting result... walletID: ${walletID}`
      );
    }

    const shutdown = async () => {
      console.log("Cleaning up...");
      await mpcClient.cleanup();
      await nc.drain();
      process.exit(0);
    };

    process.on("SIGINT", shutdown);
  } catch (error) {
    console.error("Error:", error);
    await mpcClient.cleanup();
    await nc.drain();
    process.exit(1);
  }
}

main().catch(console.error);
/mpcium-client-ts# ts-node ./examples/generate.ts 
Connected to NATS at 127.0.0.1:4222
Subscribed to wallet creation results (consume mode)
  result_type: 'success',
  error_reason: '',
  error_code: ''
}
Solana wallet address: Cmroym1zhiv6Q3DaHSRQv3CJBFnBqm4dtLkYnsAqbQ2c
Ethereum wallet address: 0xef049bd27aCD860D7A157e492378bf54eE9671B9
Wallet saved to wallets.json with ID: b7b3cb5e-fbea-4785-86c7-f67c3562ddfc:0

签名发送交易

测试地址,忽略手续费不足问题

import { connect } from "nats";
import { MpciumClient, KeyType, SigningResultEvent } from "../src";
import { ethers } from "ethers";
import { SigningResultType } from "../src/types";
import * as fs from "fs";
import * as path from "path";

// The wallet ID should be provided via command line argument
const walletId = "b7b3cb5e-fbea-4785-86c7-f67c3562ddfc:0";

// Destination wallet to send ETH to
const DESTINATION_WALLET = "0x742d35Cc6634C0532925a3b844Bc454e4438f44e";

// Amount to send in ETH
const AMOUNT_TO_SEND = "0.0001"; // 0.001 ETH

// Function to load wallet from wallets.json
function loadWallet(walletId: string) {
  const walletsPath = path.resolve("./wallets.json");
  try {
    if (fs.existsSync(walletsPath)) {
      const wallets = JSON.parse(fs.readFileSync(walletsPath, "utf8"));
      if (wallets[walletId]) {
        return wallets[walletId];
      }
      throw new Error(`Wallet with ID ${walletId} not found in wallets.json`);
    } else {
      throw new Error("wallets.json file not found");
    }
  } catch (error) {
    console.error(`Failed to load wallet: ${error.message}`);
    process.exit(1);
  }
}

async function main() {
  console.log(`Using wallet ID: ${walletId}`);

  // First, establish NATS connection separately
  const nc = await connect({ servers: "nats://127.0.0.1:4222" }).catch(
    (err) => {
      console.error(`Failed to connect to NATS: ${err.message}`);
      process.exit(1);
    }
  );
  console.log(`Connected to NATS at ${nc.getServer()}`);

  // Create client with key path
  const mpcClient = await MpciumClient.create({
    nc: nc,
    keyPath: "./event_initiator.key",
    // password: "your-password-here", // Required for .age encrypted keys
  });

  try {
    // Connect to Ethereum testnet (Sepolia)
    const provider = new ethers.JsonRpcProvider(
      "https://eth-sepolia.public.blastapi.io"
    );
    console.log("Connected to Ethereum Sepolia testnet");

    // Get the wallet's public key/address
    const ethAddress = await getEthAddressForWallet(walletId);
    const fromAddress = ethAddress;

    console.log(`Sender account: ${fromAddress}`);
    console.log(`Destination account: ${DESTINATION_WALLET}`);
    console.log(`Amount to send: ${AMOUNT_TO_SEND} ETH`);

    // Get the current nonce for the sender address
    const nonce = await provider.getTransactionCount(fromAddress);

    // Get the current gas price
    const feeData = await provider.getFeeData();

    console.log("feeData:", feeData);

    // Create an Ethereum transaction
    const transaction = {
      to: DESTINATION_WALLET,
      value: ethers.parseEther(AMOUNT_TO_SEND),
      gasLimit: 21000, // Standard gas limit for ETH transfers
      maxFeePerGas: feeData.maxFeePerGas,
      maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
      nonce: nonce,
      type: 2, // EIP-1559 transaction
      chainId: 11155111, // Sepolia chain ID
    };

    // Calculate the transaction hash (this is what needs to be signed)
    const unsignedTx = ethers.Transaction.from(transaction);
    const txHash = unsignedTx.unsignedHash;
    const txHashHex = txHash.substring(2); // Remove '0x' prefix

    console.log(`Transaction hash: ${txHash}`);

    // Subscribe to signing results
    let signatureReceived = false;
    mpcClient.onSignResult((event: SigningResultEvent) => {
      console.log("Received signing result:", event);
      signatureReceived = true;

      if (event.result_type === SigningResultType.Success) {
        processSuccessfulSignature(event);
      } else {
        console.error(`Signing failed: ${event.error_message}`);
      }
    });

    // Process a successful signature
    function processSuccessfulSignature(event: SigningResultEvent) {
      try {
        // For ECDSA Ethereum signatures, we need the r, s, and v (recovery) values
        if (!event.r || !event.s || event.signature_recovery === null) {
          console.error("Missing signature components in result:", event);
          return;
        }

        // Convert from base64 to hex strings
        const r = "0x" + Buffer.from(event.r, "base64").toString("hex");
        const s = "0x" + Buffer.from(event.s, "base64").toString("hex");

        // Decode signature_recovery from base64 to a number
        const recoveryBuffer = Buffer.from(event.signature_recovery, "base64");
        const v = recoveryBuffer[0]; // Get the first byte as the recovery value

        console.log(`Signature components - r: ${r}, s: ${s}, v: ${v}`);

        // Create a signed transaction
        const signedTx = ethers.Transaction.from({
          ...transaction,
          signature: { r, s, v },
        });

        // Verify signature
        const recoveredAddress = signedTx.from;
        console.log(`Recovered signer: ${recoveredAddress}`);
        if (!recoveredAddress) {
          console.error("Signature verification failed!");
          return;
        }

        if (recoveredAddress.toLowerCase() !== fromAddress.toLowerCase()) {
          console.error(
            "Signature verification failed! Addresses don't match."
          );
          return;
        }

        console.log("Signature verification successful!");
        broadcastTransaction(signedTx.serialized);
      } catch (error) {
        console.error("Error processing signature:", error);
        if (error instanceof Error) {
          console.error(error.stack);
        }
      }
    }

    // Broadcast the transaction to the network
    function broadcastTransaction(signedTxHex: string) {
      provider
        .broadcastTransaction(signedTxHex)
        .then((tx) => {
          console.log(`Transaction sent! Transaction hash: ${tx.hash}`);
          console.log(
            `View transaction: https://sepolia.etherscan.io/tx/${tx.hash}`
          );
        })
        .catch((err) => {
          console.error("Error broadcasting transaction:", err);
        });
    }

    // Send the transaction hash for signing, not the serialized transaction
    const txId = await mpcClient.signTransaction({
      walletId: walletId,
      keyType: KeyType.Secp256k1,
      networkInternalCode: "ethereum:sepolia",
      tx: Buffer.from(txHashHex, "hex").toString("base64"), // Convert hex to base64
    });

    console.log(`Signing request sent with txID: ${txId}`);

    // Wait for the result
    await new Promise((resolve) => {
      const checkInterval = setInterval(() => {
        if (signatureReceived) {
          clearInterval(checkInterval);
          resolve(null);
        }
      }, 1000);
    });

    // Keep the process running to allow time for transaction confirmation
    await new Promise((resolve) => setTimeout(resolve, 5000));

    console.log("Cleaning up...");
    await mpcClient.cleanup();
    await nc.drain();
  } catch (error) {
    console.error("Error:", error);
    await mpcClient.cleanup();
    await nc.drain();
    process.exit(1);
  }
}

// Helper function to get a wallet's Ethereum address
async function getEthAddressForWallet(walletId: string): Promise<string> {
  // Load wallet from wallets.json
  const wallet = loadWallet(walletId);

  if (wallet && wallet.ecdsa_pub_key) {
    // Convert base64 public key to Ethereum address
    const pubKeyBuffer = Buffer.from(wallet.ecdsa_pub_key, "base64");
    // Convert the buffer to hex string with "0x" prefix
    const pubKeyHex = "0x" + pubKeyBuffer.toString("hex");
    // Ethereum addresses are derived from the keccak256 hash of the uncompressed public key
    const address = ethers.computeAddress(pubKeyHex);
    return address;
  }

  throw new Error(`Wallet with ID ${walletId} has no ECDSA public key`);
}

// Run the example
main().catch(console.error);
/mpcium-client-ts# ts-node ./examples/sign-eth.ts
Using wallet ID: b7b3cb5e-fbea-4785-86c7-f67c3562ddfc:0
Connected to NATS at 127.0.0.1:4222
Connected to Ethereum Sepolia testnet
Sender account: 0xef049bd27aCD860D7A157e492378bf54eE9671B9
Destination account: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e
Amount to send: 0.0001 ETH
feeData: FeeData {
  gasPrice: 14533302n,
  maxFeePerGas: 28066604n,
  maxPriorityFeePerGas: 1000000n
}
Transaction hash: 0x9c7835d42ef9643c110d5ddf038fba76a580c93a650242a1d66f5d74333bf3c7
Subscribed to signing results (consume mode)
SignTransaction request sent via JetStream for txID: c8354d11-aba5-46e8-88c2-c846b1fdac51
Signing request sent with txID: c8354d11-aba5-46e8-88c2-c846b1fdac51
Received signing result: {
  result_type: 'success',
  error_code: '',
  error_reason: '',
  is_timeout: false,
  network_internal_code: 'ethereum:sepolia',
  wallet_id: 'b7b3cb5e-fbea-4785-86c7-f67c3562ddfc:0',
  tx_id: 'c8354d11-aba5-46e8-88c2-c846b1fdac51',
  r: 'pylQvdMr+Z7N+9ADAdruVN7MTqysS0LNvVERgEx4Ktw=',
  s: 'ZA4LMVBf3Di9E35C3vph3ZWUvH4GHNnRTBb7aKYHFuc=',
  signature_recovery: 'AA==',
  signature: null
}
Signature components - r: 0xa72950bdd32bf99ecdfbd00301daee54decc4eacac4b42cdbd5111804c782adc, s: 0x640e0b31505fdc38bd137e42defa61dd9594bc7e061cd9d14c16fb68a60716e7, v: 0
Recovered signer: 0xef049bd27aCD860D7A157e492378bf54eE9671B9
Signature verification successful!
Error broadcasting transaction: Error: insufficient funds for intrinsic transaction cost (transaction="0x02f87283aa36a780830f42408401ac432c82520894742d35cc6634c0532925a3b844bc454e4438f44e865af3107a400080c080a0a72950bdd32bf99ecdfbd00
301daee54decc4eacac4b42cdbd5111804c782adca0640e0b31505fdc38bd137e42defa61dd9594bc7e061cd9d14c16fb68a60716e7", info={ "error": { "code": -32003, "message": "insufficient funds for gas * price + value: have 0 want 100589398684000" } }, code=INSUFFICIENT_FUNDS, version=6.13.7)
    at makeError (/mnt/d/github/bcskill/code/MPC/mpcium-client-ts/node_modules/ethers/src.ts/utils/errors.ts:694:21)
    at JsonRpcProvider.getRpcError (/mnt/d/github/bcskill/code/MPC/mpcium-client-ts/node_modules/ethers/src.ts/providers/provider-jsonrpc.ts:1025:33)
    at /mnt/d/github/bcskill/code/MPC/mpcium-client-ts/node_modules/ethers/src.ts/providers/provider-jsonrpc.ts:563:45
    at processTicksAndRejections (node:internal/process/task_queues:95:5) {
  code: 'INSUFFICIENT_FUNDS',
  transaction: '0x02f87283aa36a780830f42408401ac432c82520894742d35cc6634c0532925a3b844bc454e4438f44e865af3107a400080c080a0a72950bdd32bf99ecdfbd00301daee54decc4eacac4b42cdbd5111804c782adca0640e0b31505fdc38bd137e42defa61dd9594bc7e061cd9d14c16fb68a60716e7',
  info: {
    error: {
      code: -32003,
      message: 'insufficient funds for gas * price + value: have 0 want 100589398684000'
    }
  },
  shortMessage: 'insufficient funds for intrinsic transaction cost'
}
Cleaning up...
Cleaned up all subscriptions

多钱包测试

Subscribed to wallet creation results (consume mode)
CreateWallet request sent via JetStream for wallet: bbcd5019-2e7d-46aa-a1e8-5bc14371bc57:0
2025-07-14T12:57:13.424Z CreateWallet sent #1, awaiting result... walletID: bbcd5019-2e7d-46aa-a1e8-5bc14371bc57:0
2025-07-14T12:57:17.181Z Received wallet creation result: {
  wallet_id: 'bbcd5019-2e7d-46aa-a1e8-5bc14371bc57:0',
  ecdsa_pub_key: '2+ZZj1XCFR/OGWk9g1EMYZFyXfDNaNHCf+egn1oTo6DvR6kQ6tOBHK0oZkCL6QNsjmfi14tQN71W5n7OOXLSBg==',
  eddsa_pub_key: 's8rOVjh6dMwmpXqRDRYSyOBenY/uaHBOUvumU2QFuBE=',
  result_type: 'success',
  error_reason: '',
  error_code: ''
}
Solana wallet address: D6qLkdfZLXjHhSLAJ38UHjkYmBHU4Ac8S1oQKUDJZ2Gx
Ethereum wallet address: 0xCfcec2Beed19a40A663494396fB73042f2049dB1
Wallet saved to wallets.json with ID: bbcd5019-2e7d-46aa-a1e8-5bc14371bc57:0

测试通过

异常测试

使用相同walletID

WRN Keygen session error error="Key already exists: 81418adb-fb32-4e17-9b85-8ae207aa9cb8:0" context="Failed to create ECDSA key generation session" errorCode=ERROR_KEY_ALREADY_EXISTS walletID=81418adb-fb32-4e17-9b85-8ae207aa9cb8:0

https://github.com/fystack/mpcium/issues/56

常见问题

创建地址无返回

问题排查

nats consumer info
> Select a Stream mpc
> Select a Consumer mpc_keygen_result

查看 Waiting Pulls: 3 of maximum 512 当前有几个客户端连接

移除所有监听列表

nats consumer rm mpc mpc_keygen_result

标签: none

相关文章推荐

添加新评论,含*的栏目为必填