读书会 学习以太坊 Dapp 开发

jimi · 2018年06月12日 · 1381 次阅读

一、区块链

1. 分布式去中心化

比特币设计的初衷就是要避免依赖中心化的机构,没有发行机构,也不可能操纵发行数量。既然没有中心化的信用机构,在电子货币运行的过程中,也势必需要一种机制来认可运行在区块链上的行为 (包括比特币的运营,亦或是运行在区块链上的其他业务),这种机制就是共识机制。在完全去中心化的区块链上运行的比特币,采用的是 PoW(Proof of Work,工作量证明),该机制完美的解决了拜占庭将军问题(存在异常的情况下仍能达成一致)。因为基础网络架构为分布式,对单独一个节点是无法控制或破坏整个网络,掌握网内 51% 的运算能力 (非节点数) 才有可能操作交易,而这个代价大概要超过 270 亿美元。

2. 无须信任

整个区块链网络中的数据是公开透明的,每个节点 (参与者) 都可自由加入该网络中,下载到所有的数据。任意两个节点间的数据交换无需互相信任,完全依靠区块链中的交易历史和数据的可追溯,以及共识机制来保证数据交换的正确且不可逆的执行。

3. 不可篡改和加密安全性

跟当前银行网银系统 (特别是公司网银系统) 的加密机制类似,区块链的数据结构和交易流程中大量的使用了公私钥来加解密,保证数据的安全性。基于该技术基础,甚至可以应用群组签名来保证共有数据的安全性。任何事物既然有优点,也同时会存在不足之处。根源于分布式网络架构和共识机制,在区块链上运行的交易确认时间会比较长 (比特币的确认时间大概是 15 分钟),交易并发数受限 (比特币的每秒交易数为 7 笔,而淘宝的每秒并发数能达到 10 万左右),区块的容量限制 (当前为 1M,区块链的扩容一直在讨论中),监管难以介入,基于工作量证明的共识机制存在浪费系统资源和带宽的问题。

4. 区块链技术

a. 区块

区块是一个包含在区块链 (公开账簿) 里的聚合了交易信息的容器。它由一个包含元数据的区块头和紧跟其后的构成区块主体的一长串交易组成。区块头是 80 字节,而平均每个交易至少是 250 字节,而且平均每个区块至少包含超过 500 个交易。
区块结构如下图

交易 (Tx) 详情中的结构如下图

b. 区块链

当一个节点从网络接受到传入的区块时,它会验证这些区块,然后链接到现有的区块链上,链接的形态如下图:

由于每个区块包含前一个区块的 HASH 值,这就使得从创世块到当前块形成了一条块链,每个区块必定按时间顺序跟随在前一个区块之后,因为如果不知道前一块区块的 HASH 值就没法生成当前区块。要改变一个已经在块链中存在一段时间的区块,从计算上来说是不可行的,因为如果它被改变,它之后的每个区块必须随之改变。这些特性使得双花比特币非常困难,区块链是比特币的最大创新。

5. 比特币钱包

a. 比特币钱包的生成

  1. 首先使用随机数发生器生成一个 私钥 。一般来说这是一个 256bits 的数,拥有了这串数字就可以对相应 钱包地址 中的比特币进行操作,所以必须被安全地保存起来。
  2. 私钥经过 SECP256K1 算法处理生成了公钥。SECP256K1 是一种椭圆曲线算法,通过一个已知私钥时可以算得公钥,而公钥已知时却无法反向计算出私钥。这是保障比特币安全的算法基础。
  3. 同 SHA256 一样,RIPEMD160 也是一种 Hash 算法,由公钥可以计算得到公钥哈希,而反过来是行不通的。
  4. 将一个字节的地址版本号连接到公钥哈希头部(对于比特币网络的 pubkey 地址,这一字节为 “0”),然后对其进行两次 SHA256 运算,将结果的前 4 字节作为公钥哈希的校验值,连接在其尾部。
  5. 将上一步结果使用 BASE58 进行编码 (比特币定制版本),就得到了钱包地址。

流程图如下

b .转账

