您正在查看: Ethereum-新手教程 分类下的文章

MPCium 实测

部署

  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

BNB / BSC 节点中的 BLS 密钥:原理、作用与架构升级解析

随着 BNB Chain(包含 BNB Beacon Chain 与 BNB Smart Chain,简称 BSC)持续演进,其共识层经历了重要升级:从早期基于 ECDSA 的签名机制,过渡到采用 BLS(Boneh–Lynn–Shacham)聚合签名的高性能 PoSA 共识架构。

在新的节点体系中,每个验证者除了传统的 ECDSA 私钥之外,还必须生成并配置一套 BLS 密钥。本文将系统性介绍:

  • BLS 是什么
  • 为什么 BNB/BSC 要使用 BLS
  • BLS 密钥在共识中的具体作用
  • 与 ECDSA 的对比
  • 架构升级带来的性能与安全变化

一、为什么 BNB/BSC 需要 BLS?

BSC 早期使用的是 PoSA(Proof of Staked Authority)+ ECDSA 签名。
这种方式在功能上可用,但存在两个核心问题:

  1. 签名无法聚合,每个区块需要存储和验证大量签名
  2. 验证者数量和区块大小难以进一步扩展

随着 BNB Chain 融合 Beacon Chain 架构、提升 TPS 的需求,BNB 引入了基于 BLS 的聚合签名技术,使共识开销更低、性能更高。


二、BLS 的核心能力

BLS(Boneh–Lynn–Shacham)最重要的特点包括以下三点。

1. 支持签名聚合(Aggregate Signature)

多个验证者的签名可以数学地合并为一个签名:

  • 100 个签名
    最终只需要 1 个聚合签名

区块头体积极大缩小,使出块更快、网络传输更轻。

2. 原生支持阈值签名(Threshold Signature)

可以实现多数验证者共同生成一个有效签名:

  • 超过 2/3 的验证者签名
    自动生成一个可验证的单一签名

无需额外的多重签名协议。

3. 验证速度快、可扩展性强

在验证区块时:

  • 节点只需验证一个聚合签名
  • 而不需要验证每个验证者的单独签名

这使得共识的可扩展性显著提升。


三、BLS 私钥在 BNB/BSC 中的作用

在新的 BNB Chain 共识架构中,BLS 密钥承担的是“共识身份”和“投票签名”的职责,而不是交易签名。

它主要有以下三个作用。


1. 作为验证者的共识层身份密钥

在共识过程(prevote、precommit、aggregate)中,验证者使用 BLS 私钥对投票进行签名。
链上保存的是验证者的 BLS 公钥,用于:

  • 验证投票来源
  • 验证者轮换
  • 共识身份验证

可以理解为验证者的“共识身份证”。


2. 生成区块的 BLS 聚合签名

每个区块高度都需要验证者投票确认。
使用 BLS 后:

  • 所有验证者的投票可以聚合为一个签名
  • 区块头中只写入一个 BLS 聚合签名
  • 节点验证速度更快,区块体积更小

这是 BNB Chain 性能提升的关键机制。


3. 支持安全的阈值共识机制

PoSA 要求超过三分之二的验证者同意区块才能最终确认。
BLS 天生支持阈值验证,并提供:

  • 更简单的投票验证逻辑
  • 更强的防伪造能力
  • 更安全的验证者切换

让整个共识层更安全可靠。


四、为什么不是使用 ECDSA?(对比)

能力 ECDSA BLS
签名聚合 不支持 支持
阈值签名 不支持,需要外部协议 原生支持
多验证者共识性能 一般 高效
区块头大小
扩展性 显著受限
用途 交易签名 共识投票签名

结论:
交易层仍然使用 ECDSA(钱包、合约交互等)。
共识层使用 BLS 才能获得高性能和更佳的扩展性。


五、BNB/BSC 架构升级后的变化

随着 BLS 的引入,BNB Chain 获得了以下提升。

1. 共识性能显著提升

减少共识消息体积、减少签名验证次数,使:

  • 区块时间更稳定
  • TPS 上限提升
  • 网络带宽消耗更低

2. 验证者数量可扩展

BSC 早期固定 21 个验证者,使用 BLS 后可以支持更多验证者参与,而不用担心过高的签名成本。


3. 更安全的验证者管理

BLS 将验证者的共识身份抽象为统一的公钥,使身份管理更彻底、更可控。


六、BNB/BSC 节点中的两套密钥

