这篇文章主要介绍了Genosis Safe ,文中通过代码以及文档配合进行讲解,很详细,它对在座的每个人的研究和工作具有很经典的参考价值。 如果需要,让我们与区块链资料网一起学习。

https://www.interchains.cc/24400.html

Genosis Safe是很好的区块链资料,他说明了区块链当中的经典原理,可以给我们提供资料,Genosis Safe学习起来其实是很简单的,

不多的几个较为抽象的概念也很容易理解,之所以很多人感觉Genosis Safe比较复杂,一方面是因为大多数的文档没有做到由浅入深地讲解,概念上没有注意先后顺序,给读者的理解带来困难

Genosis Safe

  • samczsun

Genosis Safe简单来讲是一个多签钱包,它允许多个账户对同一笔交易进行签名,实现账户资金的安全管理。

Genosis Safe简介

Genosis Safe

Genosis Safe简单来讲是一个多签钱包,它允许多个账户对同一笔交易进行签名,实现账户资金的安全管理。主要的应用有如下三个场景:

  1. 公司和合约可以安全地持有他们的资金,并要求大多数所有者接受转移资金。所以没有一个所有者可以带着钱跑。
  2. 公司可以在大多数所有者的共识下执行敏感交易。
  3. 个人可以使用multisig来拥有钥匙的冗余度。多重认证的一个特性是,如果你丢失了一把钥匙,你可以用剩下的两把钥匙恢复钱包。

业务逻辑分析:

Genosis Safe的主要业务逻辑是组合多个用户(可以是EOA,也可以是合约),多个用户同时对一笔交易进行签名,签名验证成功后,执行交易并扣除相应的Gas费用,将剩余Gas返回给设定的refund账户。用户可以加载多个模块,通过不同的模块对多签钱包的行为进行限制。

EIP712:

$$ hashStruct(S)=keccak256(abi.encode(typeHash,encodeData(S))) typeHash=keccak256(encodeType(typeOf(S))) $$

EIP712中定义的结构体:

EIP712Domain(uint256 chainId,address verifyingContract) SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)

EIP712结构体1:EIP712Domain

首先是encodeType  bytes32 DOMAIN_SEPARATOR_TYPEHASH = keccak256("EIP712Domain(uint256 chainId,address verifyingContract)"); 然后是encodeData uint chain_id; assembly{     chain_id := chainid() } bytes memory encodeData = abi.encode(                             DOMAIN_SEPARATOR_TYPEHASH,                             chain_id,                             address(this)); 最后是得到DomainSeparatorHash bytes32 DomainSeparatorHash = keccak256(encodeData);

EIP712结构体2:SafeTx

首先是encodeType: bytes32 SafeTx_TYPEHASH = keccak256("SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)"); 然后是encodeData: 注意到data中由bytes类型,针对这种不固定长度的数据类型,编码时直接取其hash值作为编码数据 bytes memory SafeTx_encodeData = abi.encode(     SafeTx_TYPEHASH,     address(to),     uint256(value),     keccak256(data),     uint256(uint8(operation)),     uint256(safeTxGas),     uint256(baseGas),     uint256(gasPrice),     address(gasToken),     address(refundReceiver),     uint256(nonce) ) 最后是得到SafeTxHash bytes32 SafeTxHash = keccak256(SafeTx_encodeData);

EIP712得到的结构化编码信息如下:

abi.encodePacked(bytes1(0x19),bytes1(0x01),DomainSeparatorHash,SafeTxHash);

签名的三种类型:

在GnosisSafe中,所有的签名都编码为{bytes32 r}{btyes32 s}{uint8 v},共计65个bytes

  • 来自EOA的ECDSA签名: V值范围为(26,31), 若V值大于30,说明需要减去4以适配eth_sign方式
EOA的签名由三部分组成,R,S,v. 其中V值在EIP-155后的定义为:由{0,1}+27 -> {0,1} + chian_id*2 + 35 r: 0xbde0b9f486b1960454e326375d0b1680243e031fd4fb3f070d9a3ef9871ccfd5 s: 0x7d1a653cffb6321f889169f08e548684e005f2b0c3a6c06fba4c4a68f5e00624  v: 0x1c => 编码后的签名为: bde0b9f486b1960454e326375d0b1680243e031fd4fb3f070d9a3ef9871ccfd5 7d1a653cffb6321f889169f08e548684e005f2b0c3a6c06fba4c4a68f5e00624 1c

