Dapp开发区块链区块链研习社

以太坊教程:学习以太坊Dapp开发

2018-06-12  本文已影响21人  编程狂魔

一、区块链

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)有着非常多的相似之处。两者均为数字储值货币,且无法伪造,都以去中心化的方式运行来保证货币供应不被某一方所控制。两者都可以像电子邮件一样,作为货币自由地在全世界流通。而且,由于它们可以做到传统货币做不到的事情,因此用户对它们未来的价值充满期待 。

另外
1.详情请阅读以太坊白皮书 (中文英文)。
2.以太坊教程

b. 基本知识

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;
            }
        }
    }
}

解读

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格式如下

其中,

编译器返回的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,"获取投票权失败");
            });
       });

    });

}

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);
  });

};

上一篇下一篇

猜你喜欢

热点阅读