比特币钱包间的转账是通过交易(Transaction)实现的。交易数据是由转出钱包私钥的所有者生成,也就是说有了私钥就可以花费该钱包的比特币余额。生成交易的过程如下:

  1. 交易的原始数据包括 “转账数额” 和 “转入钱包地址”,但是仅有这些是不够的,因为无法证明交易的生成者对 “转出钱包地址” 余额有动用的权利。所以需要用私钥对原始数据进行签名。
  2. 生成 “转出钱包公钥”,这一过程与生成钱包地址的第 2 步是一样的。
  3. 将 “转出签名” 和 “转出公钥” 添加到原始交易数据中,生成了正式的交易数据,这样它就可以被广播到比特币网络进行转账了。

二、以太坊 Ethereum

1. 概念

a. 什么是以太坊

简单来说,以太坊是一种新的法律形式。现行法律的本质是一种合约。它是由(生活于某一社群的)人和他们的领导者之间所缔结的,一种关于彼此该如何行动的共识。个体之间也存在着一些合约,这些合约可以理解为一种私法,相应的,这种私法仅对合约的参与者生效。

例如,你和一个人订立合约,借给他一笔钱,但他最后毁约了,不打算还这笔钱。此时你多半会将对方告上法庭。在现实生活中,打官司这种事情常常混乱不堪并且充满了不确定性。将对方告上法庭,也通常意味着你需要支付高昂的费用聘请律师,来帮你在法庭上针对法律条文展开辩论,而且这一过程一般都旷日持久。而且,即使你最终赢了官司,你依然可能会遇到问题(比如,对方拒不执行法庭判决)。
令人欣慰的是,当初你和借款人把条款写了下来,订立了合约。但法律的制定者和合约的起草者们都必须面对一个不容忽视的挑战:那就是,理想情况下,法律或者合约的内容应该是明确而没有歧义的,但现行的法律和合约都是由语句构成的,而语句,则是出了名的充满歧义。
因此,一直以来,现行的法律体系都存在着两个巨大的问题:首先,合约或法律是由充满歧义的语句定义的,第二,强制执行合约或法律的代价非常大。
而以太坊,通过数字货币和编程语言的结合,解决了现行法律体系的这两大问题。
以太坊系统自身带有一种叫做以太币(Ether)的数字货币。以太币和著名的数字货币比特币(Bitcoin)有着非常多的相似之处。两者均为数字储值货币,且无法伪造,都以去中心化的方式运行来保证货币供应不被某一方所控制。两者都可以像电子邮件一样,作为货币自由地在全世界流通。而且,由于它们可以做到传统货币做不到的事情,因此用户对它们未来的价值充满期待 。

另外
详情请阅读以太坊白皮书(中文英文)。

b. 基本知识

  • 公钥加密系统。 Alice 有一把公钥和一把私钥。她可以用她的私钥创建数字签名,而 Bob 可以用她的公钥来验证这个签名确实是用 Alice 的私钥创建的,也就是说,确实是 Alice 的签名。当你创建一个以太坊或者比特币钱包的时候,那长长的 0xdf...5f 地址实质上是个公钥,对应的私钥保存某处。类似于 Coinbase 的在线钱包可以帮你保管私钥,你也可以自己保管。如果你弄丢了存有资金的钱包的私钥,你就等于永远失去了那笔资金,因此你最好对私钥做好备份。
  • 点对点网络。 就像 BitTorrent, 以太坊分布式网络中的所有节点都地位平等,没有中心服务器。
  • 区块链。 区块链就像是一个全球唯一的帐簿,或者说是数据库,记录了网络中所有交易历史。
  • 以太坊虚拟机 (EVM)。 它让你能在以太坊上写出更强大的程序(比特币上也可以写脚本程序)。它有时也用来指以太坊区块链,负责执行智能合约以及一切。
  • 节点。 你可以运行节点,通过它读写以太坊区块链,也即使用以太坊虚拟机。完全节点需要下载整个区块链。轻节点仍在开发中。
  • 矿工。 挖矿,也就是处理区块链上的区块的节点。这个网页可以看到当前活跃的一部分以太坊矿工:stats.ethdev.com。
  • 工作量证明。 矿工们总是在竞争解决一些数学问题。第一个解出答案的 (算出下一个区块) 将获得以太币作为奖励。然后所有节点都更新自己的区块链。所有想要算出下一个区块的矿工都有与其他节点保持同步,并且维护同一个区块链的动力,因此整个网络总是能达成共识。(注意:以太坊正计划转向没有矿工的权益证明系统 (POS),不过那不在本文讨论范围之内。)
  • 以太币。 缩写 ETH。一种你可以购买和使用的真正的数字货币。这里是可以交易以太币的其中一家交易所的走势图。在写这篇文章的时候,1 个以太币价值 65 美分。
  • Gas. 在以太坊上执行程序以及保存数据都要消耗一定量的以太币,Gas 是以太币转换而成。这个机制用来保证效率。
  • DApp. 以太坊社区把基于智能合约的应用称为去中心化的应用程序 (Decentralized App)。DApp 的目标是 (或者应该是) 让你的智能合约有一个友好的界面,外加一些额外的东西,例如 IPFS(可以存储和读取数据的去中心化网络,不是出自以太坊团队但有类似的精神)。DApp 可以跑在一台能与以太坊节点交互的中心化服务器上,也可以跑在任意一个以太坊平等节点上。这里分享一个以太坊 DApp教程,可以高效的学习如何开发一个 DApp,很适合入门。