签名验证:

if v < 30: (v={0,1}+27) addr = ecrecover(dataHash,v,r,s) if V > 30: (v={0,1}+27+4) newDataHash = keccak256(abi.encodePacked("x19Ethereum Signed Message:n32",dataHash)) addr = ecrecover(newDataHash,v-4,r,s) require(Owners[addr]!=address(0))
  • 来自合约的EIP-1271签名
EIP-1271签名是利用智能合约对一笔交易进行签名,其会返回一个验证签名的地址以及需要验证的动态数组bytes, 故在GnosisSafe的编码规则中,让V值为0,r值为验证签名的地址,v值为动态数组bytes的offset。类似于abi编码 动态数组bytes的内容为: bytes public data = new bytes(); data.push(0xdeadbeaf) 则data中的内容为: {bytes32 offset}{bytes32 length}{bytes data} r: 0000000000000000000000000000000000000000000000000000000000000001 //验证合约地址 s: 00000000000000000000000000000000000000000000000000000000000000c3 //bytes offset v: 00 =>编码后的签名为: 0000000000000000000000000000000000000000000000000000000000000001 00000000000000000000000000000000000000000000000000000000000000c3 00 0000000000000000000000000000000000000000000000000000000000000008 00000000000000000000000000000000000000000000000000000000deadbeaf

签名验证:

ISignatureValidator(addr).isValidSignature(data,sig) == EIP1271_MAGIC_VALUE
  • 预签名:
预签名中设计V值为1,预签名用于GnosisSafe内部维护的map(address=>map(bytes32=>bool)),来确认某个账户对一笔交易哈希是否以及预先签名。为统一编码,这里的签名信息编码如下: r: 0000000000000000000000000000000000000000000000000000000000000002 //该比交易哈希的验证者地址 s: 0000000000000000000000000000000000000000000000000000000000000000 //占位符 v: 01 => 0000000000000000000000000000000000000000000000000000000000000002 0000000000000000000000000000000000000000000000000000000000000000 01

签名验证:

addr = r msg.sender == addr || approveHashes[addr][dataHash] == uint(true)

revert语法:

The provided string will be abi-encoded as if it were a call to a function Error(string). In the above example, revert("Not enough Ether provided."); will cause the following hexadecimal data be set as error return data: 0x08c379a0                                                         // Function selector for Error(string) 0x0000000000000000000000000000000000000000000000000000000000000020 // Data offset 0x000000000000000000000000000000000000000000000000000000000000001a // String length 0x4e6f7420656e6f7567682045746865722070726f76696465642e000000000000 // String data

链上签名

GnosisSafe提出了一个名为链上签名的解决方案,其要解决的问题是智能合约没有私钥,无法对一笔交易签名。它的解决方案是在状态合约中维护一个全局变量:

mapping(address => mapping(bytes32 => uint256)) public approvedHashes;

然后该多签钱包的一个Owner通过调用合约的approveHash方法,传递要签名的交易hash,来实现链上对一笔交易签名。

function approveHash(bytes32 hashToApprove) external {     require(owners[msg.sender] != 0, "GnosisSafe/approveHash msg.sender is not an owner");     approveHashes[msg.sender][hashToApprove] = uint256(1); }

链下签名链上验证

这是GnosisSafe中最常用的用法,多个用户同时对一笔交易签名后,组合得到的签名数据发送给链上,让链上对其验证。链上验证的逻辑主要在execTransaction方法中执行。

链下签名过程:

普通的EOA交易签名过程

对于如下一笔交易数据:

