基于以太坊智能合约的Dapp开发与实践

环境准备

安装Go-lang
安装go-ethereum 1.8+
Atom编译器(optional)安装solidity插件
安装python3 + pyCharm(optional)安装web3.py模块
安装Truffle (optional)

1, 定制创世区块

1.1 创建工作目录

创建工作目录“devnet”,创建两个节点目录

$mkdir devnet
$cd devnet
$mkdir node1 ndoe2

1.2 生成账户

初始化两个节点的以太坊账户与密码

$geth --datadir node1/ account new
$geth --datadir node2/ account new
$echo {ACCOUNT1} >> accounts.txt
$echo {ACCOUNT2} >> accounts.txt
$echo {PASSWD1} > node1/password.txt
$echo {PASSWD2} > node2/password.txt

1.3 创建创世区块

使用以太坊自带工具puppeth生成创世区块

$puppeth
Please specify a network name to administer (no spaces, please)

> devnet

What would you like to do? (default = stats)

1. Show network stats

2. Configure new genesis

3. Track new remote server

4. Deploy network components

> 2

Which consensus engine to use? (default = clique)

1. Ethash - proof-of-work

2. Clique - proof-of-authority

> 2

How many seconds should blocks take? (default = 15)

> 3 // for example

Which accounts are allowed to seal? (mandatory at least one)

> 0x87366ef81db496edd0ea2055ca605e8686eec1e6 //copy paste from account.txt :)

> 0x08a58f09194e403d02a1928a7bf78646cfc260b0

Which accounts should be pre-funded? (advisable at least one)

> {ACCOUNT1} // free ethers !

> {ACCOUNT2}

Specify your chain/network ID if you want an explicit one (default = random)

> {NETWORK_ID} // for example. Do not use anything from 1 to 10

Anything fun to embed into the genesis block? (max 32 bytes)

>

What would you like to do? (default = stats)

1. Show network stats

2. Manage existing genesis

3. Track new remote server

4. Deploy network components

> 2

1. Modify existing fork rules

2. Export genesis configuration

> 2

Which file to save the genesis into? (default = devnet.json)

> genesis.json

INFO [01-23|15:16:17] Exported existing genesis block

What would you like to do? (default = stats)

1. Show network stats

2. Manage existing genesis

3. Track new remote server

4. Deploy network components

> ^C // ctrl+C to quit puppeth

生成了genesis.json到当前路径

2, 节点初始化

2.1 用创世文件初始化节点

$geth —datadir node1/ init genesis.json
$geth —datadir node2/ init genesis.json

2.2 初始化boot节点

$bootnode -genkey boot.key

3, 开始挖矿

3.1 开启bootnode

$ bootnode -nodekey boot.key -verbosity 9 -addr :30310 // 请勿使用 30303

3.2 开启矿工节点

$geth --datadir node1/ --syncmode 'full' --port 30311 --rpc --rpcaddr 'localhost' --rpcport 8551 --rpcapi 'personal,db,eth,net,web3,txpool,miner' --bootnodes 'enode://3ec4fef2d726c2c01f16f0a0030f15dd5a81e274067af2b2157cafbf76aa79fa9c0be52c6664e80cc5b08162ede53279bd70ee10d024fe86613b0b09e1106c40@127.0.0.1:30310' --networkid 1515 --gasprice '1' --unlock '0x87366ef81db496edd0ea2055ca605e8686eec1e6' --password node1/password.txt --mine

$ geth --datadir node2/ --syncmode 'full' --port 30312 --rpc --rpcaddr 'localhost' --rpcport 8552 --rpcapi 'personal,db,eth,net,web3,txpool,miner' --bootnodes 'enode://3ec4fef2d726c2c01f16f0a0030f15dd5a81e274067af2b2157cafbf76aa79fa9c0be52c6664e80cc5b08162ede53279bd70ee10d024fe86613b0b09e1106c40@127.0.0.1:30310' --networkid 1515 --gasprice '0' --unlock '0x08a58f09194e403d02a1928a7bf78646cfc260b0' --password node2/password.txt --mine

控制台命令参数解析:

--datadir 指定链的数据位置

--syncmode 

    ‘full’:节点同步所有区块头 区块数据以及签名 ,并验证区块

    ‘fast’:节点同步区块头和区块数据,不做任何校验,类似于“快照”

    ‘light’:节点直接从其他‘full’节点同步当前状态

