欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 焦点 > 从链下签名到链上验证:如何实现一个多签名钱包

从链下签名到链上验证:如何实现一个多签名钱包

2025/7/4 11:58:57 来源:https://blog.csdn.net/Huahua_1223/article/details/143976356  浏览:    关键词:从链下签名到链上验证:如何实现一个多签名钱包

文章目录

  • 前言
  • 一、什么是数字签名与ECDSA?
    • 1. 数字签名的定义
    • 2. ECDSA的工作原理
  • 二、链下签名与链上验证的结合
    • 重放攻击简介
    • 防御策略
  • 三、如何实现一个多签名钱包?
    • 1. 环境准备
    • 2. 合约设计与功能模块
    • 3. 实现步骤
  • 总结


前言

数字签名是区块链技术的重要基础之一,它在保证数据真实性和完整性方面发挥着不可替代的作用。在智能合约中,链下签名与链上验证的结合,不仅提升了效率,还增强了安全性。本篇文章将以 ECDSA(椭圆曲线数字签名算法)为核心,详细介绍如何实现链下签名、链上验证,并通过多签名钱包的具体实现,帮助你深入理解这一过程。同时,我们将深入解析重放攻击的原理及其防御方法。

在这里插入图片描述


一、什么是数字签名与ECDSA?

1. 数字签名的定义

数字签名是一种数学方案,用于验证消息或数据的真实性和完整性。在区块链中,数字签名被广泛应用于:

  • 确认身份:验证交易的签署者身份是否真实。
  • 防止篡改:任何未经授权的更改都会导致签名无效。
  • 防重放攻击:通过唯一标识符(如 nonce)避免同一笔交易被重复提交。

2. ECDSA的工作原理

ECDSA 是一种基于椭圆曲线密码学的数字签名算法,与传统的 RSA 相比,它具有密钥长度短计算速度快的特点,非常适合区块链应用。
其基本流程包括:

  • 私钥签名:利用私钥对消息(或哈希值)生成签名。
  • 公钥验证:通过公钥验证签名是否有效。

ECDSA 签名的特点

  1. 签名的唯一性:每个签名与对应的消息哈希绑定,不能伪造。
  2. 公私钥配对:签名由私钥生成,验证由公钥完成,确保了身份的唯一性。
  3. 高效性:相比 RSA 等传统算法,ECDSA 在资源受限的设备(如硬件钱包)上表现更佳。

二、链下签名与链上验证的结合

链下签名与链上验证是一种分布式计算策略。签名过程在链下完成,验证逻辑在链上执行,这种模式具备以下优点:

  1. 节省资源:减少链上计算与存储消耗。
  2. 提高安全性:签名数据保存在链下,避免被恶意用户截取或篡改。
  3. 灵活性高:链下生成签名后,可以在多个链上合约中验证,提高了应用的扩展性。

重放攻击简介

重放攻击是数字签名中常见的安全威胁。攻击者利用网络上的已捕获数据包,将同一笔交易反复发送到区块链,造成资产的重复消耗或其他安全风险。

防御策略

  1. Nonce 机制:在交易中加入唯一的 nonce(计数器),确保每笔交易的唯一性。
  2. 签名绑定上下文:在签名消息中加入链 ID、智能合约地址等上下文信息,防止跨链或跨合约攻击。

三、如何实现一个多签名钱包?

为了实现一个支持链下签名与链上验证的多签名钱包(MultiSigWallet),以下是具体的开发步骤、设计思路以及关键代码实现。

1. 环境准备

为了实现一个支持链下签名的多签名钱包,我们需要以下工具和环境:

  • 本地 Geth 私链🚪:模拟区块链网络,用于部署合约与调试交易。
  • Remix IDE🚪:用于编写和部署 Solidity 合约。
  • MetaMask🚪:支持链下签名功能,便于用户签名交易并与区块链交互。
  • 浏览器开发者工具(F12):调试和生成链下签名。

⚠️ 注意事项:

  • 启动 Geth 私链后,请确保 MetaMask 连接到本地网络
  • 至少创建两个账户,用于多签名验证操作
    在这里插入图片描述