const tx2 = {     nonce:9,     gasPrice: "0x"+(20000000000).toString(16),//"0x4A817C800",      gasLimit: "0x"+(21000).toString(16),//"0x5208",     to: '0x3535353535353535353535353535353535353535',     value: ethers.utils.parseEther("1.0"),     data: '',     chainId: 1, };

第一步:对其RLP编码

$$ RLP([nonce,gasprice,gaslimit,to,value,data,chainid,0,0]) $$

nonce: 9, => 0x09 => len=1 gasPrice: 0x4A817C800,//20*10**9, => 0x8504A817C800 => len=6 gasLimit: 0x5208,//21000, => 0x825208 => len=3 to: 0x3535353535353535353535353535353535353535, => 0x943535353535353535353535353535353535353535 => len=21 value: 0xDE0B6B3A7640000 //1 ether => 0x880DE0B6B3A7640000 => len=9 data: "", => 0x80 => len=1 v: 1 => 0x01 => len=1 s: "" => 0x80 => len=1 v: "" => 0x80 => len=1 则RLP编码后签名前的交易为: 上述字段组成了一个列表,RLP在对列表编码得:192+44=0xEC await ethers.utils.serializeTransaction(tx2) =  0xEC098504A817C800825208943535353535353535353535353535353535353535880DE0B6B3A764000080018080

第二步:得到未签名交易Hash

keccak256(0xEC098504A817C800825208943535353535353535353535353535353535353535880DE0B6B3A764000080018080) = 0xdaf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53

第三步:将未签名的交易哈希签名

签名的私钥:0x4646464646464646464646464646464646464646464646464646464646464646 则Alice的地址为: const alice = await new ethers.Wallet("0x4646464646464646464646464646464646464646464646464646464646464646") alice.address = 0x9d8A62f656a8d1615C1294fd71e9CFb3E4855A4F 得到的 r: 18515461264373351373200002665853028612451056578545711640558177340181847433846=> s: 46948507304638947509940763649030358759909902576025900602547168820602576006531 v: 37 转换为对应的16进制为: ethers.utils.hexlify(ethers.BigNumber.from("18515461264373351373200002665853028612451056578545711640558177340181847433846")) r: 0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276 s: 0x67cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83 v: 0x25 则对应的tx变为: const tx3 = {     nonce:9,     gasPrice: ethers.utils.hexlify(ethers.BigNumber.from("20000000000")),//"0x4A817C800",      gasLimit: ethers.utils.hexlify(ethers.BigNumber.from("21000")),//"0x5208",     to: '0x3535353535353535353535353535353535353535',     value: ethers.utils.parseEther("1.0"),     data: '',     v: 0x25,     r: 0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276,     s: 0x67cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83 }; 故签名后的数据进行RLP编码后为: await alice.signTransaction(tx2)= 0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83  ethers.utils.RLP.decode("0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83") [   '0x09',   '0x04a817c800',   '0x5208',   '0x3535353535353535353535353535353535353535',   '0x0de0b6b3a7640000',   '0x',   '0x25',   '0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276',   '0x67cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83' ]

这是一个EOA账户对该笔交易进行签名得到数据。

预签名交易:

最开始理解presigned message时,会感到困惑。一直在思考按照EIP-712编码后的数据如何和普通的转账类交易混合编码,当重新读了一遍EIP-191后才渐渐理解。其实Presigned message压根就不需要再和普通的转账类交易去混合,它自己本身就是一种已经签名的信息,代表了EOA的意志,智能合约(通常是钱包)只需要验证签名是否正确,然后按照EOA的想法来执行相关的交易就行。预签名交易不遵循RLP编码方式。

EIP-191 签名数据标准:

为了与普通的交易信息区分开,EIP-191定义了presigned message标准。即:

0x19 <1 byte version> <version specific data> <data to sign>

选择0x19作为开始字段的原因是普通的交易信息采用RLP编码,而RLP编码中的0x19开头只能是代表一个值:0x19本身,无法去对后续信息编码。从而与普通的RLP编码的普通交易区分开。

对于目前的Presigned message来讲,一共有三种不同的实现版本:

Version Type EIP Desc
0x00 EIP-191 Data with intended validator
0x01 EIP-712 Structured data
0x45 EIP-191 personal_sign message

对于version type = 0x00, <version specific data>中存放的是32位的验证者地址

对于version type = 0x45, 在<data to sign>中,将x19Ethereum Signed Message:n+len(message)添加到了<data to sign>的前面,然后在进行哈希,签名。

在Gnosis中,我们主要关注version type = 0x01, 即EIP-712的实现:

如业务逻辑分析中指出:Gnosis定义了两个结构体,分别是EIP712Domain和SafeTx。一个作为DomainSeperator存入<version specific data>中,一个作为payload存入<data to sign>数据中。

// const alice = await new ethers.Wallet("0x4646464646464646464646464646464646464646464646464646464646464646"); // alice.address = 0x9d8A62f656a8d1615C1294fd71e9CFb3E4855A4F let EIP712Domain = {     chainId: "1",     verifyingContract: '0x9d8A62f656a8d1615C1294fd71e9CFb3E4855A4F', //alice } let SafeTx_types = {     SafeTx : [         {name: 'to', type:'address'},         {name: 'value', type: 'uint256'},         {name: 'data', type: 'bytes'},         {name: 'operation', type: 'uint8'},         {name: 'SafeTxGas', type: 'uint256'},         {name: 'baseGas', type: 'uint256'},         {name: 'gasPrice', type: 'uint256'},         {name: 'gasToken', type: 'address'},         {name: 'refundReceiver', type: 'address'},         {name: 'nonce', type: 'uint256'}     ] } let SafeTx_values = {     "to": '0x0000000000000000000000000000000000000000',     "value": "1",     "data": "0xdeadbeaf",     "operation": "0",     "SafeTxGas": ethers.utils.hexlify(ethers.BigNumber.from("20000000000")), //0x04a817c800     "baseGas": ethers.utils.hexlify(ethers.BigNumber.from("20000000000")), //0x04a817c800     "gasPrice": '0',     "gasToken": '0x0000000000000000000000000000000000000000',     "refundReceiver": "0x0000000000000000000000000000000000000000",     "nonce": "1", } 哈希域名: keccak256(abi.encode(keccak256("EIP712Domain(uint256 chainId,address verifyingContract)"),uint256(1),uint256(uint160(address(0x9d8A62f656a8d1615C1294fd71e9CFb3E4855A4F))))) => 0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218   0000000000000000000000000000000000000000000000000000000000000001   0000000000000000000000009d8A62f656a8d1615C1294fd71e9CFb3E4855A4F  0x98f25862027c897aa0e497eac4a47094fd7b52be9b1bea9bb803916aec0409a2  ethers.utils._TypedDataEncoder.hashDomain(EIP712Domain) => 0x98f25862027c897aa0e497eac4a47094fd7b52be9b1bea9bb803916aec0409a2 数值哈希: keccak256(abi.encode(     keccak256("SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)"),     uint256(uint160(address(0x0000000000000000000000000000000000000000))),     uint256(1),     keccak256(bytes(0xdeadbeaf)),     uint256(uint8(0)),     uint256(0x04a817c800),     uint256(0x04a817c800),     uint256(0),     uint256(uint160(address(0x0000000000000000000000000000000000000000))),     uint256(uint160(address(0x0000000000000000000000000000000000000000))),     uint256(1) )) => 0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8 0000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000001 b2245a4923cdc7f56016b95b14f0fc5f5b3cd64ee68e68c3da3d8f4a67da3752 0000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000004a817c800 00000000000000000000000000000000000000000000000000000004a817c800 0000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000001 => aad21e462218b5aa0cbcf79b0a35aedb0cb5284c70521a35065681c3641cd2c0 整体编码: ethers.utils._TypedDataEncoder.encode(EIP712Domain,SafeTx_types,SafeTx_values)  => abi.encodePacked(bytes1(0x19),bytes1(0x01),) 0x19 //EIP-191定义的首字符为0x19   01 //EIP-191定义的version type 01   98f25862027c897aa0e497eac4a47094fd7b52be9b1bea9bb803916aec0409a2 //EIP-191定义的&lt;version specific data> => hashDomain   aad21e462218b5aa0cbcf79b0a35aedb0cb5284c70521a35065681c3641cd2c0 //EIP-191定义的&lt;data to sign> 数据的哈希值

上面通过EIP-721的编码后,需要进行哈希,然后在进行签名得到r,s,v

ethers.utils._TypedDataEncoder.hash(EIP712Domain,SafeTx_types,SafeTx_values) => 0x0fda8d4f1c154fd9bb5139f4d0dd1eb327fd952ce7fe600be00bbf7486b91d6a

利用alice账户对哈希值进行签名,这样就得到了alice用户对于message的一个签名:

const alice = await new ethers.Wallet('0x4646464646464646464646464646464646464646464646464646464646464646') await alice.signMessage(ethers.utils.arrayify("0x0fda8d4f1c154fd9bb5139f4d0dd1eb327fd952ce7fe600be00bbf7486b91d6a")) => 0xb2ea252c31e822cedad8bdd1656c713b96f61345459ca47e14925f0f6e345ac4 //r   070c60d290a0acac4ebd44550b71dd032a99608dc770bd4a7295c235c5178aca //s   1c                                                          //v

当有多个EOA账户对该message进行签名时,会将多个交易的签名信息,即r,s,v值按照第一种签名编码方式进行编码,{bytes32 r}{bytes32 v}{uint8 v},最后得到一个bytes类型的数据Signature.

链上验证过程

前面对链下的签名过程进行了分析,其实质是综合了EIP-191,EIP-155,EIP-712等签名规范。这个章节部分,我们将对Gnosis的链上验证过程进行讨论。 GnosisSafe的链上验证函数主要是:checkNSignatures, 其函数签名如下:

function checkNSignatures(bytes32 dataHash,bytes memory data, bytes memory signatures, uint256 requiredSignatures)      public view{}

函数里的主要逻辑是执行一个for循环,在每一个循环内部从sigatures里拿到对应的r,s,v值,然后根据它自定义的编码规则中的v值,依次判断签名的有效性。

第一步:拿到sigantures中对应的r,s,v值

由于其编码{bytes32 r}{bytes32 v}{bytes1 v}的编码方式,每一个owner的签名都占据65个bytes。

首先拿到owner[i]对应的offset值: 注意bytes memory signatures 指向的是sigantures的数据长度,真正的数据存放位置为: add(signatures,0x20) 这里忽略了EIP-1127的情况,需要在签名情况验证中去处理 uint i; bytes32 r; bytes32 s; uint8 v; assembly{     let base_offset := add(signatures, 0x20)     let owner_i_offset :=  add(base_offset, mul(i, 0x41))     r := mload(owner_i_offset)     s := mload(add(owner_i_offset, 0x20))     v := byte(0, mload(add(owner_i_offset, 0x40))) } return r,s,v;

第二步:EOA签名情况验证

if (v &lt; 30 && v > 25) {     address currentOwner = ecrecover(dataHash, v,r,s);     require(currentOwner == address(alice)); }

第三步:EOA签名带personal_sign方法情况验证

if (v > 30) {     v = v -4;     bytes memory newDataHash = keccak256(abi.encodePacked("x19Ethereum Signed Message:n32", dataHash));     address currentOwner = ecrecover(newDataHash,v,r,s);     require(currentOwner == address(alice)); }

第四步:线上签名情况验证

if (v == 1) {     address currentOwner = address(uint160(uint256(r)));     require(approvedHashes[currentOwner][dataHash] != 0 || msg.sender == currentOwner); }

第五步:合约签名情况验证

bytes memory contractSignature; if (v == 0) {     address currentOwner = address(uint160(uint256(r)));     assembly{          let signatures_base := add(sigatures,0x20)         let sig_len := mload(add(signatures_base, s))         contractSignature := add(signatures_base, s) //并未跳过长度部分,仍然遵循Solidity规范     }     require(IEIP1271SignatureValidator(currentOwner).isValidSignature(data,contractSignature) == EIP1271_MAGIC_VALUE); }

部分转自网络,侵权联系删除www.interchains.cchttps://www.interchains.cc/24400.html

区块链毕设网(www.interchains.cc)全网最靠谱的原创区块链毕设代做网站 部分资料来自网络,侵权联系删除! 最全最大的区块链源码站 ! QQ3039046426
区块链知识分享网, 以太坊dapp资源网, 区块链教程, fabric教程下载, 区块链书籍下载, 区块链资料下载, 区块链视频教程下载, 区块链基础教程, 区块链入门教程, 区块链资源 » Genosis Safe

提供最优质的资源集合

立即查看 了解详情