用户交易免gas?这里有良方!

翻译:西溪明月

作者:Mahesh Murthy

使用以太坊dapp时存在很多摩擦点,其中之一便是用户需要支付gas才可以将其交易记录在区块链上。比如,我有一款简单的投票DAPP,任何用户都可以为候选人投票,且票数会存储在区块链上。但是,用户若想把他的投票记录在区块链上,就必须为此支付交易gas。这里就出现了一点不完美,用户想要的是不付费即可投票,但DAPP的所有者却希望用户可以用以太币来支付gas。但如果这项交易必须在区块链上执行,用户就别无选择,只能为此付费。那么有没有一种方法可以一举两得,既让用户可以安全地执行交易(在这里指的是为候选人投票),同时又让其他人(可能是合约持有者)将交易记录在区块链上并为之付费?

多亏John Backus下面这条推特,里面包含了足够的信息可以帮我在投票dapp上实施这样的方案。

 

 

完整的操作码在这儿:https://github.com/maheshmurthy/ethereum_voting_dapp/tree/master/chapter4

演示应用在此: https://www.zastrin.com/voting-dapp-without-paying-gas.html

我想分享一下我在这个简单的DAPP上应用这个方案的细节,以便更多的人把这项技术应用在其DAPP上,也希望可以进一步改进它。本文包括以下内容:

 

1、简要概述了公钥密码学及数字签名,这是理解这项解决方案的关键。

2、解决方案详情及新应用程序流

3、实施细节(前端js与Solidity合约的代码)

4、对潜在的问题及强化措施进行论述

数字签名

 

为讲清楚数字签名,需要你对数字签名在密码学中的运用有一个基本了解。如果你熟悉公钥密码学,请大胆跳过这部分内容。我会非常简要地对公/私钥及数字签名进行概述,但我强烈推荐大家对此进行更详细地学习——可以从维基百科入手。

公钥密码学的密码系统中有两把秘钥——公钥(Pu)及私钥(Pr)。公钥是公开的,而私钥仅为你所有。比如:你的以太坊地址是公钥(其实它由公钥衍生而来,但在这里让我们暂时把它当作公钥),而你的私钥或存储在你的浏览器上或存储在手机/电脑上。如你所知,给你发送以太币的人只需要知道你的公钥(账户)即可。然而,只有你才可以获取账户里的资金,因为只有你才知道自己的私钥。

 

公钥密码学中有些算法可以让你利用自己的公/私钥对信息进行加密、解密、签名及验证。

Pu = “0x44ac12c1e3dfd8edaf83b6f65918229d5279a6f5”

Pr = “dbc226043e390cf39280e5edfd418d7ad61931c76509270867d300f110c46506”

 

下面举例说明对消息签名和验证的含义。假如用户Kim有一对公/私钥。

 

公钥是“0x44ac12c1e3dfd8edaf83b6f65918229d5279a6f5”

私钥是“dbc226043e390cf39280e5edfd418d7ad61931c76509270867d300f110c46506”

signature = 0x9127112de0033555c7f6508d963d484965a953844dfcff092712102c236467a25af57edc53b63880ea39af8ce7334f6d77a8206e805305e7c6ad919d12bfae5c1b

 

要给消息签名,Kim需要执行函数sign(“Vote for Alice”, Pr),随后输出一个字母数字串signature = 0x9127112de0033555c7f6508d963d484965a953844dfcff092712102c236467a25af57edc53b63880ea39af8ce7334f6d77a8206e805305e7c6ad919d12bfae5c1b

 

这便是Kim用自己的私钥为“Vote for Alice”这个消息签名后得到的数字签名。

现在,任何执行验证函数verify(“Vote for Alice”, signature)(该函数的输出是 “0x44ac12c1e3dfd8edaf83b6f65918229d5279a6f5”)的人都可以验证到“Vote for Alice”这个消息是由Kim签名的。如果你留意了,这个结果正是Kim的公钥(请记住,每个人都知道这是Kim的公钥),这意味着这个消息百分百是由Kim签名的。如果你篡改签名或消息(比如,即使更改一个字母),上面的验证算法就会得到一个完全不同的公钥,而这个公钥与Kim的公钥不同,据此你将会知道消息被篡改了。

 