BNB/BSC 节点实际上使用两套密钥体系,用途完全不同。

1. ECDSA Key(交易密钥)

  • 用于链上交易签名
  • secp256k1 曲线
  • 与以太坊、MetaMask 完全一致

2. BLS Key(共识密钥)

  • 仅验证者节点使用
  • 只用于共识投票
  • 不用于交易
  • 不出现在任何钱包中

这两套密钥互不影响,各司其职。


七、总结

BLS 密钥在 BNB/BSC 中主要用于验证者共识层的投票与聚合签名,通过支持多签名聚合、阈值验证和快速校验,显著增强了区块确认速度、网络扩展性与整体安全性。它不用于交易,只用于共识层,是 BNB Chain 迈向高性能架构的重要基础组件。

BNB BSC 合约关系与方法梳理

1. 合约架构概述

核心合约

  • System.sol / SystemV2.sol:基础合约,定义系统常量、地址和修饰符
  • BSCValidatorSet.sol:验证者集合管理
  • StakeHub.sol:验证者质押管理
  • GovHub.sol:治理参数更新
  • SlashIndicator.sol:验证者行为监控与惩罚
  • SystemReward.sol:系统奖励管理

合约关系图

2. 各合约主要功能与方法

2.1 System.sol / SystemV2.sol

  • 功能:定义系统常量、地址和修饰符
  • 核心常量:各系统合约地址(VALIDATOR_CONTRACT_ADDR、SLASH_CONTRACT_ADDR等)
  • 修饰符:onlyCoinbase、onlyZeroGasPrice、onlyGov等

2.2 BSCValidatorSet.sol

  • 功能:管理验证者集合,处理惩罚和维护
  • 对外方法
    • initializeFromGenesis:从创世块初始化验证者集合
    • updateValidatorSetV2:更新验证者集合
    • deposit:收集交易费用并分配
    • distributeFinalityReward:分配最终性奖励
    • getLivingValidators:获取活跃验证者
    • getMiningValidators:获取挖矿验证者
    • enterMaintenance:进入维护模式
    • exitMaintenance:退出维护模式
  • 被链代码调用的方法
    • updateValidatorSetV2:由共识引擎调用
    • deposit:由区块生产者调用
    • distributeFinalityReward:由区块生产者调用

2.3 StakeHub.sol

  • 功能:验证者质押管理,包括创建验证者、委托等
  • 对外方法
    • createValidator:创建验证者
    • editConsensusAddress:编辑共识地址
    • editCommissionRate:编辑佣金率
    • editDescription:编辑描述
    • editVoteAddress:编辑投票地址
    • unjail:解锁验证者
    • delegate:委托
    • undelegate:取消委托
    • redelegate:重新委托
    • claim:领取取消委托的BNB
  • 被链代码调用的方法
    • distributeReward:由验证者合约调用
    • downtimeSlash:由惩罚合约调用
    • maliciousVoteSlash:由惩罚合约调用
    • doubleSignSlash:由惩罚合约调用

2.4 GovHub.sol

  • 功能:治理参数更新
  • 对外方法
    • updateParam:更新参数,只能由治理时间锁合约调用

2.5 SlashIndicator.sol

  • 功能:监控验证者行为,处理惩罚
  • 对外方法
    • slash:惩罚未出块的验证者
    • clean:清理惩罚记录
    • submitFinalityViolationEvidence:提交最终性违规证据
    • submitDoubleSignEvidence:提交双重签名证据
  • 被链代码调用的方法
    • slash:由区块生产者调用
    • clean:由验证者合约调用

2.6 SystemReward.sol

  • 功能:系统奖励管理
  • 对外方法
    • claimRewards:领取奖励,只能由操作员调用
  • 被链代码调用的方法
    • claimRewards:由轻客户端、激励合约等调用

3. 合约调用关系

3.1 链代码调用合约

  • 共识引擎 → BSCValidatorSet.updateValidatorSetV2
  • 区块生产者 → BSCValidatorSet.deposit
  • 区块生产者 → BSCValidatorSet.distributeFinalityReward
  • 区块生产者 → SlashIndicator.slash