--rpcapi 会使用那些api与此节点交互

--rpcaddr 这个节点监听从哪里发来的rpc请求

--rpcport 这个节点监听从哪个端口发来的rpc请求

--bootnodes 指定bootnode ID

--networkid 同一个链上节点networkid相同

--gasprice 这个矿工节点可以接受的最小gas

--mine 开始挖矿

3.3 创建第三个节点

$mkdir node3
$geth --datadir node3/ account new
$echo {ACCOUNT3} >> accounts.txt
$echo {PASSWD3} > node3/password.txt
$geth --datadir node3/ init genesis.json

3.4 已存在节点投票加入新节点

进入node1的控制台

$geth attach node1/geth.ipc

投票加入node3的地址

>clique.propose( {ACCOUNT3}, true)

>exit

同上

$geth attach node2/geth.ipc

>clique.propose( {ACCOUNT}, true)

>exit

$ geth --datadir node3/ --syncmode 'full' --port 30313 --rpc --rpcaddr 'localhost' --rpcport 8553 --rpcapi 'personal,db,eth,net,web3,txpool,miner' --bootnodes 'enode://3ec4fef2d726c2c01f16f0a0030f15dd5a81e274067af2b2157cafbf76aa79fa9c0be52c6664e80cc5b08162ede53279bd70ee10d024fe86613b0b09e1106c40@127.0.0.1:30310' --networkid 1515 --gasprice '0' --unlock '0x08a58f09194e403d02a1928a7bf78646cfc11ab3' --password node3/password.txt --mine

4,智能合约

4.1 EVM基础

账户

以太坊中有两种不同类型但是共享同一地址空间的账户:外部账户由一对公私钥控制,合约账户由账户内部的合约代码控制。

外部账户的地址是由公钥(经过hash运算)决定的,而合约账户的地址在此合约被创建的时候决定的(由合约创建者的地址和发送到此合约地址的交易数决定,这就是所谓的“nonce”)不管是哪种类型的账户,EVM的处理方式是一样的
每个账户都有一个持久的key-value类型的存储,把256字节的key映射到256字节的value
此外,每个账户都有以“Wei”为单位,在交易过程中会被修改的资产(balance)信息

交易

交易是一个从账户发往另一个账户(可以是同一个账户或者是special zero-account)的消息。它包含二进制数据(交易相关的数据)and Ether。
如果目标账户包含代码,代码会被执行,交易相关的数据将作为参数
如果目标账户是地址为0的账户zero-account, 交易会创建一个新的合约。如上文提到的,合约地址不是一个地址为0的地址,而是一个由交易发送者和交易数来决定的地址。这样的一笔(到zero-account)交易的相关参数会被转化为EVM字节码然后被执行,输出结果就是被永久存储的合约代码。这意味着为了创建一个合约,并不需要发送真实的合约代码,代码可以被自动创建

费用

创建之后,每笔交易都需要一定数量的费用,用于限制交易所消耗的工作量,即交易是需要付出代价的(避免DDoS攻击)。EVM执行交易的过程中,费用会按一个特殊规则逐渐减少
费用的多少是由交易发起者设置,至少需要从发起账户支付gas_price * gas用费。如果交易执行完毕费用还有剩余的,将退回到发起账户。
如果交易完成之前费用耗尽,将会抛出一个out-of-gas的异常,所有的修改都会被回滚

Storage,Memory,Stack

每个账户都有一个持久的内存空间,称之为storage,storage以key-value形式存储,256字节的key映射到256字节value,合约内部不可能枚举storage(内部元素),读取或者修改storage操作消耗都很大(原文是 It is not possible to enumerate storage from within a contract and it is comparatively costly to read and even more so, to modify storage. )。 合约只能读取和修改自己的storage里的数据。
第二种内存空间称之为memory,里面存储着每个消息调用时合约创建的实例。memory是线型的,可以以字节级别来处理,但是限制为256字节宽度,写入可以是8或256字节宽度。当读取或写入一个预先未触发的指令的时候会消耗memory的空间,消耗空间的同时,必须支付费用(gas)。memory消耗的越多,手续费越多(按平方级增长)
EVM不是一个注册的机器而是一个堆栈机器,所以所有的计算指令都在stack空间里面执行。stack最多只能容纳1024个长度不超过256字节的指令元素。只能用下述方法,从顶部访问stack:可以拷贝最顶部的16个元素中的一个到stack的最顶部,或者将最顶部的那个元素与其下面的16个元素之一互换。所有其它操作从stack最顶部取出两个(或一个,或更多,取决于操作)元素,然后把结果push到stack顶端。当然将stack中的元素移到memory或者storage也是可以的,但是不能直接访问stack中间的元素(必须从头部开始访问)