解决方案具体细节

 

如果你了解了数字签名,解决方案便只是一碟小菜了。我们来看如何将它用在投票app上,使用户无需支付gas就可以成功投票。从下表可以看到DAPP的所有用户及其行为。

 

 

  1. 投票人通过用自己的私钥为信息添加签名来表明他们为某个候选人投票的意愿。他们不会将其交易提交到区块链,故而不必支付任何gas。上图中的信息队列只是链下位置,所有的投票细节都存储在这里。
  2. 任何愿意支付税费(通常是合约的所有者)的人可以获取签名、候选人姓名和投票人账户地址,并将其提交到区块链上。
  3. 智能合约基于候选人姓名和签名利用验证函数衍生出公钥(以太坊账户地址)。如果衍生出的公钥与给消息添加签名的用户地址一致,智能合约就会记录下这次投票,否则将拒绝这次交易。

实现细节

 

现在我们来看一看实现细节以及各部分是如何组合在一起的。

第一步:为消息签名

第一步是作为投票人为消息添加签名。我们将利用eth_signTypedData函数来签名。这个函数已经在Metamask上得到了应用,并使签名变得十分便捷。

点击https://github.com/ethereum/EIPs/pull/712可以查看更多相关信息。

在下面可以找到为消息签名的代码。

window.voteForCandidate = function(candidate) {
  let candidateName = $("#candidate").val();

  let msgParams = [
    {
      type: 'string',      // Any valid solidity type
      name: 'Message',     // Any string label you want
      value: 'Vote for ' + candidateName  // The value to sign
    }
  ]

  var from = web3.eth.accounts[0]

  var params = [msgParams, from]
  var method = 'eth_signTypedData'

  console.log("Hash is ");
  console.log(sigUtil.typedSignatureHash(msgParams));

  // Invoke the eth_signTypedData function and pass in the message and account address.
  web3.currentProvider.sendAsync({
    method,
    params,
    from,
  }, function (err, result) {
    if (err) return console.dir(err)
    if (result.error) {
      alert(result.error.message)
    }
    if (result.error) return console.error(result)
    $("#msg").html("User intends to vote for " + candidateName + ". Any one can now submit the vote to the blockchain on behalf of this user. Copy the values");
    $("#vote-for").html("Candidate: " + candidateName);
    $("#addr").html("Address: " + from);
    $("#signature").html("Signature: " + result.result);
    console.log('PERSONAL SIGNED:' + JSON.stringify(result.result))
  })
}

需要特别注意的是,eth_signTypedData在内部用哈希算法对消息进行处理,处理后的消息是添加了签名的消息。你可以参考typedSignatureHash函数,详细了解哈希算法。

第二步:将已签名的投票提交到区块链

由于这仅仅只是一个演示应用,我们就不存储签名和其他细节信息了。签名后它会直接显示在页面上。所有人都可以将这些信息提交到区块链上。以下为将投票信息提交到区块链的代码:

window.submitVote = function(candidate) {
  let candidateName = $("#candidate-name").val();
  let signature = $("#vote-signature").val();
  let voterAddress = $("#voter-address").val();
  $("#msg").html("Vote has been submitted. The vote count will increment as soon as the vote is recorded on the blockchain. Please wait.")
  Voting.deployed().then(function(contractInstance) {
    contractInstance.voteForCandidate(candidateName, voterAddress, signature, {gas: 140000, from: web3.eth.accounts[0]}).then(function() {
      let div_id = candidates[candidateName];
      console.log(div_id);
      return contractInstance.totalVotesFor.call(candidateName).then(function(v) {
        console.log(v.toString());
        $("#" + div_id).html(v.toString());
        $("#msg").html("");
      });
    });
  });
}

第三步:在智能合约中验证投票详情

现在我们在智能合约中验证一下提交的投票信息是否有效,并记下这次投票。

pragma solidity ^0.4.18;
import "./ECRecovery.sol";