2. 合约设计与功能模块

多签名钱包的核心功能包括以下几点:

  1. 资金存储与余额管理:用户可以安全存款,合约记录余额。
  2. 多签名转账机制:交易执行需要至少两个账户共同签署交易。
  3. 防重放机制:通过 nonce 确保每笔交易唯一,避免重复执行。

核心代码实现

(1)生成交易哈希(Hash)
交易哈希是转账验证的关键,通过接收转账目标地址、金额、nonce 等信息生成独特的哈希值:

function getTxHash(address _to,uint256 _amount,uint256 _nonce
) public pure returns (bytes32) {return keccak256(abi.encodePacked(_to, _amount, _nonce));
}

(2)链上签名验证与转账操作

  • 验证链下签名的有效性。
  • 检查是否重复执行交易。
  • 完成转账操作并标记交易为已执行。
function transfer(address _to,uint256 _amount,uint256 _nonce,bytes[2] memory _sigs
) external {bytes32 txHash = getTxHash(_to, _amount, _nonce);require(!executed[txHash], "tx executed");require(_checkSigs(_sigs, txHash), "invalid sig");executed[txHash] = true;(bool sent,) = _to.call{value: _amount}("");require(sent, "Failed to send Ether");
}

MultiSigWallet.sol完整代码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;import "./ECDSA.sol";contract MultiSigWallet {using ECDSA for bytes32;address[2] public owners;mapping(bytes32 => bool) public executed;constructor(address[2] memory _owners) payable {owners = _owners;}function deposit() external payable {}function transfer(address _to,uint256 _amount,uint256 _nonce,bytes[2] memory _sigs) external {bytes32 txHash = getTxHash(_to, _amount, _nonce);require(!executed[txHash], "tx executed");require(_checkSigs(_sigs, txHash), "invalid sig");executed[txHash] = true;(bool sent,) = _to.call{value: _amount}("");require(sent, "Failed to send Ether");}function getTxHash(address _to, uint256 _amount, uint256 _nonce)publicviewreturns (bytes32){return keccak256(abi.encodePacked(address(this), _to, _amount, _nonce));}function _checkSigs(bytes[2] memory _sigs, bytes32 _txHash)privateviewreturns (bool){bytes32 ethSignedHash = _txHash.toEthSignedMessageHash();for (uint256 i = 0; i < _sigs.length; i++) {address signer = ethSignedHash.recover(_sigs[i]);bool valid = signer == owners[i];if (!valid) {return false;}}return true;}
}

