使用solidity实现以太坊多签功能

2018-12-10| Category: solidity, 多重签名, multi-signature| website:
work-single-image

虽然以太坊没有提供直接的多重签名的功能, 但是号称区块链2.0的公链平台可以具有图灵完备的智能合约运行平台。 下面我就使用solidity和golang给大家暂时一下如何用智能合约来实现以太坊的多签功能。

前言

玩过比特币交易的大概都通过多重签名的功能, 比特币的多重签名一般有MS和P2SH两种类型。 多重签名可以实现N:M的的交易。 但是以太坊是一个内置智能合约的区块链平台, 没有直接内置比特币这种多签交易的功能。 那是不是就不能实现多重签名的功能呢?既然以太坊是号称区块链2.0,有图灵完备的智能合约平台, 自然就可以实现多签功能, 废话不说让我来展示给大家看。

准备实现的功能

  1. 实现可以设置任意N:M的签名交易 一般的比特币多签交易在最开始设置完N:M的多签之后是不能更改的, 我们既然是可以写代码当然是可以实现N:M是动态变化的。
  2. 实现任意修改可以发起交易的多个签名者
  3. 可以随时控制交易暂停或者启动
  4. 实现权限控制

设计思路

  1. 对于以太坊如果想实现多签转账的话,那么拥有token的账户肯定不能是个人账户了,因为只有合约账户才能受代码控制,这样才能实现多签功能。

  2. 其次既然合约账户最终拥有了token,如果想要合约账户可以转账给其他账户,这个时候就必须做到可以确认控制合约转账的账户是特定的用户,也即是N个签名者。那么如何才能确认发起交易的用户不是伪造的呢? 那么我们就需要用到非对称加密的功能了。

  3. 最后如果我们验证多签用户通过, 那么如何才能把合约账户的Token转移到其他账户呢, 这里就要用到ERC20的接口协议了, 只要我们在合约中定义ERC20接口, 并且在初始化时指向具体的token合约地址即可以调用具体合约实现转账的功能。

核心代码实现

  • 验证签名原理

在solidity中有一个内置函数为ecrecoverDecode可以根据待签名内容和签名内容恢复出地址, 所以我们只要通过恢复的地址来确认该地址是不是合约账户指定的多签账户中的一个。

  function recoverAddress(bytes32 hashValue, string signValue) internal returns (address){
      bytes memory signedString = hexStr2bytes(signValue);
      bytes32  r = bytesToBytes32(slice(signedString, 0, 32));
      bytes32  s = bytesToBytes32(slice(signedString, 32, 32));
      byte  v = slice(signedString, 64, 1)[0];
      return ecrecoverDecode(hashValue, r, s, v);
  }
  
    //将原始数据按段切割出来指定长度
  function slice(bytes memory data, uint start, uint len)  internal returns (bytes){
    bytes memory b = new bytes(len);

    for(uint i = 0; i < len; i++){
      b[i] = data[i + start];
    }

    return b;
  }

  //使用ecrecover恢复公匙
  function ecrecoverDecode(bytes32 hashValue, bytes32 r, bytes32 s, byte v1) internal returns (address){
     uint8 v = uint8(v1) + 27;
    return ecrecover(hashValue, v, r, s);
  }
  • 进行转账的实现

定义ERC20的接口, 当转账时只需要调用transfer即可实现转账。

  contract ERC20 {

    uint256 public totalSupply;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    function balanceOf(address who) public view returns (uint256);
    function transfer(address to, uint256 value) public returns (bool);

    function allowance(address owner, address spender) public view returns (uint256);
    function approve(address spender, uint256 value) public returns (bool);
    function transferFrom(address from, address to, uint256 value) public returns (bool);

  }

    function _transfer(address to, uint256 value) private returns(bool){
        return _erc20.transfer(to, value);
    }
  • 整个多签转账的核心逻辑如下
      function transfer(address _to, uint256 _value, string _txid, string _signs) onlyNoPause()  public returns (bool) {
        uint256 balance = _erc20.balanceOf(this);
        // 分隔多个签名 因为多重签名应该有多个账户对同一个内容的签名内容
        strings.slice memory s = _signs.toSlice();                
        strings.slice memory delim = ",".toSlice();                            
        string[] memory allsigns = new string[](s.count(delim)+1);                  
        for (uint i = 0; i < allsigns.length; i++) {                              
           allsigns[i] = s.split(delim).toString();                               
        }

        // 进行条件判断
        if (balance<=_value) {
            emit TransferError(_txid, 400, _to, _value);
            return false;
        }
        // 必须要求进行交易的ID不可重复 
        if (isExistTx[_txid]) {
            emit TransferError(_txid, 403, _to, _value);
            return false;
        }

        if (allsigns.length < minSenderNum) {
            emit TransferError(_txid, 401, _to, _value);
            return false;
        }

        // 对参数进行编码
        bytes32  hashValue =  keccak256(abi.encode(_to, "&", _value, "&", _txid));
        for(i=0; i<minSenderNum; i++) {
           address fromAddr = recoverAddress(hashValue, allsigns[i]);
          if (!canSenders[fromAddr]) {
              emit TransferError(_txid, 402, _to, _value);
              return false;
          }
        }
        isExistTx[_txid] = true;
        emit TransferError(_txid, 200, _to, _value);
        return _transfer(_to, _value);
    }

