区块链技术-智能合约-以太坊(译文修改版)

Author Avatar
patrickcty 12月 03, 2017

本文仅仅是出于学习的目的,对 King Wang 的译文中的代码部分根据当前版本的工具进行了更新。


本文征得IBM的Glynn Bird同意翻译发表。虽然文章没有很深的理论,但是它用简洁的语言描述了区块链技术,智能合约,以及以太坊。文章用了一个可以运行的慈善募捐实例,把以太坊最核心的功能做了演示。


区块链(BlockChain)技术吸引了大量的关注,原因不仅是它作为账本来纪录交易,成为加密货币(例如比特币)的引擎;更重要的是,它可以封装代码和数据,成为“智能合约”(Smart Contract)。本文介绍开源区块链技术的一种:以太坊(Ethereum), 并且以慈善募捐为例构建一个智能合约,来演示这种技术的强大之处。

什么是区块链

区块链是一种数据结构,把数据按时间顺序存储在可以无限伸长的链表中,就像一个账本。区块链数据结构通过分布式,无中心“主”节点,点对点的计算机网络来维护。链表中的每个区块包含一定数量的交易,交易代表数据库状态的改变,例如,钱从一个帐号转到另一个。交易由网络中的多个节点验证,并且最终存储在区块链的区块里面。每个区块包含一个签名哈希(Signed Hash),包含的是链表中前一个区块的内容。遍历整个区块链可以验证:某个区块的哈希确实是存储在链表中后一个区块里。区块链用图来表示是这样子的:

The Genesis Block是最原始的区块。H()是哈希。Transaction是交易。Time是时间轴,从原始区块链到无限。

区块链结构

区块链包含一系列价值的转移,从一个地址转到另一个。一个地址代表系统中唯一的一个帐号。地址实际上是一个公钥,它的对应私钥属于创建账号的那个用户。没有用账户的私钥做数字签名,价值就不能从那个账户中转移。

在加密保护交易的同时,区块链还提供数据库状态的分布共识。它保证价值转移要么发生一次,要么不发生。应用开发人员可以放心:因为数据一旦存储,就不能修改,可以信赖。区块链网络抽取一个计算机节点,在区块链中产生下一个区块,这个节点要花费大量的算力解决一个数学任务,这个节点是事先未知的。这个节点找到了问题的解答,可以命名下一个区块并发布,网络中的其它节点验证这个发布的区块。由于证明了工作量(Proof of Work), 赢得发布的节点得到两种奖励:获得新铸成的加密货币,以及从创建交易的一方收取费用。这个过程称为“挖矿”,它的目的是:

  1. 铸成新的加密货币,而产生货币的速率是受严格控制的
  2. 奖励“矿工”,“矿工”验证了交易以及在网络中达成共识

实践中,有非常快的特殊硬件,挖矿就有优势,所以就有了“军备竞赛”:矿工们采用越来越多的硬件,来维持同样的获取货币的速率。

比特币是最有名的区块链的实用例子。比特币是可以交换真实货币的加密货币。它可以通过区块链交易,在比特币账号(钱包)之间转移。本文写作时,比特币点对点网络有7000个节点。

以太坊和智能合约

区块链吸引应用开发人员的主要的性质有:

  1. 区块链由别人运营。如果你的应用把数据存在大家都用的区块链,它就不需要为数据提供存储机制。区块链中的节点提供存储有激励:收交易费和有机会铸新币。
  2. 区块链提供分布共识机制,你自己做很难。
  3. 区块链为用户提供匿名机制。一个账号身份标识(ID)是个公钥,它不一定和人直接相关。
  4. “价值”可以从一个账号转移到另一个,但是费用要小于传统的真实货币转账机制。
  5. 如果某个区块链受信赖,那么存储在那个区块链的交易也受信赖。

以太坊采用区块链的原理,又增加了在区块链上创建智能合约:智能合约是一种应用,它能保存价值,存储数据,封装代码,执行计算任务。类似比特币,以太坊也含有货币,称为以太(ether)。以太是计算机节点挖出来的,由节点验证交易,交易存储在分布共识的区块链中。以太可以在账户(公钥)之间以及智能合约之间转移。

智能合约允许匿名的几方进入约束协议,每个参与方对交易完全知情。价值可以在账户间转移,或者放在智能合约中的第三方托管(escrow )。由于合约就是代码,开发者想做什么应用,就能做得出,你的想象力是唯一的限制。

智能合约的例子:智能赞助(smartSponsor)