3.2 合约间调用

  • BSCValidatorSet → StakeHub.distributeReward
  • BSCValidatorSet → SlashIndicator.clean
  • SlashIndicator → BSCValidatorSet.felony
  • SlashIndicator → BSCValidatorSet.misdemeanor
  • SlashIndicator → StakeHub.downtimeSlash
  • SlashIndicator → StakeHub.maliciousVoteSlash
  • SlashIndicator → StakeHub.doubleSignSlash
  • GovHub → BSCValidatorSet.updateParam
  • GovHub → StakeHub.updateParam
  • GovHub → SlashIndicator.updateParam
  • GovHub → SystemReward.updateParam

4. 接口定义

4.1 0.6.x 接口

  • IBSCValidatorSet:验证者集合接口
  • ISlashIndicator:惩罚接口
  • IStakeHub:质押管理接口
  • IParamSubscriber:参数订阅接口
  • ISystemReward:系统奖励接口

4.2 0.8.x 接口

  • IBSCValidatorSet:验证者集合接口
  • IGovToken:治理代币接口
  • IStakeCredit:质押信用接口
  • IStakeHub:质押管理接口

5. 系统流程

5.1 验证者创建流程

  1. 用户调用 StakeHub.createValidator 创建验证者
  2. StakeHub 部署 StakeCredit 代理合约
  3. StakeHub 记录验证者信息
  4. 用户可以调用 delegate 进行委托

5.2 验证者惩罚流程

  1. 区块生产者调用 SlashIndicator.slash 惩罚未出块的验证者
  2. SlashIndicator 检查惩罚阈值
  3. 如果达到 misdemeanor 阈值,调用 BSCValidatorSet.misdemeanor
  4. 如果达到 felony 阈值,调用 BSCValidatorSet.felony 和 StakeHub.downtimeSlash

5.3 参数更新流程

  1. 治理提案通过后,GovernorTimelock 调用 GovHub.updateParam
  2. GovHub 调用目标合约的 updateParam 方法
  3. 目标合约更新参数并触发事件

6. 总结

该合约系统是一个完整的区块链验证者管理和治理系统,主要包括:

  • 验证者管理:BSCValidatorSet 负责验证者集合的更新和维护
  • 质押管理:StakeHub 负责验证者的质押、委托等操作
  • 惩罚机制:SlashIndicator 负责监控验证者行为并处理惩罚
  • 治理系统:GovHub 负责参数更新
  • 奖励管理:SystemReward 负责系统奖励的分配

合约之间通过明确的接口进行交互,形成了一个完整的生态系统,确保区块链网络的安全和稳定运行。

对比 BNB Chain(BSC)旧版与新版 genesis-template.json:从 Parlia 到多硬分叉时代的全面升级

BNB Chain(BSC)近两年进行了大幅结构升级,特别是在 genesis-template.json 中,引入了多版本硬分叉、时间激活机制、Blob(EIP-4844)、EIP-1559、Deposit 接口等框架性变更,使其从传统 “Parlia 简易模式” 升级为与以太坊主网高度兼容的现代化架构。

本文将对比旧版与新版 BSC genesis-template.json 的结构变化,并解释每个字段设计背后的技术原因。


1. 旧版 BSC Genesis —— 只有 Parlia 配置的简易模式

早期版本的 BSC(包括 Mainnet 与 Testnet 很长时间)仅在 genesis 中包含 Parlia 共识参数:

"parlia": {
  "period": 3,
  "epoch": 200
}

1.1 字段含义

字段 作用
period 出块间隔(秒),默认 3 秒
epoch validator 切换周期(多少区块重新选举),默认 200

1.2 特点

  • 结构简单
  • 没有 EIP-1559
  • 没有 EIP-4844(Blob)
  • 没有时间控制的 Hardfork
  • 与现代以太坊协议差距越来越大

旧版 BSC genesis 的核心只有 Parlia 配置,因此功能扩展能力有限。


2. 新版 BSC Genesis —— 多硬分叉 + 时间激活 + Blob 时代

BNB Chain 在 2024–2025 年陆续引入了大量升级(Feynman、Haber、Pascal、Prague、Lorentz…),genesis 文件结构变得高度现代化:

"feynmanFixTime": 0,
"cancunTime": 0,
"haberTime": 0,
"haberFixTime": 0,
"bohrTime": 0,
"pascalTime": 0,
"pragueTime": 0,
"lorentzTime": 0,

"depositContractAddress": "0x0000000000000000000000000000000000000000",

"parlia": {},

"blobSchedule": {
  "cancun": {
    "target": 3,
    "max": 6,
    "baseFeeUpdateFraction": 3338477
  },
  "prague": {
    "target": 3,
    "max": 6,
    "baseFeeUpdateFraction": 3338477
  }
}