使用truffle进行调试

  1. 启动ganache 默认端口为7545
  2. 修改truffle-config.js文件

      module.exports = {
      networks: {
        development: {
          host: "127.0.0.1",
          port: 7545,
          network_id: "*" // Match any network id
        },
        priv: {
          host: "127.0.0.1",
          port: 8542,
          network_id: "*", // Match any network id
          gas: 0x2fefd8
        },
        test : {
          host: "172.16.129.108",
          port: 8544,
          gas: 0x2fefd8,
          network_id: "*" // Match any network id
        }
      }
    };
    
  3. 在migrations文件夹下添加部署文件2_deploy.js

      var Token = artifacts.require("./TokenERC20");
      var ms = artifacts.require("./MultiSigAcount")
          
      module.exports = function(deployer) {
        deployer.deploy(Token, 1000000000000, "TokenTest", "t", 4).then(function(){
          return deployer.deploy(ms, Token.address);
        })
      }
    
  4. 开始部署到私有节点中

    truffle migrate --network=development --reset
    
  5. 启动控制台进行调试

  truffle console --network=development
  truffle(development)> var token;
  undefined
  truffle(development)> var mutisign;
  undefined
  truffle(development)> TokenERC20.deployed().then(function(i){token=i});
  undefined
  truffle(development)> MultiSigAcount.deployed().then(function(i){mutisign=i})
  undefined
  truffle(development)> token.transfer(mutisign.address, 10000000)
  
  // 监听转账错误事件
  truffle(development)> var event = mutisign.TransferError()
  
  truffle(development)> event.watch(function(err, result){console.log(result.args)})
  
  mutisign.transfer("0xb9b7e0cb2edf5ea031c8b297a5a1fa20379b6a0a", 100, "qwerty", "a0cf8e911765596151d9826c51fbb3dfede93904fe6772442fd1ea5d589efe0d28d82e97516c2ceee00ecd2e6d0acafc1b5721dda499724900ba7feac08245e401,54b99da440a9f7dd1fd6f65ea082229314977f6bf238e9ea1dc0959f336996475e6bf94b7104bb9a93ce1affac16853a42d8311892322fa72ff511bf6db3b2cd01")

如何使用golang实现调用方的交易签名

在上面的智能合约多签验证中, 我们首先使用了solidity内置的abi.encode对交易的参数进行了编码然后再进行了sha/keccak256的hash运算,最后恢复出地址。 所以如果想要恢复成功必须要求调用方做相同的步骤, 整个golang的核心代码如下:

      func main() {
      // 需要进行编码的参数为// _to, "&", _value, "&", _txid
      // 所对应的参数类型为address, string, uint256, string, string
    	uint256Ty, _ := abi.NewType("uint256")
    	stringTy, _ := abi.NewType("string")
    	addressTy, _ := abi.NewType("address")
    	
    	arguments := abi.Arguments{
    		{
    			Type: addressTy,
    		},
    		{
    			Type: stringTy,
    		},
    		{
    			Type: uint256Ty,
    		},
    		{
    			Type: stringTy,
    		},
    		{
    			Type: stringTy,
    		},
    	}
    
      // 对参数进行编码 此编码出的结果与solidity对应的abi.encode结果一致
    	bytes, _ := arguments.Pack(
    		common.HexToAddress("0xb9b7e0cb2edf5ea031c8b297a5a1fa20379b6a0a"),
    		"&",
    		big.NewInt(100),
    		"&",
    		"qwerty",
    	)
      
      // 进行hash运算
    	var buf []byte
    	hash := sha3.NewKeccak256()
    	hash.Write(bytes)
    	buf = hash.Sum(buf)
    	// 加载私钥 进行签名
    	priKey, _ := crypto.HexToECDSA(privkeyStr1)
    	sig, _ := crypto.Sign(buf, priKey)
    	fmt.Printf("签名内容为: %0x\n", sig)
    }

总结

整个使用以太坊的智能合约实现多签的功能核心部分就是这么多, 我已经在ropsten网络部署了此智能合约并进行了相关的测试。 整个测试中发现当多签数量为1个时进行的转账gas消耗大约在25W左右,2个时大约在31W左右。 所以优化的任务就留给有缘人了。

多签合约的地址为:0x23d0fe96aa989bf7ff76fc34901d4a5633792a2e (源码已经验证)

ERC20代币地址为: 0x07b6cDc7df9A1ff1eD97FB40531f2BB4bb32499B

具体详细的智能合约和golang代码可以查看的我的github

Clients
wupeaking
date
12月10号, 2018
category
Investment, Business