在本文的剩余部分,我们构建一个智能合约,它可以让持有账户的用户做以下互动:

  1. 一个慈善机构搞募捐,称为受益者thebenefactor
  2. 一个赞助实施方为这个慈善机构募捐,称为实施者therunner
  3. 想要提供赞助的用户,称为赞助者thesponsor
  4. 一个以太坊区块链的挖矿节点,验证交易,称为矿工theminer

我们的称为智能赞助(smartSponsor)的合约(contract)是:

  1. 实施者therunner通过一个募捐活动为慈善机构募捐
  2. 构建合约时,实施者therunner命名接受募捐的受益者thebenefactor
  3. 实施者therunner然后邀请用户提供赞助,用户调用智能合约的函数(function), 这个函数的功能是从赞助者thesponsor的账号转移以太到合约(contract)中,这些以太沉淀在合约中,直到有回调发生
  4. 在合约的整个生命周期,各方都可以看到谁是受益者thebenefactor,募到了多少以太,从谁哪儿募到(当然赞助者thesponsor可以匿名)

合约模型

合约定了以后,两种现象必有其一:

1.募捐照计划实施,实施者指示合约把全部募捐转移给受益者

合约模型2

2.因为某种原因募捐实施不了,实施者指示合约把赞助者承诺的募捐退还

合约模型3

以太坊允许智能合约用代码定义,代码使用一种叫Solidity的编程语言。这是一种类似Java的语言。合约就像Java类,区块链交易存储在成员变量中,可以调用合约的方法来查询合约,或者改变合约的状态。由于区块链的拷贝分布在网络中所有的节点,任何人可以查询合约来获取合约中的公有信息。

我们的合约会包含以下方法:

  1. smartSponsor –合约的构造器。它初始化合约的状态。合约的构建者命名账户的地址,这个账户会在合约提款时得益。
  2. pledge –可以被任何捐赠以太的赞助者调用。赞助者可以提供支持募捐的信息,但这个不是必需的。
  3. getPot –返回合约中当前的以太总量。
  4. refund –把承诺的募捐退还给赞助者。只有合约的拥有者能够调用这个方法。
  5. drawdown –把合约中的全部募捐转移到受益者账户。同样,只有合约的拥有者能够调用这个方法。

我们的想法是合约要有约束:如果赞助者把以太转移到合约,除非整个合约退回,否则赞助者不能取回。如此,所有的数据都公开可取,意思是任何能够访问以太坊的人,通过合约的代码,能够看到谁设置了合约,谁是受益者,谁承诺了哪一笔捐赠。

重要的是,所有改变合约状态的操作(构建,承诺,退还,或者把合约中的全部募捐转移到受益者账户)都需要在区块链中创建交易。这就意味着,交易被“挖矿”和区块被存储以前,数据还没有被存储。有些操作只是读取已经存在的合约状态(getPot或者读取公共成员变量),这些操作不需要挖矿。这点重要而微妙:写操作很慢(我们必须等到挖矿完成)。或许写操作结果最终进不了区块链(如果你的代码抛出一个异常或者有其他的错误),需要调用者给矿工干活的激励。这在以太坊术语中称为gas。所有写操作需要支付gas来改变区块链的状态。

我们有幸不需要加入太坊网络和购买以太,就能使用相同的软件,配置一个本地测试区块链,运行一个矿工来产生自己所谓的以太。这样我们不用浪费真的以太,来测试我们的代码。

Solidity 代码

这里是用Solidity语言写的我们的智能合约的全部代码:(注:这是修改后的代码)

pragma solidity ^0.4.0;
// Smart contract to allow someone to organise a sponsored event for charity
// Glynn Bird - 2016

contract smartSponsor {
  address public owner;
  address public benefactor;
  bool public refunded; 
  bool public complete;
  uint public numPledges;
  struct Pledge {
    uint amount;
    address eth_address;
    bytes32 message;
  }
  mapping(uint => Pledge) public pledges;
  
  // constructor
  function smartSponsor(address _benefactor) public {
    owner = msg.sender;
    numPledges = 0;
    refunded = false;
    complete = false;
    benefactor = _benefactor;
  }

  // add a new pledge
  function pledge(bytes32 _message) public payable {
    if (msg.value == 0 || complete || refunded) revert();
    pledges[numPledges] = Pledge(msg.value, msg.sender, _message);
    numPledges++;
  }

  function getPot() public constant returns (uint) {
    return this.balance; 
  }

  // refund the backers
  function refund() public {
    if (msg.sender != owner || complete || refunded) revert();
    for (uint i = 0; i < numPledges; ++i) {
      pledges[i].eth_address.transfer(pledges[i].amount);
    }
    refunded = true;
    complete = true;
  }

  // send funds to the contract benefactor
  function drawdown() public {
    if (msg.sender != owner || complete || refunded) revert();
    benefactor.transfer(this.balance);
    complete = true;
  }
}
  • Pledge结构描述一个捐赠,记录赞助者的账号,赞助值,和一个消息字符串
  • pledges数组记录一组Pledge
  • 所有contract的成员变量都是公共的,因此它们的getter是自动产生的
    有些函数调用throw来避免在出错时,数据被存储到区块链
    请注意,代码没有提及交易,区块,gas,或者区块链以及加密货币的任何术语。代码仅仅存储成员变量的状态。以太坊仅仅创建了必要的交易,提交给网络验证(本例采用了我们的测试网络),然后存储到区块链。所有复杂的东西,都避免让我们看到。结果我们的代码很少(50行),又容易理解。