2. 工作流程

a. 环境搭建

建议使用 Mac OS 环境,不然可能会出现各种坑。

  1. 安装NodeJS,安装Python
  2. 安装testrpc(测试环境中使用),安装go-ethereum(真实环境中使用)。
  3. 安装solc
  4. 安装truffle

如果是 windows 的话建议用工具 ethbox 可以一键安装以太坊开发环境的工具: ethbox

b. Solidity 语言简介

下面是官网上面的一段关于智能投票合约的示例代码

  contract Ballot {
   //一个选民的构造体
    struct Voter {
        uint weight; // 权重(即他可以投几票)
        bool voted;  //是否已经投过票
        address delegate; // 代表地址(他可以代表某个人进行投票)
        uint vote;   // index of the voted proposal
    }

    // 投票的提案的构造体
    struct Proposal
    {
        bytes32 name;   // 提案名称
        uint voteCount; //获得的票数
    }

    address public chairperson;//会议主席

    //地址 -选民 的map
    mapping(address => Voter) public voters;

    // 投票种类的动态数组
    Proposal[] public proposals;

    ///构造函数
    function Ballot(bytes32[] proposalNames) {
        chairperson = msg.sender;//初始化会议主席
        voters[chairperson].weight = 1;

       //初始化所有的提案
        for (uint i = 0; i < proposalNames.length; i++) {

            proposals.push(Proposal({
                name: proposalNames[i],
                voteCount: 0
            }));
        }
    }

    // 给予投票权
    function giveRightToVote(address voter) returns (bool b) {
        if (msg.sender != chairperson || voters[voter].voted) {
            //对于会议主席和已经投过票的选民这里不处理
            return false;;
        }
        voters[voter].weight = 1;
        return true;
    }

    /// 投票权转移函数
    function delegate(address to) {
        // 投票权转移的发起人
        Voter sender = voters[msg.sender];
        if (sender.voted)
            throw;

      //递归找到没有转移投票权的  选民
        while (
            voters[to].delegate != address(0) &&
            voters[to].delegate != msg.sender
        ) {
            to = voters[to].delegate;
        }

        if (to == msg.sender) {
            throw;
        }

       //将发起人设置为已经投过票的状态
        sender.voted = true;
        //将代表设置为刚才递归获取的选民
        sender.delegate = to;
        Voter delegate = voters[to];
        if (delegate.voted) {
            //如果代表已经投过票就在他投票的提案的票数增加
            proposals[delegate.vote].voteCount += sender.weight;
        }
        else {
           //将代表的的票数增加
            delegate.weight += sender.weight;
        }
    }

    /// 投票函数
    function vote(uint proposal) {
        Voter sender = voters[msg.sender];
        if (sender.voted)
            throw;
        sender.voted = true;
        sender.vote = proposal;

        //将投的提案票数增加
        proposals[proposal].voteCount += sender.weight;
    }

    ///获得票数最多的提案
    function winningProposal() constant
            returns (uint winningProposal)
    {
        uint winningVoteCount = 0;
        for (uint p = 0; p < proposals.length; p++) {
            if (proposals[p].voteCount > winningVoteCount) {
                winningVoteCount = proposals[p].voteCount;
                winningProposal = p;
            }
        }
    }
}