ECDSA.sol完整代码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;// OpenZeppelin Contracts (last updated v4.5.0) (utils/cryptography/ECDSA.sol)library ECDSA {enum RecoverError {NoError,InvalidSignature,InvalidSignatureLength,InvalidSignatureS,InvalidSignatureV}function _throwError(RecoverError error) private pure {if (error == RecoverError.NoError) {return; // no error: do nothing} else if (error == RecoverError.InvalidSignature) {revert("ECDSA: invalid signature");} else if (error == RecoverError.InvalidSignatureLength) {revert("ECDSA: invalid signature length");} else if (error == RecoverError.InvalidSignatureS) {revert("ECDSA: invalid signature 's' value");} else if (error == RecoverError.InvalidSignatureV) {revert("ECDSA: invalid signature 'v' value");}}function tryRecover(bytes32 hash, bytes memory signature)internalpurereturns (address, RecoverError){// Check the signature length// - case 65: r,s,v signature (standard)// - case 64: r,vs signature (cf https://eips.ethereum.org/EIPS/eip-2098) _Available since v4.1._if (signature.length == 65) {bytes32 r;bytes32 s;uint8 v;// ecrecover takes the signature parameters, and the only way to get them// currently is to use assembly.assembly {r := mload(add(signature, 0x20))s := mload(add(signature, 0x40))v := byte(0, mload(add(signature, 0x60)))}return tryRecover(hash, v, r, s);} else if (signature.length == 64) {bytes32 r;bytes32 vs;// ecrecover takes the signature parameters, and the only way to get them// currently is to use assembly.assembly {r := mload(add(signature, 0x20))vs := mload(add(signature, 0x40))}return tryRecover(hash, r, vs);} else {return (address(0), RecoverError.InvalidSignatureLength);}}function recover(bytes32 hash, bytes memory signature)internalpurereturns (address){(address recovered, RecoverError error) = tryRecover(hash, signature);_throwError(error);return recovered;}function tryRecover(bytes32 hash, bytes32 r, bytes32 vs)internalpurereturns (address, RecoverError){bytes32 s = vs& bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);uint8 v = uint8((uint256(vs) >> 255) + 27);return tryRecover(hash, v, r, s);}function recover(bytes32 hash, bytes32 r, bytes32 vs)internalpurereturns (address){(address recovered, RecoverError error) = tryRecover(hash, r, vs);_throwError(error);return recovered;}function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)internalpurereturns (address, RecoverError){// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines// the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most// signatures from current libraries generate a unique signature with an s-value in the lower half order.//// If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or// vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept// these malleable signatures as well.if (uint256(s)> 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {return (address(0), RecoverError.InvalidSignatureS);}if (v != 27 && v != 28) {return (address(0), RecoverError.InvalidSignatureV);}// If the signature is valid (and not malleable), return the signer addressaddress signer = ecrecover(hash, v, r, s);if (signer == address(0)) {return (address(0), RecoverError.InvalidSignature);}return (signer, RecoverError.NoError);}function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)internalpurereturns (address){(address recovered, RecoverError error) = tryRecover(hash, v, r, s);_throwError(error);return recovered;}function toEthSignedMessageHash(bytes32 hash)internalpurereturns (bytes32){// 32 is the length in bytes of hash,// enforced by the type signature abovereturn keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));}
}

3. 实现步骤

以下是实现多签名钱包的详细步骤:

  1. 部署与初始化
    在 Remix 中编写并部署 MultiSigWallet 合约,初始化两个账户地址(owners),确保每个账户有足够的余额用于链上操作
    在这里插入图片描述

  2. 存入资金
    通过调用 deposit 方法向合约存入资金,验证余额是否更新
    在这里插入图片描述

  3. 生成交易哈希
    调用 getTxHash 方法,根据转账目标地址、金额和交易唯一标识符 nonce 生成唯一的交易哈希值(txHash
    在这里插入图片描述

  4. 链下签名
    在浏览器开发者工具(F12)中,使用 MetaMask 分别切换至两个所有者账户,对 txHash 交易哈希进行签名,并保存签名值。以下是生成签名的代码:

    示例签名代码:

    ethereum.enable()
    account = "替换为账户一的地址"
    hash ="替换为刚才生成的交易哈希"
    const signature = await ethereum.request({method: "personal_sign", params:[account, hash]})
    console.log("签名结果:", signature)
    
    • 账户一签名:生成第一个签名在这里插入图片描述
      在这里插入图片描述
    • 账户二签名:生成第二个签名
      在这里插入图片描述
      在这里插入图片描述
  5. 链上验证与转账
    调用合约的 transfer 方法,输入以下参数:

    • 转账目标地址
    • 转账金额
    • nonce
    • 两个签名值的数组
      在这里插入图片描述
      调用transfer方法之前,目标账户与合约余额如图:
      在这里插入图片描述
      调用transfer方法之后,确认交易完成,目标账户将收到转账金额,同时合约余额减少
      在这里插入图片描述
  6. 防重放验证
    再次尝试重复用相同参数调用 transfer 方法,合约应返回错误,提示 “交易已被执行” ,表示 nonce 防重放机制生效
    在这里插入图片描述


总结

在本文中,我们深入探讨了如何利用链下签名与链上验证的结合,实现一个多签名钱包。我们从ECDSA数字签名的工作原理入手,介绍了其在区块链中的重要性及防御重放攻击的策略。随后,通过一个实际的多签名钱包开发案例,详细讲解了合约设计、链下签名生成、链上验证及转账操作的全过程,并展示了如何有效防止交易重复执行。希望这篇文章能帮助你更好地理解数字签名在区块链中的应用,并为你的智能合约开发提供参考。如果你有任何疑问或建议,欢迎在评论区交流🌹

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com