下面逐项解析这些字段。


3. 新增字段解析(硬分叉、升级机制、Blob)

3.1 时间激活(Timestamp-based)硬分叉

新版 BSC 使用 Timestamp-based Hardfork 与以太坊主网保持一致。

字段 说明
feynmanFixTime Feynman 修复分叉激活时间
cancunTime Cancun 升级激活时间(支持 EIP-4844 Blob)
haberTime Haber 升级
haberFixTime Haber 修复版
bohrTime Bohr 升级
pascalTime Pascal 升级
pragueTime Prague 升级
lorentzTime Lorentz 升级

统一规则:
设置为 0 表示链启动后立即激活所有功能
→ 适合私链 / DevNet / 测试链
→ 公链通常使用具体 Unix 时间戳

从此 BSC 不再使用区块高度(blockNumber)触发升级,而全面迁移到 ETH 的时间触发机制,升级可控性更强。


3.2 parlia 变成空对象

旧版的:

"parlia": {
  "period": 3,
  "epoch": 200
}

新版变成:

"parlia": {}

这是因为:

✔ 新版 BSC 内置默认值

  • period = 3
  • epoch = 200

如果需要自定义,可以继续写入:

"parlia": {
  "period": 1,
  "epoch": 200
}

3.3 EIP-4844 Blob 交易:blobSchedule

BSC 新版开始兼容以太坊 Cancun 升级(EIP-4844),新增 Blob Gas 调整参数:

"blobSchedule": {
  "cancun": {
    "target": 3,
    "max": 6,
    "baseFeeUpdateFraction": 3338477
  },
  "prague": {
    "target": 3,
    "max": 6,
    "baseFeeUpdateFraction": 3338477
  }
}
字段 说明
target 每个块的目标 Blob 数量
max 每块最大 blob 数量
baseFeeUpdateFraction Blob baseFee 调整参数

这是与 ETH 主网完全一致的 Blob Gas 定价模型,用于 Rollup / 数据可用性(DA)场景。


3.4 depositContractAddress

"depositContractAddress": "0x0000000000000000000000000000000000000000"

目前 BSC 仍是 PoSA(并非 ETH PoS),但这一字段用于兼容未来可能的信标链接口,是预留字段。


4. 对比总结:从「简单模型」到「现代以太坊兼容链」

项目 旧版 BSC 新版 BSC
共识 Parlia Parlia(默认配置)
升级机制 blockNumber 激活 timestamp 激活(与 ETH 主网一致)
EIP-1559 ❌ 不支持 ✔ 支持
EIP-4844 Blob ❌ 不支持 ✔ 支持
硬分叉版本 基本无 Feynman / Haber / Pascal / Prague / Lorentz
genesis 结构 极简 高度模块化
可扩展性 较弱 强,接近 ETH 主网协议栈

新版 BSC 通过一套统一的时间激活 hardfork 机制,使其升级模型、blob gas 模型、EIP 兼容性全面向以太坊主网对齐。


5. 为什么 BSC 必须升级?

需求 原因
兼容 ETH 主网升级(Cancun/Prague) 保持生态一致性
引入 Blob(EIP-4844) 提升 DA 能力、支持 L2
统一升级机制 简化多版本支持
为未来 PoS 预留功能 兼容 depositContractAddress
拓展链能力 支持更现代的 Gas 模型与交易类型

因此新版 genesis 是一个从 ETH 主网演化而来的高度可扩展结构。

doesn't look like an ERC 1967 proxy with a logic contract addres

背景

使用 upgrades.erc1967.getImplementationAddress 来获取通过 upgrades.deployProxy 部署的合约的逻辑合约地址(implementation address)

测试部署脚本