解读

  • address. 地址类型。chairperson 是会议主席的钱包地址。这个地址会在合约的构造函数 function Ballot() 中被赋值。很多时候也称呼这种地址为'owner'(所有人)。
  • public. 这个关键字表明变量可以被合约之外的对象使用。private 修饰符则表示变量只能被本合约 (或者衍生合约) 内的对象使用。如果你想要在测试中通过 web3.js 使用合约中的某个变量,记得把它声明为 public。
  • Mapping 或数组。mapping(address => Voter) 为选民钱包地址和选民构造体的键值对。Proposal[] public proposals 是一个提案构造体的数组。
  • 有特殊的变量和函数总是在全局命名空间存在,主要用于提供有关 blockchain 信息,例如 msg,block,tx,其中 msg.sender 为发起人的地址。

solidity 语言更深入的理解可以阅读官方文档

c. 使用 geth 部署合约

  1. 启动一个测试节点

geth --testnet --fast --cache=512 --genesis CustomGenesis.json console

这里的 CustomGenesis.json 是为了给测试的账户分配以太币

{
    "coinbase": "0x0000000000000000000000000000000000000000",
    "difficulty": "0x20000",
    "extraData": "",
    "gasLimit": "0x2fefd8",
    "nonce": "0x0000000000000042",
    "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "timestamp": "0x00",
    "alloc": {
        "0xe49c283bc6bf92c5833cc981b97679238dd3b5da": {
            "balance": "111111111000000000000000000000000000"
        },
        "0xd8927c296b3ebe454a6409770a0c323ec4ed23ba": {
            "balance": "222222222000000000000000000000000000"
        }
    }
}

solc 下的内容要替换成你的测试账户地址。具体 geth 的用法请查看官方文档源码介绍

  1. 使用 solc 编译智能合约,获得二进制代码 例如以下代码
contract test { 
      function multiply(uint a) returns(uint d) { 
             return a * 7; 
      }
}

在 geth 中输入

source = "contract test { function multiply(uint a) returns(uint d) { return a * 7; } }"

clientContract = eth.compile.solidity(source).test

编译返回的结果的 JSON 格式如下

其中,

  • code:编译后的 EVM 字节码
  • info:编译器返回的 metadata
  • abiDefination:Application Binary Interface 定义。具体接口规则参见这里
  • compilerVersion:编译此代码的 solidity 编译器版本
  • developerDoc:针对开发者的 Natural Specification Format,类似于 Doxygen
  • language:合约语言
  • languageVersion:合约语言版本
  • source:源代码
  • userDoc:针对用户的 Ethereum 的 Natural Specification Format。

编译器返回的 JSON 结构反映了合约部署的两种不同的路径。info 信息真实的存在于区中心化的云中,作为 metadata 信息来公开验证 Blockchain 中合约代码的实现。而 code 信息通过创建交易的方式部署到区块链中。

  1. 使用 solc 编译智能合约,获得二进制代码 部署合约前,确保你有一个解锁的账户并且账户中有余额,因为部署合约得过程中会消耗以太币。输入 web3.fromWei(eth.getBalance(eth.accounts[0]),"ether") 可以查看账户余额。 解锁一个账户

personal.unlockAccount(eth.accounts[0])

获得账户

primaryAddress = eth.accounts[0]

定义一个 abi(abi 是个 js 的数组,否则不成功)

abi = [{ constant: false, inputs: [{ name: 'a', type: 'uint256' } ]}]

创建智能合约

MyContract = eth.contract(abi)

发送交易部署合约

contract = MyContract.new({from: primaryAddress, data:"0x6060604052602a8060106000396000f3606060405260e060020a6000350463c6888fa18114601a575b005b6007600435026060908152602090f3"})

如果交易被 pending,如图说明你的 miner 没有在挖矿

启动一个矿工
miner.setEtherbase(eth.primaryAddress) //设定开采账户
miner.start(8)

eth.getBlock("pending", true).transactions
这时候发现交易已经在区块中

不过会发现,交易还是 pending,这是因为该交易区块没有人协助进行运算验证,这时候只需要再启动一个矿工就行了
miner.start(8)

  1. 与合约进行交互

