什么是多签钱包

多签钱包是一种特殊的钱包,可以添加多个签名用户,在执行交易的时候需要多个持有者同时签名才能提交,比如3个用户的多签钱包需要2个以上的用户同时签名。

这种设计可以有效防止单点故障,保证资产的安全,在dao群中有广泛的应用。

实现逻辑

多签钱包其实是一个智能合约,在合约中存储了多签持有者的信息。

执行交易时,需要先将交易组装成data,计算出hash。拿到交易hash后,多签用户需要分别对hash进行签名,并且将得到的签名拼接成最终的签名作为参数。

将交易data和signature作为参数调用合约中的方法,合约做的工作是:

  1. 判断签名数量需要大于多签规定的签名数量
  2. 拆分签名
  3. 对应每条签名通过ecrecover还原出签名地址,判断该地址是否存储于合约中
  4. 满足签名条件后,根据data执行交易

具体实现

ecrecover

ecrecover是solidity内置的验签方法,定义如下:

function ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) public pure returns (address)

其中hash为交易哈希,也就是签名的msg,v,r,s是从签名中拆分得到的,返回值为签名的公钥。

签名拆分

这里使用的是ECDSA标准的签名,

一个标准的 ECDSA 签名是:

  • bytes32 r
  • bytes32 s
  • uint8 v(27 或 28,也可能是 0 或 1)

总共65字节的内容,因此要写一个对signature进行拆分的方法,得到v、r、s

function signatureSplit(bytes memory signatures, uint256 pos)
internal
pure
returns (
uint8 v,
bytes32 r,
bytes32 s
)
{
// 签名的格式:{bytes32 r}{bytes32 s}{uint8 v}
assembly {
let signaturePos := mul(0x41, pos)
r := mload(add(signatures, add(signaturePos, 0x20)))
s := mload(add(signatures, add(signaturePos, 0x40)))
v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff)
}
}

这里使用了内联汇编的写法,因为涉及到了内存的读取。

signatures是拼接后的签名,pos代表的是当前读取的签名在signatures中是第几个,即索引值。

let signaturePos := mul(0x41, pos)是用来定位当前拆分出签名的偏移量,因为每个签名的长度是65字节,换算成16进制就是0x41,所以偏移量就是0x41*pos

r := mload(add(signatures, add(signaturePos, 0x20)))中,mload的作用是从某个内存地址开始,向后读取固定32字节的内容,而add(signatures, add(signaturePos, 0x20))从内部到外部的两个add分别表示:

  • add(signaturePos, 0x20)表示signaturePos0x20偏移量相加,因为bytes类型的前32字节是头部,而非实际数据,所以读取时先偏移到signaturePos,即签名起点,再向后偏移32个字节。
  • add(signatures, pos)指的是从拼接签名的起始位置,偏移到pos的位置

这两个add,一个是偏移量的相加,一个是位置的移动,但在内联汇编计算中都是使用add方法,因为signatures是一个指针,指向的地址也是用偏移字节数表示的,代表从内存0的位置偏移的数量。

用同样的原理可以取出s和v,要注意的是v的字节数为1,而mload固定取32字节,所以使用and()方法与0xff做了一个与操作,得到最低位1字节的值。

设计好验签逻辑之后,就可以开始写合约内容了。

	address[] public owners;                   // 多签持有人数组
mapping(address => bool) public isOwner; // 记录一个地址是否为多签持有人
uint256 public ownerCount; // 多签持有人数量
uint256 public threshold; // 多签执行门槛,交易至少有n个多签人签名才能被执行。
uint256 public nonce; // nonce,防止签名重放攻击 constructor(
address[] memory _owners,
uint256 _threshold
) {
_setupOwners(_owners, _threshold);
} /// @param _owners: 多签持有人数组
/// @param _threshold: 多签执行门槛,至少有几个多签人签署了交易
function _setupOwners(address[] memory _owners, uint256 _threshold) internal {
// 多签执行门槛 小于或等于 多签人数
require(_threshold <= _owners.length, "invalid threshold");
// 多签执行门槛至少为1
require(_threshold >= 1, "threshold at least 1"); for (uint256 i = 0; i < _owners.length; i++) {
address owner = _owners[i];
// 多签人不能为0地址,本合约地址,不能重复
require(owner != address(0) && owner != address(this) && !isOwner[owner], "owner can not be repeated");
owners.push(owner);
isOwner[owner] = true;
}
ownerCount = _owners.length;
threshold = _threshold;
}

