多重签名账户

多重签名(multisig)账户是一种智能账户,需要多个授权签名者批准操作才能执行。与由单个私钥控制的传统账户不同,多重签名将控制权分配给多个参与方,从而消除单点故障。例如,一个 2-of-3 多重签名需要来自 3 个可能的签名者中至少 2 个的签名。

Safe (之前称为 Gnosis Safe) 这样的流行实现已经成为保护有价值资产的标准。多重签名通过集体授权、所有权和阈值的可定制控制以及在不更改账户地址的情况下轮换签名者的能力来提供增强的安全性。

超越标准签名验证

正如在 账户部分 中讨论的那样,智能合约验证签名的标准方法是 ERC-1271,它定义了一个 isValidSignature(hash, signature)。但是,它在两个重要方面受到限制:

  1. 它假设签名者具有 EVM 地址

  2. 它将签名者视为一个单一身份

当实现多重签名账户时,这会变得有问题,其中:

  • 您可能希望使用没有 EVM 地址的签名者(例如来自硬件设备的密钥)

  • 需要单独验证每个签名者,而不是将其视为一个集体身份

  • 您需要一个阈值系统来确定何时存在足够的有效签名

SignatureChecker 库可用于验证 EOA 和 ERC-1271 签名,但它并非设计用于更复杂的安排,例如基于阈值的多重签名。

ERC-7913 签名者

ERC-7913 扩展了签名者表示的概念,以包括没有 EVM 地址的密钥,从而解决了这一限制。 OpenZeppelin 通过三个合约实现此标准:

SignerERC7913

SignerERC7913 合约允许单个 ERC-7913 格式的签名者控制一个账户。签名者表示为一个 bytes 对象,该对象连接一个验证者地址和一个密钥:verifier || key

// contracts/MyAccountERC7913.sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.24;

import {Account} from "@openzeppelin/community-contracts/account/Account.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
import {ERC7739} from "@openzeppelin/community-contracts/utils/cryptography/signers/ERC7739.sol";
import {ERC7821} from "@openzeppelin/community-contracts/account/extensions/ERC7821.sol";
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {SignerERC7913} from "@openzeppelin/community-contracts/utils/cryptography/signers/SignerERC7913.sol";

contract MyAccountERC7913 is Account, SignerERC7913, ERC7739, ERC7821, ERC721Holder, ERC1155Holder, Initializable {
    constructor() EIP712("MyAccount7913", "1") {}

    function initialize(bytes memory signer) public initializer {
        _setSigner(signer);
    }

    function setSigner(bytes memory signer) public onlyEntryPointOrSelf {
        _setSigner(signer);
    }

    /// @dev 允许入口点作为授权的执行者。
    function _erc7821AuthorizedExecutor(
        address caller,
        bytes32 mode,
        bytes calldata executionData
    ) internal view virtual override returns (bool) {
        return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
    }
}
如果账户未初始化,可能会使其无法使用,因为没有公钥与其关联。

MultiSignerERC7913

MultiSignerERC7913 合约扩展了这个概念,以支持具有基于阈值的签名验证系统的多个签名者。

// contracts/MyAccountMultiSigner.sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

import {Account} from "@openzeppelin/community-contracts/account/Account.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
import {ERC7739} from "@openzeppelin/community-contracts/utils/cryptography/signers/ERC7739.sol";
import {ERC7821} from "@openzeppelin/community-contracts/account/extensions/ERC7821.sol";
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {MultiSignerERC7913} from "@openzeppelin/community-contracts/utils/cryptography/signers/MultiSignerERC7913.sol";

contract MyAccountMultiSigner is
    Account,
    MultiSignerERC7913,
    ERC7739,
    ERC7821,
    ERC721Holder,
    ERC1155Holder,
    Initializable
{
    constructor() EIP712("MyAccountMultiSigner", "1") {}

    function initialize(bytes[] memory signers, uint256 threshold) public initializer {
        _addSigners(signers);
        _setThreshold(threshold);
    }

    function addSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
        _addSigners(signers);
    }

    function removeSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
        _removeSigners(signers);
    }

    function setThreshold(uint256 threshold) public onlyEntryPointOrSelf {
        _setThreshold(threshold);
    }

    /// @dev 允许入口点作为授权的执行者。
    function _erc7821AuthorizedExecutor(
        address caller,
        bytes32 mode,
        bytes calldata executionData
    ) internal view virtual override returns (bool) {
        return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
    }
}

此实现非常适合标准多重签名设置,其中每个签名者具有相同的权限,并且需要固定数量的批准。

MultiSignerERC7913 合约提供了几个用于管理多重签名账户的关键功能。它维护一组授权签名者,并实现一个基于阈值的系统,该系统需要最少数量的签名才能批准操作。该合约包括一个用于管理签名者的内部接口,允许添加和删除授权方。

MultiSignerERC7913 采取保护措施,以确保阈值根据当前活动签名者的数量保持可实现,从而防止操作可能无法执行的情况。

该合约还提供了用于查询签名者信息的公共函数:isSigner(bytes memory signer) 用于检查给定的签名者是否已授权,getSigners(uint64 start, uint64 end) 用于检索授权签名者的分页列表,以及 getSignerCount() 用于获取签名者的总数。在验证签名、实施自定义访问控制逻辑或构建需要显示签名者信息的用户界面时,这些函数非常有用。

MultiSignerERC7913Weighted

对于更复杂的治理结构,MultiSignerERC7913Weighted 合约通过为每个签名者分配不同的权重来扩展 MultiSignerERC7913

