环境准备
安装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
智能合约介绍