MPCium 实测
部署
-
编译部署
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
-
启动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]
-
部署节点
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版权属于:区块链中文技术社区 / 转载原创者
本文链接:https://www.bcskill.com/index.php/archives/2443.html
相关技术文章仅限于相关区块链底层技术研究,禁止用于非法用途,后果自负!本站严格遵守一切相关法律政策!