[翻译] Ethereum多重签名设计方案

2019-03-14  本文已影响0人  已不再更新_转移到qiita
eth

原文:https://blog.gridplus.io/toward-an-ethereum-multisig-standard-c566c7b7a3f6,写于 2017年11月10号

最近的一些事件让以太坊的多重签名钱包(又称“多重签名”)成为人们关注的焦点。今年黑客已经2次利用 Parity的多重签名合约的漏洞。批评人士很快就引用了这些事件,他们Ethereum因为攻击面太大而无法工作,但现实情况要微妙得多。

Grid+ 正在进行预售(即将结束),将资金存放在安全的离线设置中。在权衡我们的存储选择时,我们立即决定反对所有我们熟悉的多重签名合约,特别是那些由: Parity, Ethereum Foundation, ConsenSys, 和 Gnosis制定的合约。这并不是说所有这些钱包都是有漏洞的(例如,Gnosis的钱包在过去一年多的时间里一直持有2亿美元,没有出现任何漏洞),这一决定是出于对复杂性的普遍谨慎。这些钱包都没有满足我们的需求,但这并不是说这样的钱包不能存在于Ethereum上。

作为一种比较手段,我们可以看看比特币的基于 P2SH的多签名方案,自2012年首次使用以来,该方案没有收到任何黑客攻击的报告。安全性的差异确实使得攻击面减少,尽管这在很大程度上是由于比特币的受约束脚本语言

在本文中,我将在比特币的多重签名和一个提议的Ethereum多重签名之间进行比较。我希望这将有助于引导讨论走向一个通用的、标准化的、简单的Ethereum 多重签名合约。

比特币多重签名钱包

在探索Ethereum之前,首先了解比特币的P2SH)方案是如何工作的以及它如何应用于多重签名方案是很有意义的。

正式的P2SH定义可以在 BIP16中找到,这里可以找到一个2-of-3 多重签名示例。步骤大致如下:

  1. 根据一组公钥和一个“阈值”参数生成一个多重签名地址,阈值是触发支出所需的最少签名数量。

  2. 为一个新的多重签名地址发送资金,将由这个多重签名地址产生一个
    UTXO(unspent transaction output)

  3. 使用多重签名的UTXO创建一个新的原始离线交易。

  4. 使用私钥签署原始交易,将会返回一个16进制的字符串

  5. 使用另一个私钥在16进制字符串上签名。这将返回一个新的16进制字符串。

  6. 继续第5步,直到达到阈值(例如5个签名中有3个签名),
    将结果发送到UTXO要使用的脚本中。比特币将转移到所需的地址上。

状态和转换

让我们分析一下上面的过程。首先,选择一个UTXO并形成一个原始交易。这是由一个私钥操作的。然后输出由另一个私钥操作,依此类推。因此,在原始交易上有N个操作,其中N是多重签名钱包所需的签名阈值数量。

但是,重要的是要注意这里的结果只有两种:要么进行N个签名(并且花费了UTXO),要么没有任何反应。没有中间环节,也没有链上的状态转换(不包括UTXO消耗)。同样值得注意的是,上面的多重签名钱包只能做一件事——使用UTXOs。

扩展到Ethereum

如果目标是在Ethereum模拟Bitcoin的多重签名方案,我们可以从前面的小节中学到很多。我们需要创建一个没有多余功能中间状态的多重签名方案。换句话说,我们的多重签名执行只能有两种结果:要么执行事务,要么什么也不做。

注意,Ethereum的交易是基于帐户的,而不是基于UTXO的,因此它们更加复杂。所说的“进行交易”,是指将任意一组数据签署到指定的地址(可以是合约地址),并具有指定的值(即ether的数量)。通过“进行交易”,我的意思是将一组任意数据签名到指定地址(可以是合同地址),并具有指定值(即ether的数量)。在 Solidity 中,这表示为:

destination.call.value(value)(data)

比特币多签名的另一个重要特性是它们的实例化。一旦创建,就不能更改多重签名。这意味着合约的所有者和阈值参数将永远冻结。Ethereum并行将在实例化时创建不可变状态(The Ethereum parallel would be to create an immutable state upon instantiation)。

总而言之,我们希望在Ethereum 多重签名中有以下属性:

  1. 二义性结果—— 接受交易或立即失败。

  2. 功能受限—— 钱包可以进行交易,但它不能做任何其他事情。

  3. 创建结束—— 一旦创建了钱包,参数就被锁定。

一个简单的建议

Christian Lundkvist 提出了一种符合上述特性的多重签名方案:下面是他的文章

设置阶段是这样的:

require(owners_.length <= 10 && threshold_ <= owners_.length && threshold_ != 0);
address lastAdd = address(0);

for (uint i=0; i<owners_.length; i++) {
  require(owners_[i] > lastAdd);
  isOwner[owners_[i]] = true;
  lastAdd = owners_[i];
}

ownersArr = owners_;
threshold = threshold_;

这确保 owners以排序的顺序输入,并将它们发布到合约状态。这是仅有的一次 ownersArrthreshold 可以更改时候。

一旦参数化,合约只能用于一个目的:执行交易(由tovaluedata定义)。执行过程是这样的:

require(sigR.length == threshold);
require(sigR.length == sigS.length && sigR.length == sigV.length);
// Follows ERC191 signature scheme
bytes32 txHash = keccak256(byte(0x19), byte(0), this, destination, value, data, nonce);
address lastAdd = address(0); // cannot have address(0) as an owner   

for (uint i = 0; i < threshold; i++) {
  address recovered = ecrecover(txHash, sigV[i], sigR[i], sigS[i]);
  require(recovered > lastAdd && isOwner[recovered]);
  lastAdd = recovered;
}
// If we make it here all signatures are accounted for
nonce = nonce + 1;
require(destination.call.value(value)(data));

这将检查是否提供了threshold 签名,并根据交易参数形成hash。
然后,它检查每个签名,并验证它是由计算值大于前一个所有者的所有者创建的。注意,比较有点随意——这是一个简单的十六进制字符串比较。这个约束进一步限制了攻击面,但可能没有必要。

这的签名表示为rsv——这些是由hash的椭圆曲线签名生成的,点击这里了解更多内容。

这些hash符合ERC191标准

如果所有检查都通过,则使用提供的参数(tovaluedata)在合约中进行交易调用。增加nonce,以防止重播攻击。注意,这是惟一发生的状态更改。理论上,这个合约的攻击面被限制为这个nonce值递增,这个攻击面非常小。

Ethereum的优势

这个方案正是Grid+团队正在寻找的。在43行代码中,它就像多重签名合约一样简单。它只有一个函数,即在Ethereum上创建交易。因为任何复杂的函数调用都可以执行,所以这个多重签名函数就像一个标准的钱包一样,但是需要多个签名

虽然这个钱包正在等待ConsenSys Diligence的审核,可能还需要Phil Daian的形式化验证,但我想建议社区接受这个实现的简单性,并采用标准的多sig方案。“克里斯蒂安”有一个回购,我已经开始自己的进一步测试合同。此外,我已经将代码发布到EPM (Ethereum package manager):这是代码

pragma solidity 0.4.18;
contract SimpleMultiSig {

  uint public nonce;                // (only) mutable state
  uint public threshold;            // immutable state
  mapping (address => bool) isOwner; // immutable state
  address[] public ownersArr;        // immutable state

  function SimpleMultiSig(uint threshold_, address[] owners_) public {
    require(owners_.length <= 10 && threshold_ <= owners_.length && threshold_ != 0);

    address lastAdd = address(0);
    for (uint i=0; i<owners_.length; i++) {
      require(owners_[i] > lastAdd);
      isOwner[owners_[i]] = true;
      lastAdd = owners_[i];
    }
    ownersArr = owners_;
    threshold = threshold_;
  }

  // Note that address recovered from signatures must be strictly increasing
  function execute(uint8[] sigV, bytes32[] sigR, bytes32[] sigS, address destination, uint value, bytes data) public {
    require(sigR.length == threshold);
    require(sigR.length == sigS.length && sigR.length == sigV.length);

    // Follows ERC191 signature scheme: https://github.com/ethereum/EIPs/issues/191
    bytes32 txHash = keccak256(byte(0x19), byte(0), address(this), destination, value, data, nonce);

    address lastAdd = address(0); // cannot have address(0) as an owner
    for (uint i = 0; i < threshold; i++) {
        address recovered = ecrecover(txHash, sigV[i], sigR[i], sigS[i]);
        require(recovered > lastAdd && isOwner[recovered]);
        lastAdd = recovered;
    }

    // If we make it here all signatures are accounted for
    nonce = nonce + 1;
    require(destination.call.value(value)(data));
  }

  function () public payable {}
}

多签名钱包应该成为存储大量加密货币的实际标准。它们应该尽可能的安全,并对用户友好,这样它们就可以成为常用的工具,即使是那些想要确保资金不受单点故障影响的个人也可以使用它们。让我们制定一个人们不会有过多顾虑的使用标准。

上一篇 下一篇

猜你喜欢

热点阅读