这个很重要,因为智能合约就是信赖的共享;合约中的所有参与方应该清楚他们的承諾,募捐去了哪里,谁可以做哪些操作。代码越简单,越容易验证合约是否值得信赖。

运行智能合约

要运营合约,先要把以太坊跑起来。我的Ubuntu服务器安装指南在https://github.com/glynnbird/smartsponsor。 我使用的是IBM的Bluemix虚拟机(译注:这个不是必须的),再用apt-get加些需要的包。

假定你根据我的安装指南,在你的测试网络创建了4个以太坊账号,以及配置了一个挖矿进程, 我们可以拷贝smartSponsor代码,在以太坊控制台执行(也就是说打开了两个窗口,一个用来挖矿另一个执行导入合约的代码):

> git clone https://github.com/glynnbird/smartsponsor.git  // 注:代码有修改
> cd smartsponsor
> geth attach

从geth控制台,执行和以太坊API交互的JavaScript命令

> loadScript("./smartsponsor.js")
Contract transaction send: TransactionHash: 0xe797ce5c1e5eeaae6e4bd09ad6564f9deba1beeeb7f09b6c16eec728584e370c waiting to be mined...
true
> Contract mined! Address: 0x15590c0417f6421fd35e113db0fdb2055df2344b
[object Object]

其中 JavaScript 文件修改成了这篇文章的情况。

smartsponsor.js文件创建了一些变量,它们是我们创建的4个以太坊坊账号的地址(theminer, therunner, thebenefactor, thesponsor),这样接下来的代码片段中,谁做了什么,就容易理解了。该文件还包含一系列的指令,用来编译Solidity源码,构建smartSponsor合约,产生therunner合约实例,确保合约的受益者是thebenefactor。

直到交易被挖矿了,合约才变活。这个需要几秒或几分钟,取决于运行计算机的速度。让我们来观察一下合约(合约赋给了变量ss):

> ss
{
  address: "0xe021f45922e141f5e17d05a4b2721ec972065960",
  transactionHash: "0x77ba5bc77f0a62888c08084a7c00cf00b6cc024f88f988e9daada751788c8693",
  allEvents: function(),
  benefactor: function(),
  complete: function(),
  drawdown: function(),
  getPledge: function(),
  getPot: function(),
  numPledges: function(),
  owner: function(),
  pledge: function(),
  refund: function(),
  refunded: function()
}

我们可以看到合约有一个address,意味着它能收发以太值,以及一个transactionHash,用它来找到区块链中的位置。公共的,可对合约调用的函数也列出了。现在让我们调用几个:

> ss.benefactor()
"0x63de8807ac0bd63be460be0de250749c4df1dcb0"
> ss.owner()
"0x458305055882d53663b41a00eebd0b657469843f"
> ss.getPot()
0
> ss.numPledges()
0
> ss.complete()
False

我们可以看到合约的拥有者和受益者是不同的账号(分别是therunner和thebenefactor),合约的状态初始化为没有募捐和赞助。从自己的区块链拷贝做读操作,是免费的,所以我们不需要提供gas。

接下来我们寄给thesponsor账号一些钱,原先账号是没有以太的:

> personal.unlockAccount(thesponsor,"password");
> eth.sendTransaction({from: theminer, to: thesponsor, value: 100000000000000000});
"0xd4fc641311e31abb6546c3503c367c6ac971b0ad9cb4bcd4c56597e3b98d6d7a"
> eth.getBalance(theminer);
4.9524805801917e+22
> eth.getBalance(thesponsor);
100000000000000000

接下来认证thesponsor用户,给我们的智能合约赞助一些钱:

> personal.unlockAccount(thesponsor,"password");
true
> ss.pledge("Good luck with the run!", {from: thesponsor, value: 10000000, gas: 3000000});
"0xc0880c4151946014389e135bcbefe39fb8f786e9e3e0ce077fa5f967e2a31ab3"

价值参数是我们希望转移到合约的以太数量。10000000似乎很大,但单位是wei。1个以太有1000000000000000000个wei!