// contracts/MyAccountMultiSignerWeighted.sol
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

import {Account} from "@openzeppelin/community-contracts/account/Account.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import {ERC1155Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC1155Holder.sol";
import {ERC7739} from "@openzeppelin/community-contracts/utils/cryptography/signers/ERC7739.sol";
import {ERC7821} from "@openzeppelin/community-contracts/account/extensions/ERC7821.sol";
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {MultiSignerERC7913Weighted} from "@openzeppelin/community-contracts/utils/cryptography/signers/MultiSignerERC7913Weighted.sol";

contract MyAccountMultiSignerWeighted is
    Account,
    MultiSignerERC7913Weighted,
    ERC7739,
    ERC7821,
    ERC721Holder,
    ERC1155Holder,
    Initializable
{
    constructor() EIP712("MyAccountMultiSignerWeighted", "1") {}

    function initialize(bytes[] memory signers, uint256[] memory weights, uint256 threshold) public initializer {
        _addSigners(signers);
        _setSignerWeights(signers, weights);
        _setThreshold(threshold);
    }

    function addSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
        _addSigners(signers);
    }

    function removeSigners(bytes[] memory signers) public onlyEntryPointOrSelf {
        _removeSigners(signers);
    }

    function setThreshold(uint256 threshold) public onlyEntryPointOrSelf {
        _setThreshold(threshold);
    }

    function setSignerWeights(bytes[] memory signers, uint256[] memory weights) public onlyEntryPointOrSelf {
        _setSignerWeights(signers, weights);
    }

    /// @dev 允许入口点作为授权的执行者。
    function _erc7821AuthorizedExecutor(
        address caller,
        bytes32 mode,
        bytes calldata executionData
    ) internal view virtual override returns (bool) {
        return caller == address(entryPoint()) || super._erc7821AuthorizedExecutor(caller, mode, executionData);
    }
}

此实现非常适合以下情况:不同的签名者应具有不同的权限级别,例如:

  • 具有不同投票权的董事会成员

  • 具有分层决策的组织结构

  • 结合核心团队和社区成员的混合治理系统

  • 像“社交恢复”这样的执行设置,在这些设置中,您比其他人更信任特定的监护人

MultiSignerERC7913Weighted 合约使用加权系统扩展了 MultiSignerERC7913。每个签名者都可以具有自定义权重,并且操作需要签名参与者的总权重达到或超过阈值。没有明确权重的签名者默认为权重 1。

设置加权多重签名时,请确保阈值与用于签名者权重的比例匹配。例如,如果签名者的权重为 1、2 或 3,则阈值 4 将需要至少两个签名者(例如,一个权重为 1,一个权重为 3)。

设置多重签名账户

要创建多重签名账户,您需要:

  1. 定义您的签名者

  2. 确定您的阈值

  3. 使用这些参数初始化您的账户

下面的示例演示了使用不同类型的签名者设置 2-of-3 多重签名账户:

// Example setup code
function setupMultisigAccount() external {
    // Create signers using different types of keys
    bytes memory ecdsaSigner = alice; // EOA address (20 bytes)

    // P256 signer with format: verifier || pubKey
    bytes memory p256Signer = abi.encodePacked(
        p256Verifier,
        bobP256PublicKeyX,
        bobP256PublicKeyY
    );

    // RSA signer with format: verifier || pubKey
    bytes memory rsaSigner = abi.encodePacked(
        rsaVerifier,
        abi.encode(charlieRSAPublicKeyE, charlieRSAPublicKeyN)
    );

    // Create array of signers
    bytes[] memory signers = new bytes[](3);
    signers[0] = ecdsaSigner;
    signers[1] = p256Signer;
    signers[2] = rsaSigner;

    // Set threshold to 2 (2-of-3 multisig)
    uint256 threshold = 2;

    // Initialize the account
    myMultisigAccount.initialize(signers, threshold);
}

对于加权多重签名,您还需要指定权重:

// Example setup for weighted multisig
function setupWeightedMultisigAccount() external {
    // Create array of signers (same as above)
    bytes[] memory signers = new bytes[](3);
    signers[0] = ecdsaSigner;
    signers[1] = p256Signer;
    signers[2] = rsaSigner;

    // Assign weights to signers (Alice:1, Bob:2, Charlie:3)
    uint256[] memory weights = new uint256[](3);
    weights[0] = 1;
    weights[1] = 2;
    weights[2] = 3;

    // Set threshold to 4 (requires at least Bob+Charlie or all three)
    uint256 threshold = 4;

    // Initialize the weighted account
    myWeightedMultisigAccount.initialize(signers, weights, threshold);
}
_validateReachableThreshold 函数确保所有活动签名者的权重之和达到或超过阈值。 构建在多重签名合约之上的任何自定义都必须确保阈值始终可达到。

对于多重签名账户,签名是一个复杂的结构,其中包含签名者及其各自的签名。 该格式遵循 ERC-7913 的规范,并且必须正确编码。

签名格式

多重签名编码为:

abi.encode(
    bytes[] signers,   // Array of signers sorted by `keccak256`
    bytes[] signatures // Array of signatures corresponding to each signer
)

其中:

  • signers 是参与此特定签名的签名者数组

  • signatures 是与每个签名者对应的各个签名数组

为了避免重复的签名者,该合约使用 keccak256 为每个签名者生成唯一 ID。 提供多重签名时,signers 数组应按 keccak256 的升序排序,并且 signatures 数组必须与其对应的签名者的顺序匹配。