此处是简化多签钱包的逻辑,不做多签持有人的变更,持有人的列表和threshold都是固定不变的。

然后写一个打包交易hash的方法,在参数中增加了一个chainid,这是为了防止拿到签名之后可以去其他链执行,进行交易重放。

/// @dev 编码交易数据
/// @param to 目标合约地址
/// @param value msg.value,支付的以太坊
/// @param data calldata
/// @param _nonce 交易的nonce.
/// @param chainid 链id
/// @return 交易哈希bytes.
function encodeTransactionData(
address to,
uint256 value,
bytes memory data,
uint256 _nonce,
uint256 chainid
) public pure returns (bytes32) {
bytes32 safeTxHash =
keccak256(
abi.encode(
to,
value,
keccak256(data),
_nonce,
chainid
)
);
return safeTxHash;
}

然后写一个验签方法:

/**
* @dev 检查签名和交易数据是否对应。如果是无效签名,交易会revert
* @param dataHash 交易数据哈希
* @param signatures 几个多签签名打包在一起
*/
function checkSignatures(
bytes32 dataHash,
bytes memory signatures
) public view {
// 读取多签执行门槛
uint256 _threshold = threshold;
require(_threshold > 0, "threshold not set"); // 检查签名长度足够长
require(signatures.length >= _threshold * 65, "signature not satisify threshold"); // 通过一个循环,检查收集的签名是否有效
// 大概思路:
// 1. 用ecdsa先验证签名是否有效
// 2. 利用 currentOwner > lastOwner 确定签名来自不同多签(多签地址递增)
// 3. 利用 isOwner[currentOwner] 确定签名者为多签持有人
address lastOwner = address(0);
address currentOwner;
uint8 v;
bytes32 r;
bytes32 s;
uint256 i;
for (i = 0; i < _threshold; i++) {
(v, r, s) = signatureSplit(signatures, i);
// 利用ecrecover检查签名是否有效
currentOwner = ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v, r, s);
require(currentOwner > lastOwner && isOwner[currentOwner], "singer not owner");
lastOwner = currentOwner;
}
}

几个要注意的地方:

  1. threshold是否满足的判断是由signatures的长度来判断的,因为单个签名的长度固定是65字节。
  2. ecrecover的时候拼接了\x19Ethereum Signed Message:\n32,这是因为在使用eth_sign或者钱包签名的时候,会自动在前面加上这一段话,用于标识是签名而非真实的交易数据,所以验签的时候也需要加上。
  3. lastOwner的记录是因为signatures的拼接是根据address从小到大进行拼的,这样保证了多签拼接顺序的固定,所以验签时还需要判断address之间的大小关系。

最后实现一个执行合约的方法:

/// @dev 在收集足够的多签签名后,执行交易
/// @param to 目标合约地址
/// @param value msg.value,支付的以太坊
/// @param data calldata
/// @param signatures 打包的签名,对应的多签地址由小到达,方便检查。 ({bytes32 r}{bytes32 s}{uint8 v}) (第一个多签的签名, 第二个多签的签名 ... )
function execTransaction(
address to,
uint256 value,
bytes memory data,
bytes memory signatures
) public payable virtual returns (bool success) {
// 编码交易数据,计算哈希
bytes32 txHash = encodeTransactionData(to, value, data, nonce, block.chainid);
nonce++; // 增加nonce
checkSignatures(txHash, signatures); // 检查签名
// 利用call执行交易,并获取交易结果
(success, ) = to.call{value: value}(data);
if (success) emit ExecutionSuccess(txHash);
else emit ExecutionFailure(txHash);
}