Multiply7 = eth.contract(clientContract.info.abiDefinition);
var myMultiply7 = Multiply7.at(contract.address);
myMultiply7.multiply.call(3)

myMultiply7.multiply.sendTransaction(3, {from: contract.address})

d. 使用 truffle 框架

使用 truffle 部署智能合约的步骤:

  1. truffle init (在新目录中) => 创建 truffle 项目目录结构,
  2. 编写合约代码,保存到 contracts/YourContractName.sol 文件。 例如 Ballot .sol,此时要找到 migrations 文件夹,在 deploy_contracts.js 文件中添加deployer.deploy(Ballot);
  3. truffile compile 编译合约代码。
  4. 启动以太坊节点(例如在另一个终端里面运行 testrpc)。
  5. truffle migrate(在 truffle 项目目录中)。

1. 编写单元测试

test文件夹中新建 ballot.js 文件

contract('Ballot',function(accounts)){
    //accounts是所以账户得数值
    it("获取投票权",function(){
        var meta = Ballot.deployed();
        return meta.giveRightToVote(accounts[1]).then(function(b){
           assert.equal(Boolean(b),true,"获取投票权失败");
        });
    });

}

在项目根目录下运行 truffle test,你应该看到测试通过,如果使用自己构造的 ballot 对象,可以这样写:

contract('Ballot',function(accounts)){
    //accounts是所以账户得数值
    it("获取投票权",function(){
       var proposals = [];
       proposals.push("proposal0");
       Ballot.new(proposals).then(function(meta){
           return meta.giveRightToVote(accounts[1]).then(function(b){
               assert.equal(Boolean(b),true,"获取投票权失败");
            });
       });

    });

}

  • 合约中发送以太币。 this 是合约实例的地址,以变接下来检查这个地址的余额(或者直接使用 this.balance)
  • 当你通过 web3.js 调用交易函数时(使用 web3.eth.sendTransaction),交易并不会立即执行。事实上交易会被提交到矿工网络中,交易代码直到其中一位矿工产生一个新区块把交易记录进区块链之后才执行。因此你必须等交易进入区块链并且同步回本地节点之后才能验证交易执行的结果。用 testrpc 的时候可能看上去是实时的,因为测试环境很快,但是正式网络会比较慢。
  • Gas.(译注:以太坊上的燃料,因为代码的执行必须消耗 Gas。直译为汽油比较突兀,故保留原文做专有名词。)直到现在我们都没有涉及 Gas 的概念,因为在使用 testrpc 时通常不需要显式的设置。当你转向 geth 和正式网络时会需要。在交易函数调用中可以在{ from: _, value: _, gas: _ } 对象内设置 Gas 参数。Web3.js 提供了 web3.eth.gasPrice 调用来获取当前 Gas 的价格,Solidity 编译器也提供了一个参数让你可以从命令行获取合约的 Gas 开销概要:solc --gas YouContract.sol.

2. 为合约创建一个界面

app目录中,可以编写自己的 html 和 js 文件,js 与智能合约的交互与单元测试基本一致,例如一个界面上有一个输入框和一个按钮,获得选民的投票权。

<!DOCTYPE html>
<html>
<head>
  <title>Ballot App</title>
  <link href='https://fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet' type='text/css'>
  <link href="./app.css" rel='stylesheet' type='text/css'>
  <script src="./app.js"></script>
</head>
<body>
  <h1>Ballot</h1>
  <h2>Example Truffle Dapp</h2>

  <br>
  <h1>Send</h1>
  <br><label for="amount">Account:</label><input type="text" id="account" placeholder="e.g., 0x453468394hdfg84858345348"></input>

  <br><br><button id="getRightVote" onclick="getRight()">Get Right Vote</button>
  <br><br>
  <span id="status"></span>
</body>
</html>

app.js 中的代码为

function getRight() {
 var account = document.getElementById("account").value;
  var meta = Ballot.deployed();

   meta.giveRightToVote(account).then(function(b){
        if(Boolean(b)){
          setStatus("Get Right Vote Success");
        }else{
          setStatus("Get Right Vote Error");
        }
  }).catch(function(e){
    setStatus("Get Right Vote Error");
    console.log(e);
  });

};

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册