4.2 Solidity代码实例

pragma solidity ^0.4.18;
contract Ballot{

    address founder;  // 用于记录谁部署的合约
    struct Proposal{
        string name;    //提案的名称
        uint count;     // 用于统计票数
    }
    Proposal[] public proposals;  // 数组,用来保存所有提案
    mapping(address => uint8) voters;  // 映射,用来保存所有投票者

    function Ballot() public {    // 构造函数,在合同部署时调用一次
        founder = msg.sender;     //记录构造函数的调用者,也就是合同部署人
    }

    
    function activeVoter(address voterAddr) public{
        if (founder != msg.sender){  // 判断,只允许部署合同的管理员新增投票者
            return;
        }
        if(voters[voterAddr] == 0){
            voters[voterAddr] = 1;
        }
        return;
    }

    function activeProposal(string proposalName) public{
        if (founder != msg.sender){    // 判断,只允许部署合同的管理员增加新提议
            return;
        }
        for (uint index = 0; index < proposals.length; index++){
            if(keccak256(proposals[index].name) == keccak256(proposalName)){    //  keccak256 就是 sha256
                return;
            }
        }
        proposals.push(Proposal({
            name: proposalName,
            count: 0 }));
        return;
    }

    // 所有被“激活”的投票者都可以投出一票 给指定提案
    function vote(string proposalName) public {
        address voter = msg.sender;
        if(voters[voter] != 1){
            return;
        }
        for(uint index = 0; index < proposals.length; index ++){  
            if(keccak256(proposals[index].name) == keccak256(proposalName)){   // 判断指定提案是否存在
                proposals[index].count++;
                voters[voter] = 2;
                return;
            }
        }
        return;
    }

    // 任何人都可以查询哪项提案获得的票数最高
    function getWinner() public constant returns(string winnerName, uint winnerCount) {
        winnerCount = 0;
        winnerName = "";
        for(uint index = 0; index < proposals.length; index ++){ 
            if(proposals[index].count > winnerCount){
                winnerName = proposals[index].name;
                winnerCount = proposals[index].count;
            }
        }
    }
}

5,Dapp的部署与交互

5.1 使用traffle 部署合约

进入一个空目录,执行

$truffle init

在contract目录下,新增.sol文件用来写合约代码
在migration目录下新增2_deploy_contracts.js文件,内容如下:

var YourContract = artifacts.require(“./你的合约文件名.sol");
module.exports = function(deployer) {
    deployer.deploy(YourContract);
};

本目录下执行

$truffle compile

在truffle项目下获取package.json

$npm init -f
安装web3

$npm install —save [email protected]

编辑 truffle.js 加入步骤1的node11节点信息

module.exports = {
    networks: {
    nodeth: {         // “nodeth” 是我给网络起的名字
        network_id: 999, // network id associated with your node
        host:'127.0.0.1',
        port:8811,   // same with node11 supported
        gas: 400000000,
        from: “0x6875483cd851990ddfcd5fd49f6732d71cbedb46”. // coinbase  for node11
        }
    }
};

Truffle 目录下执行命令

$truffle deploy — network nodeth        // “nodeth” 对应truffle.js 配置里的网络名 

第二次部署可以用· truffle migrate —reset —network {you Network}
在geth控制台终端执行挖矿,将会看到新合约被部署了,合约地址被返回

5.2 使用python部署合约 (推荐)

新建区块链部署模块deploy.py
引入如下模块

import time
import sys
from web3 import Web3, HTTPProvider
from solc import compile_source

将solidity 合约代码复制到字符串变量

_contract_source_code_ballot = '''
pragma solidity ^0.4.18;
contract Ballot{

    address founder;  // who found this contract
    struct Proposal{
        string name;
        uint count;  // count ballot
    }
    Proposal[] public proposals;  // All proposals
    mapping(address => uint8) voters;

    function Ballot() public {
        founder = msg.sender;
    }

    // Only founder could active voter
    function activeVoter(address voterAddr) public{
        if (founder != msg.sender){
            return;
        }
        if(voters[voterAddr] == 0){
            voters[voterAddr] = 1;
        }
        return;
    }

    // Only founder could active proposal
    function activeProposal(string proposalName) public{
        if (founder != msg.sender){
            return;
        }
        for (uint index = 0; index < proposals.length; index++){
            if(keccak256(proposals[index].name) == keccak256(proposalName)){
                return;
            }
        }
        proposals.push(Proposal({
            name: proposalName,
            count: 0 }));
        return;
    }

    // Any activated voter could vode
    function vote(string proposalName) public {
        address voter = msg.sender;
        if(voters[voter] != 1){
            return;
        }
        for(uint index = 0; index < proposals.length; index ++){
            if(keccak256(proposals[index].name) == keccak256(proposalName)){
                proposals[index].count++;
                voters[voter] = 2;
                return;
            }
        }
        return;
    }

    // Anyone could check who is getting the most votes
    function getWinner() public constant returns(string winnerName, uint winnerCount) {
        winnerCount = 0;
        winnerName = "";
        for(uint index = 0; index < proposals.length; index ++){
            if(proposals[index].count > winnerCount){
                winnerName = proposals[index].name;
                winnerCount = proposals[index].count;
            }
        }
    }
}

'''
编译合约源代码

# ------------- compile contract -------------
compiled_sol = compile_source(_contract_source_code_ballot)   # Compiled source code
contract_interface = compiled_sol[':Ballot']

使用web3调用区块链服务

w3 = Web3(HTTPProvider('http://127.0.0.1:8501'))   #指定了调用哪个节点

部署合约上区块链

# 编译后会生成 abi 和 字节码。
contract = w3.eth.contract(abi=contract_interface['abi'], bytecode=contract_interface['bin'])
account = w3.eth.accounts[0]
print(account)
tx_hash = contract.deploy(transaction={'from': account, 'gas': 4000000})
time.sleep(6)  // Wait new block confirmation
tx_receipt = w3.eth.getTransactionReceipt(tx_hash)
contract_address = tx_receipt['contractAddress']
print(contract_address)

提问:为什么要sleep(6)?

6 与区块链的交互

新建与链交互的模块 contactchain.py
引入如下模块

import time
from web3 import Web3, HTTPProvider
from solc import compile_source
from web3.contract import ConciseContract

与合约交互需要:1,合约在链上的地址;2,合约代码

# Solidity source code
_contract_source_code_ballot = '''
pragma solidity ^0.4.18;
contract Ballot{
    address founder;
    struct Proposal{
...
...

_contract_source_address_ballot = "0x580c04396aa683214034614e428935ad48De39A9"

现在我们模拟两位员工,主机上分别跑着一个以太坊节点

_web3_provider_employee1 = 'http://127.0.0.1:8501'
_web3_provider_employee2 = 'http://127.0.0.1:8502'

定义一个类 ContactChainBallot,其中声明active_voter, active_proposal, vote, get_winner, get_proposal 方法与链交互,在以上方法内部,分别调用了智能合约里定义的方法。

class ContactChainBallot:

    # Find the contract from chain
    def __init__(self, employee = "employee1"):
        if(employee == 'employee2'):
            web3_provider = _web3_provider_employee2
        else:
            web3_provider = _web3_provider_employee1
        compiled_sol = compile_source(_contract_source_code_ballot)
        contract_interface = compiled_sol[':Ballot']
        w3 = Web3(HTTPProvider(web3_provider))
        self.account = w3.eth.accounts[0]
        self.contract_instance = w3.eth.contract(contract_interface['abi'], _contract_source_address_ballot, ContractFactoryClass=ConciseContract)

    # -------------- interactive with contract -------------
    def active_voter(self, voter_address):
        self.contract_instance.activeVoter(voter_address, transact={'from': self.account, 'gas': 4000000})
        time.sleep(6)

    def active_proposal(self, proposal_name):
        self.contract_instance.activeProposal(proposal_name, transact={'from': self.account, 'gas': 4000000})
        time.sleep(6)

    def vote(self, proposal_name):
        return self.contract_instance.vote(proposal_name, transact={'from': self.account, 'gas': 4000000})

    def get_winner(self):
        return self.contract_instance.getWinner()

    def get_proposal(self, index):
        return self.contract_instance.proposals(index)

引用文章:
Go Ethereum
Proof Of Authority
智能合约介绍

你可能感兴趣的:(基于以太坊智能合约的Dapp开发与实践)