solidity学习之多签钱包的更多相关文章

  1. solidity学习-cryptoPunks为实例

    在这里使用cryptoPunks为实例来进行solidity的介绍,一般这些内容理解了就能够进行相对简单的智能合约的编写了,同时会添加一些我认为也十分重要的内容学习文档为http://solidity ...

  2. solidity 学习笔记(7)内联汇编

    为什么要有内联汇编? //普通循环和内敛汇编循环比较 pragma solidity ^0.4.25; contract Assembly{ function nativeLoop() public ...

  3. solidity 学习笔记(6)call 函数

    call() 方法 call()是一个底层的接口,用来向一个合约发送消息,也就是说如果你想实现自己的消息传递,可以使用这个函数.函数支持传入任意类型的任意参数,并将参数打包成32字节,相互拼接后向合约 ...

  4. solidity 学习笔记(5)接口

    接口:不用实现方法,仅仅定义方法. pragma solidity ^; contract cat{ //cat实际上实现了接口animalEat,因为他们有相同的方法. string name; f ...

  5. solidity 学习笔记(3) 函数修饰符/继承

    修饰符: 函数修饰符有 constant  view pure 其中 constant和view的功能是一样的  不会消耗gas 也就是说不会做任何存储   constant在5.0以后的版本中被废弃 ...

  6. solidity 学习笔记 2 (二维数组)

    solidity 二维数组: pragma solidity ^0.4.23; contract twoArray{ uint[2][3] grade =[[20,30],[40,50],[45,60 ...

  7. solidity学习笔记

    一 pragam solidity ^0.4.23; contract helloword{ string public  name ="hello"; function getN ...

  8. 以太坊智能合约开发 Solidity学习

    1. pragma solidity >=0.4.22 <0.6.0;//版本号,头文件 contract BooleanTest { bool _a;//默认返回false int nu ...

  9. git学习笔记12-标签管理-版本

    发布一个版本时,我们通常先在版本库中打一个标签(tag),这样,就唯一确定了打标签时刻的版本.将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来.所以,标签也是版本库的一个快照 ...

  10. solidity 学习笔记(4)library库

    library库的申明: library SafeMath{ functrion mul(uint a,uint b) public returns (uint){ uint c= a*b; asse ...

随机推荐

  1. 理解 .NET 结构体字段的内存布局

    目录 前言 基本概念 结构体的默认字段布局 对齐 64 位系统与 32 位系统的对齐要求差异 默认字段布局中 对齐要求 与 偏移量 的关系 填充 包含引用类型字段的结构体的默认字段布局 用 Struc ...

  2. ChatMoney智能体高情商接话神器

    本文由 ChatMoney团队出品 会说话是一个人的优势,而会接话才是一个人的本事.现实中很多人有这样的困扰:朋友聚会.上门拜访以及和人聊天.是不是完全不知道如何回应,只会说"嗯" ...

  3. 数据湖选型指南|Hudi vs Iceberg 数据更新能力深度对比

    数据湖作为新一代大数据基础设施,近年来持续火热,许多前线的同学都在讨论数据湖应该怎么建,许多企业也都在构建或者计划构建自己的数据湖.基于此,自然引发了许多关于数据湖选型的讨论和探究.但是经过搜索之后我 ...

  4. Golang基础笔记四之map

    本文首发于公众号:Hunter后端 原文链接:Golang基础笔记四之map 这一篇笔记介绍 Golang 里 map 相关的内容,以下是本篇笔记目录: map 的概念及其声明与初始化 map 的增删 ...

  5. Windows Server 2025激活教程

    Windows Server 如何把评估版升级改为正式版本并激活 微软官方并不提供server系统的正式版本,只提供测试的评估版本,那么我们怎么修改为正式版本呢? 1.确认版本 开始----运行--- ...

  6. Redis 在windows 下安装使用

    管理界面: Release 2022.5 · lework/RedisDesktopManager-Windows (github.com)redis 服务:发布 ·特波拉多夫斯基/雷迪斯 (gith ...

  7. Spring 概述和依赖注入(DI)

    Spring概述 Spring框架是一个轻量级的企业级开发的一站式解决方案 Spring框架主要提供Ioc容器.AOP.数据访问.WEB开发.消息.测试等相关技术的支持 每一个被Spring管理的Ja ...

  8. Shared_ptr 参考实现

    简介 为了防止我们忘记对一个指针对象的释放, C++11出现了shared_ptr. 我们也可以手动实现一个shared_ptr. 参考 https://blog.csdn.net/yanchenzh ...

  9. 什么是ETL?什么是ELT?怎么区分它们使用场景

    ELT和ETL这两种模式从字面上来看就是一个顺序颠倒的问题,每个单词拆开来看其实都是一样的.E代表的是Extract(抽取),也就是从源端拉取数据:T代表的是Transform(转换),对一些结构化或 ...

  10. lim x→c⁡ f⁢(x) = L数学语言:∀ ϵ>0, ∃ δ>0 S.T. for all x≠c, if |x-c|<δ, then |f⁢(x)-L|<ϵ 常用记号: “∃ ”:“存在”或“可以找到”,“∀ ”: “对于任意的”或“对于每一个”, maxS:数集S极大值, minS:数集S极小值, supS:上确界(上界最小值), infS下确界(下界最大值)

    实数集Completeness Axiom(连续性公理) Q: 谬论: "实数集上, 怎么求出点A"相邻"的那一点, 或A点的"下一点"? 或 &qu ...