返回值是交易标识。我们必须等到交易存储到区块链,才能看到合约状态改变了:

> ss.getPot()
10000000
> ss.numPledges()
1
> ss.pledges(0)
[10000000, "0x225905462cf12404757852c01edfd2ec0bf0dbe9", "0x476f6f64206c75636b2077697468207468652072756e21000000000000000000"]

调用pledges(0)返回第一个捐赠pledge,包含它的价值,赞助者的地址,和消息(以一串字节表示).我们可以不断地调用pledge函数来增加捐赠,并观察pot变大。在7个赞助者捐赠以后,我们有:

> ss.getPot()
70000000

请注意,合约得到了赞助者承诺的全部以太数量,但是赞助者的账号被扣除了比承诺稍多一点的价值。为什么?因为调用pledge函数的操作,必须提供gas作为动力。
当实施者准备完成合约,只需调用drawdown函数:

> personal.unlockAccount(therunner,"password");
true
> ss.drawdown({from: therunner, gas:3000000});
"0x082424d8057b8c250f8b86cda05211628bb3bae513ce27bf6194445ae035a3c4"

合约挖矿完成后,我们应该看到受益者的账号收到了合约的捐赠数量:

> eth.getBalance(thebenefactor);
70000000
> ss.getPot()
0

补充:在另一个窗口中打开智能合约:

如果想在另一个窗口中打开此智能合约则需要记录下合约的 abi 以及 address:

> abi = your_abi
> sample = eth.contract(abi)
> ss2 = sample.at("your_address")
> ss2.getPot
0

智能合约在合约里的第三方托管记录多个捐赠,要么转移到受益者,要么退还给赞助者。代码确保只有合约的创建者可以退还捐赠,或者全部转移给受益者,并且防止合约完成后再加捐赠。整个过程中,合约的状态可以被各方查询,这些都包含在50行代码里面!

用geth行命令工具执行的命令实际上是Javascript语句。你用自己的客户端代码同样可以调用远端API,和真实(或测试)网络进行交互。这样,创建基于互联网的智能合约前端就很简单。

你用Mist浏览器可以创建帐户,查看和操作合约,就像一个智能合约的应用商店。Mist设想了很多计划,但目前它是一个相对简单的钱包应用,以及合约浏览器。

区块链仅仅是一个分布式数据库?

区块链是一种按时间顺序存储数据的账本,它是跨越多个结点的分布式数据库。但和Cloudant分布式数据库的意义不同:它并不是把数据分割成片,使工作量散布,从而每片就能搞定一部分。它的网络中,所有节点必须处理所有的数据变化。还有,挖矿的节点执行工作量证明(proof-of-work),来证明节点有资格实施所预计的改变,这个造成写数据的过程极度缓慢,以太坊每秒只能处理20-30个交易(不是每个用户20-30个交易,而是全部区块链)。

以太坊不仅是一个数据仓库。它增加了新的功能:在区块链中封装代码和数据。这样参与各方可以确信一个合约能够说到做到。与签过名的纸上合约,律师,公证,银行,保险,清算相比,大有不同。

区块链和智能合约能做什么?

以太坊只是智能合约平台的一种,用它可以构建基于区块链的应用。最合适的用例有:

  • 从一方转移价值到另一方
  • 一方或几方需要匿名
  • 价值必须存储在合约本身,并且存储一段时间
  • 希望避免转移真实货币产生的交易费用 需要建立共识
  • 希望显示公开性,把状态存储在公共域
  • 需要确保物理或虚拟材料的来源和真实性

马上想到的应用有:拍卖,借贷,遗嘱,注册,众筹,股权,和投票。

从金钱和计算角度看,写操作是很贵的,所以智能合约多用在写操作少,但是数据价值高的应用。尽管读操作是免费的,只存在一些简单的查询操作。你可以给存储的数据加索引,但还不像一般的数据库,它不存在查询语言,也不存在抽取或聚合数据的方式。

区块链还有另外一些缺点。所有网络中的参与方要存储所有的区块,数据不是分割成可以分别搞定的片段,所以每个节点必须存储整个数据库,并且处理每一个变化。用工作量证明模型,在网络中分布信赖,是一种聪明的迂回解决方案,但是实际情况下,要用千万个节点消耗能量来证明节点值得信赖。最后一点,以太的价值,就像比特币,买卖受投机影响,使得以太的金融价值不稳定。以太坊在发展规划中有性能提升,可扩展性,替换工作量证明,但是在笔者写此文时,它们还只是愿景。

原文链接

中文原文:http://ethfans.org/posts/block-chain-technology-smart-contracts-and-ethereum