contract Voting {
  using ECRecovery for bytes32;

  mapping (bytes32 => uint8) public votesReceived;

  mapping(bytes32 => bytes32) public candidateHash;

  mapping(address => bool) public voterStatus;

  mapping(bytes32 => bool) public validCandidates;

  function Voting(bytes32[] _candidateNames, bytes32[] _candidateHashes) public {
    for(uint i = 0; i < _candidateNames.length; i++) {
      validCandidates[_candidateNames[i]] = true;
      candidateHash[_candidateNames[i]] = _candidateHashes[i];
    }
  }

  function totalVotesFor(bytes32 _candidate) view public returns (uint8) {
    require(validCandidates[_candidate]);
    return votesReceived[_candidate];
  }

  function voteForCandidate(bytes32 _candidate, address _voter, bytes _signedMessage) public {
    require(!voterStatus[_voter]);

    bytes32 voteHash = candidateHash[_candidate];
    address recoveredAddress = voteHash.recover(_signedMessage);

    require(recoveredAddress == _voter);
    require(validCandidates[_candidate]);

    votesReceived[_candidate] += 1;
    voterStatus[_voter] = true;
  }
}

Zeppelin有一个ECRecovery库,十分便捷,我们可以用来验证已签名的信息。voteForCandidate函数可以验证已签名的消息(recover函数)并在验证完成后更新投票数。

希算法对消息进行处理?solidity recover函数无法知晓eth_signTypedData内使用的哈希函数,所以它无法验证“Vote for Alice”这条信息。它必须生成“Vote for Alice”的哈希值,然后再进行验证。我们不在合约内生成哈希,而是对所有消息进行哈希预处理并将其传递给构造函数(constructor),这样在验证时就很容易查找了。生成哈希的代码见以下迁移文件。

var Voting = artifacts.require("./Voting.sol");
var ECRecovery = artifacts.require("./ECRecovery.sol");

const sigUtil = require("eth-sig-util")

var alice_vote_hash = sigUtil.typedSignatureHash([{ type: 'string', name: 'Message', value: "Vote for Alice"}])
var bob_vote_hash = sigUtil.typedSignatureHash([{ type: 'string', name: 'Message', value: "Vote for Bob"}])
var carol_vote_hash = sigUtil.typedSignatureHash([{ type: 'string', name: 'Message', value: "Vote for Carol"}])

module.exports = function(deployer) {
  deployer.deploy(ECRecovery);
  deployer.link(ECRecovery, Voting);
  deployer.deploy(Voting, ['Alice', 'Bob', 'Carol'], [alice_vote_hash, bob_vote_hash, carol_vote_hash]);
};

这便是运行新应用的全部代码!

我创建了一个简易的应用来演示这个应用是如何运行的:

完整的操作码在这儿:https://github.com/maheshmurthy/ethereum_voting_dapp/tree/master/chapter4

演示应用在此:https://www.zastrin.com/voting-dapp-without-paying-gas.html

 

尚需解决的潜在问题

 

真正利用该技术构建DAPP时,还需要考虑一些问题,比如:

1、已签名的消息存储在哪儿?可以利用一些排队系统(queuing system)来存储这些消息。

2、如何确保已签名的消息最终提交给了区块链?

3、既然将所有消息的哈希都存储在区块链上还不够,那么最好的解决方案是什么?

注:显然,以太币signTypedData API尚不稳定,目前只有metamask在用。如果你计划在mainnet/production上使用这一技术,请注意这点。

作者简介 Mahesh Murthy

技术专家、美食家、旅行家。

 

项目采访

首发|发币谁都会,你会发“货”么?是时候用Token干点“正经事”了电商“有病”,区块链“有药”吗?

正逢数字货币乱世,他们想用技术来保护投资人的最基本权益

数字货币交易所何去何从?

用区块链技术实现影响力的商业价值

用区块链技术拉起股权交易市场新的增长曲线

基于智能合约和区块链技术的创新信贷交换平台

专家专栏

既要懂技术又懂产业,2018将是区块链正规军入场元年

硅谷资深投资人讲析区块链项目投资|教程

王玮:区块链通证架构的思辨

软件好,才是真的好:区块链的1976—2017

信仰和投机:币圈没有奇迹

与元道对话三:区块链经济正在进行“动力切换”

百家观点

如何设计区块链项目的通证(token)模型

加密货币和区块链(一):历史的重演

裸照与区块链社群

疯狂的韩国比特币市场:“全民”炒币,人均收益率425%

开年反攻:泡沫中的token和被冷落的联盟链

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注