const [deployer] = await ethers.getSigners();
    console.log("Deployer:", await deployer.getAddress())
    const commonBridgeFactory = await ethers.getContractFactory("CommonBridge", deployer);
    let commonBridgeProxyContract;
    if (deployOutput.commonBridgeProxyContract === undefined || deployOutput.commonBridgeProxyContract === '') {
        commonBridgeProxyContract = await upgrades.deployProxy(
            commonBridgeFactory,
            [
                process.env.DOMAIN_ID,
                process.env.SUPER_ADMIN_ADDRESS,
                process.env.BRIDGE_FEE_ADDRESS
            ],
            {
                constructorArgs: [],
                unsafeAllow: ['constructor', 'state-variable-immutable'],
            });
        console.log('tx hash:', commonBridgeProxyContract.deploymentTransaction().hash);
    } else {
        commonBridgeProxyContract = commonBridgeFactory.attach(deployOutput.commonBridgeProxyContract);
    }

    console.log('commonBridgeProxyContract deployed to:', commonBridgeProxyContract.target);
    await sleep(6000);
    const commonBridgeImpl = await upgrades.erc1967.getImplementationAddress(commonBridgeProxyContract.target);
    console.log('commonBridgeProxyContract implementation deployed to:', commonBridgeImpl);

部署超时

Error: Timed out waiting for implementation contract deployment to address 0x5393... with transaction 0x9492...

解决:增加超时和轮询设置

...
commonBridgeProxyContract = await upgrades.deployProxy(
    commonBridgeFactory,
    [
        process.env.DOMAIN_ID,
        process.env.SUPER_ADMIN_ADDRESS,
        process.env.BRIDGE_FEE_ADDRESS
    ],
    {
        constructorArgs: [],
        unsafeAllow: ['constructor', 'state-variable-immutable'],
        timeout: 300000, // 5分钟
        pollingInterval: 5000, // 每5秒轮询一次
    });
    console.log('tx hash:', commonBridgeProxyContract.deploymentTransaction().hash);
    ...

不符合 ERC1967 标准

使用 upgrades.erc1967.getImplementationAddress 来获取通过 upgrades.deployProxy 部署的合约的逻辑合约地址(implementation address),结果报:部署地址不符合 ERC1967 标准的代理合约(Proxy)

Error: Contract at 0xffa5f716D890CFd4f9D709A40d6EB7Fb19fC43B4 doesn't look like an ERC 1967 proxy with a logic contract address
const implSlot = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc';
const proxyAddress = '0xffa5f716D890CFd4f9D709A40d6EB7Fb19fC43B4';

const storage = await ethers.provider.getStorage(proxyAddress, implSlot);
console.log("Raw implementation slot:", storage);

const implAddress = '0x' + storage.slice(-40);
console.log("Implementation address:", implAddress);

const implAddress1 = await upgrades.erc1967.getImplementationAddress(proxyAddress);
console.log("Implementation address1:", implAddress1);

如果返回的 implStorage 是全零(0x000...),说明实现地址确实没有写进去,可能部署时失败或未初始化。
如果你用其他方式部署 Proxy(比如自己 new TransparentUpgradeableProxy,而不是用 upgrades.deployProxy()),OpenZeppelin 插件就不能保证你用了它支持的初始化逻辑。

解决: 是因为网络延迟原因,增加延迟时间

console.log('commonBridgeProxyContract deployed to:', commonBridgeProxyContract.target);
await sleep(20000); // 提高延迟间隔
const commonBridgeImpl = await upgrades.erc1967.getImplementationAddress(commonBridgeProxyContract.target);
console.log('commonBridgeProxyContract implementation deployed to:', commonBridgeImpl);

最终版本

const [deployer] = await ethers.getSigners();
    console.log("Deployer:", await deployer.getAddress())
    const commonBridgeFactory = await ethers.getContractFactory("CommonBridge", deployer);
    let commonBridgeProxyContract;
    if (deployOutput.commonBridgeProxyContract === undefined || deployOutput.commonBridgeProxyContract === '') {
        commonBridgeProxyContract = await upgrades.deployProxy(
            commonBridgeFactory,
            [
                process.env.DOMAIN_ID,
                process.env.SUPER_ADMIN_ADDRESS,
                process.env.BRIDGE_FEE_ADDRESS
            ],
            {
                constructorArgs: [],
                unsafeAllow: ['constructor', 'state-variable-immutable'],
                timeout: 300000,
                pollingInterval: 5000,
            });
        console.log('tx hash:', commonBridgeProxyContract.deploymentTransaction().hash);
    } else {
        commonBridgeProxyContract = commonBridgeFactory.attach(deployOutput.commonBridgeProxyContract);
    }

    console.log('commonBridgeProxyContract deployed to:', commonBridgeProxyContract.target);
    await sleep(20000);
    const commonBridgeImpl = await upgrades.erc1967.getImplementationAddress(commonBridgeProxyContract.target);
    console.log('commonBridgeProxyContract implementation deployed to:', commonBridgeImpl);