Ethereum (Blockchain)

Table of Contents

1 Ethereum简介

以太坊(Ethereum)是一个开源的、有智能合约功能的区块链平台。

参考:
Ethereum Documentation: http://www.ethdocs.org/en/latest/
以太坊区块链浏览器: https://etherscan.io/

1.1 Ethereum的各个阶段

Ethereum的各个阶段及其发布日期如表 1 所示。

Table 1: Ethereum Milestones
Version Code Name Release date
0 Olympic May, 2015
1 Frontier 30 July 2015
2 Homestead 14 March 2016
3 Metropolis 16 October 2017
4 Serenity N/A

以太坊的前三个阶段(Frontier前沿、Homestead家园、Metropolis大都会)采用的是工作量证明(POW)机制。以太坊的第四个阶段 (Serenity宁静)将采用Casper,它是一种权益证明(POS)机制。

1.2 Ethereum规范及其实现

Ethereum规范又称为“黄皮书”,参见:https://github.com/ethereum/yellowpaper

2 列出了Ethereum规范的一些实现。

Table 2: Ethereum clients
Client Language Developers Latest release
go-ethereum Go Ethereum Foundation go-ethereum-v1.4.18
Parity Rust Ethcore Parity-v1.4.0
cpp-ethereum C++ Ethereum Foundation cpp-ethereum-v1.3.0
pyethapp Python Ethereum Foundation pyethapp-v1.5.0
ethereumjs-lib Javascript Ethereum Foundation ethereumjs-lib-v3.0.0
Ethereum(J) Java ether.camp ethereumJ-v1.3.1
ruby-ethereum Ruby Jan Xie ruby-ethereum-v0.9.6
ethereumH Haskell BlockApps no Homestead release yet

1.3 以太币单位的转换

以太币的最小单位是Wei。以太币单位的转换如表 3 所示,其沿袭了科学界的传统,用做过杰出贡献的数学、密码学专家的名字命名。

Table 3: 以太币单位的转换
Unit Wei Value Wei
wei 1 wei 1
Kwei (babbage) 1e3 wei 1,000
Mwei (lovelace) 1e6 wei 1,000,000
Gwei (shannon) 1e9 wei 1,000,000,000
microether (szabo) 1e12 wei 1,000,000,000,000
milliether (finney) 1e15 wei 1,000,000,000,000,000
ether 1e18 wei 1,000,000,000,000,000,000

2 以太坊客户端geth

geth(全称为go-ethereum)是一个使用go语言编写的以太坊节点。

Mac中安装geth的步骤:

$ brew tap ethereum/ethereum
$ brew install ethereum

2.1 连接测试网络

geth支持两个测试网络:Ropsten和Rinkeby,这两个测试网络的区块链浏览器分别为:https://ropsten.etherscan.io/https://rinkeby.etherscan.io/ 。可分别通过命令行参数 --testnetrinkeby 指定它们:

$ geth --help
......
  --testnet                                  Ropsten network: pre-configured proof-of-work test network
  --rinkeby                                  Rinkeby network: pre-configured proof-of-authority test network

2.1.1 启动geth节点

启动geth节点,并连接测试网络:

$ geth --testnet --rpc --syncmode light console 2>testnet.log

如果需要在web3.js代码中执行解锁帐号等操作,则还要加上参数 --rpcapi="db,eth,net,web3,personal,web3" ,即:

$ geth --testnet --rpc --rpcapi="db,eth,net,web3,personal,web3" --syncmode light console 2>testnet.log

参数说明:
1、参数 --testnet 表示连接测试网络(不指定参数时默认连接主网络);
2、参数 --rpc 表示启用rpc服务(比如使用web3.js库时需要连接到rpc服务),默认监听在localhost:8545;
2、参数 --syncmode light 表示采用light同步模式(不指定参数时默认使用full同步模式)。
3、参数 console 表示启动交互式的JavaScript environment。
4、上面例子中为了不使日志影响到交互式环境,已经把标准错误输出重定向到文件testnet.log中。

进入交互环境后,输入 eth 可以查看所支持的交互命令,如:

$ geth --testnet --rpc --syncmode light console 2>testnet.log
Welcome to the Geth JavaScript console!

instance: Geth/v1.8.14-stable/darwin-amd64/go1.10.3
 modules: admin:1.0 debug:1.0 eth:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0

> eth
{
  accounts: [],
  blockNumber: 4002167,
  coinbase: undefined,
  compile: {
    lll: function(),
    serpent: function(),
    solidity: function()
  },
  defaultAccount: undefined,
  defaultBlock: "latest",
  gasPrice: 1000000000,
  hashrate: 0,
  mining: false,
  pendingTransactions: [],
  protocolVersion: "0x2712",
  syncing: {
    currentBlock: 4002181,
    highestBlock: 4141720,
    knownStates: 0,
    pulledStates: 0,
    startingBlock: 3866623
  },
  call: function(),
  contract: function(abi),
  estimateGas: function(),
  filter: function(options, callback, filterCreationErrorCallback),
  getAccounts: function(callback),
  getBalance: function(),
  getBlock: function(),
  getBlockNumber: function(callback),
  getBlockTransactionCount: function(),
  getBlockUncleCount: function(),
  getCode: function(),
  getCoinbase: function(callback),
  getCompilers: function(),
  getGasPrice: function(callback),
  getHashrate: function(callback),
  getMining: function(callback),
  getPendingTransactions: function(callback),
  getProtocolVersion: function(callback),
  getRawTransaction: function(),
  getRawTransactionFromBlock: function(),
  getStorageAt: function(),
  getSyncing: function(callback),
  getTransaction: function(),
  getTransactionCount: function(),
  getTransactionFromBlock: function(),
  getTransactionReceipt: function(),
  getUncle: function(),
  getWork: function(),
  iban: function(iban),
  icapNamereg: function(),
  isSyncing: function(callback),
  namereg: function(),
  resend: function(),
  sendIBANTransaction: function(),
  sendRawTransaction: function(),
  sendTransaction: function(),
  sign: function(),
  signTransaction: function(),
  submitTransaction: function(),
  submitWork: function()
}

2.1.2 检测同步状态

启动geth节点后,过一段时间会自动启动数据同步功能。使用 eth.blockNumber 可以检测目前已经同步到哪个块:

> eth.blockNumber
3076701

使用 eth.syncing 可以检测现在的同步状态,下面是它的输出实例:

> eth.syncing
{
  currentBlock: 3928302,
  highestBlock: 4141720,
  knownStates: 0,
  pulledStates: 0,
  startingBlock: 3866623
}

如果目前没处于同步状态,则 eth.syncing 会输出:

> eth.syncing
false

2.1.3 获取测试币

在网站 https://faucet.metamask.io/ (需要安装MetaMask浏览器插件)上可以自助给测试网络中账号充值以太币。

2.2 账号相关

2.2.1 创建账号

使用 personal.newAccount() 可以创建账号,会提示输入密码。如:

> personal.newAccount('12345')   # 密码为12345
"0x6bf91905d0434c6029a79b08127722e0c2311512"

2.2.2 导入账号

使用 personal.importRawKey("priv key", "your password") 可以导入账号。如:

> personal.importRawKey('af180cf61900d1c741bab0a99dedb7cc0da78988ffafc364ba06feab4c88be97', '12345678')
"0xe4f394093e0dc30d28a216daece79a6d0d537042"

2.2.3 列出创建或导入的账号

使用 eth.accounts 或者 personal.listAccounts 可以列出 geth 创建的导入的账号,如:

> eth.accounts
["0x6bf91905d0434c6029a79b08127722e0c2311512", "0x33bcd711ce5917bdaf3079488f402bc75118b026", "0xe4f394093e0dc30d28a216daece79a6d0d537042"]

2.2.4 查询余额

使用 eth.getBalance 可以查看账号余额,其单位为wei,如:

> eth.getBalance('0xe4f394093e0dc30d28a216daece79a6d0d537042')
1000000000000000000

2.2.5 转账

使用 eth.sendTransaction 可以进行转账操作。如:

> personal.unlockAccount("0xe4f394093e0dc30d28a216daece79a6d0d537042", "12345678")    // 解锁账号
> eth.sendTransaction({from:"0xe4f394093e0dc30d28a216daece79a6d0d537042", to:"0x6bf91905d0434c6029a79b08127722e0c2311512", value:web3.toWei(0.5, 'ether')})
"0x026956208c00f047f05eadf900e8297e3fc445e2cbe7e85f51b95742f5e86a00"

3 web3.js

web3.js 是一个以太坊的Javascript库(可以通过 npm install web3 安装),它实现了 Generic JSON RPC spec 。在使用web3.js前你需要本地搭建一个以太坊节点(或者使用别人提供的节点)。

下面是一个实例程序test.js:

var Web3 = require("web3");
var web3 = new Web3();

// 连接到以太坊节点(请在本地自己搭建好以太坊节点)
web3.setProvider(new Web3.providers.HttpProvider("http://localhost:8545"));

console.log('web3 version=' + web3.version);

// 输出连接节点中生成或导入的帐号
web3.eth.getAccounts().then(console.log);

// 输出指定账号的余额
web3.eth.getBalance("0x6bf91905d0434c6029a79b08127722e0c2311512").then(console.log);

测试上面程序,得到下面输出:

$ node test.js
web3 version=1.0.0-beta.36
[ '0x6bf91905d0434c6029a79B08127722e0c2311512',
  '0x33bCd711cE5917BdaF3079488F402bc75118b026',
  '0xe4F394093E0Dc30d28A216dAEce79A6D0D537042' ]
500000000000000000

注:web3.js 1.0版本和之前的版本有较大的区别,本文例子使用是的1.0版本。

参考:
https://web3js.readthedocs.io/en/1.0/web3.html

4 智能合约

A contract is a collection of code (its functions) and data (its state) that resides at a specific address on the Ethereum blockchain.

使用 Ethereum智能合约 的基本流程如下:
1、启动一个以太坊节点 (例如geth或者testrpc)。
2、使用 solc 编译智能合约,可以获得部署合约时所要使用的Ethereum Virtual Machine (EVM) bytecode和abi。
3、将编译好的合约部署到网络。
4、用web3.js提供的JavaScript API来调用合约。

4.1 Solidity合约实例

以太坊的智能合约常使用 Solidity 语言来编写。Solidity是一种语法类似JavaScript的高级语言,可以被编译为EVM bytecode。

下面是以太坊智能合约的一个例子(假设文件名为test.sol):

pragma solidity >=0.4.0 <0.6.0;

contract SimpleStorage {
    uint storedData;

    function set(uint x) public {
        storedData = x;
    }

    function get() public view returns (uint) {
        return storedData;
    }
}

参考:Solidity in Depth

4.2 编译合约

使用web3来部署智能合约时,需要先获得合约的EVM bytecode以及合约的abi。可以直接在网络 https://ethereum.github.io/browser-solidity/ 上进行online编译;也可以安装 solc 后自己编译,安装solc的步骤:

$ npm install -g solc                 # 成功安装后,会得到可执行程序solcjs

下面是使用 solcjs 编译上节例子中的test.sol,得到其EVM bytecode以及abi的例子:

$ solcjs --bin test.sol          #### 获得EVM bytecode,保存在文件test_sol_SimpleStorage.bin中
$ cat test_sol_SimpleStorage.bin
608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a72305820fc40facace75159ce9071fb0b9c2ef08afa7650019cf3276f911c3a1cc29eb390029

$ solcjs --abi test.sol          #### 获的abi,保存在文件test_sol_SimpleStorage.abi中
$ cat test_sol_SimpleStorage.abi
[{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]

4.3 部署合约

在web3.js 1.0前,调用合约的new()方法就可以部署合约;在web3.js 1.0中,部署合约分为两步:
1、创建合约(新建一个web3.eth.Contract类的实例);
2、发布合约。

在发布合约时要调用 myContract.deploy().send()。对于 deploy() 而言,如果你的合约的构造函数是带参数的,则调用 deploy() 函数也要把参数带上。调用 send() 的时候需要指定从哪个地址扣费(from选项),以及燃料多少(gas选项)。 send() 函数比较有意思的是,它返回的不光是一个Promise,还是一个EventEmiter,因而可以监听发布过程中的一些事件:

  • transactionHash - 生成了交易hash的事件,一般只会触发一次;
  • receipt - 合约已经打包到区块上了,一般只触发一次,这时就已经可以调用这个合约了;
  • confirmation - 合约已经打包到区块链上并收到了确认,每次确认都会触发一次,最多到第24次确认;
  • error - 发布合约失败。

下面是部署前面合约的例子:

var Web3 = require("web3");
var web3 = new Web3();

// 连接到以太坊节点
web3.setProvider(new Web3.providers.HttpProvider("http://localhost:8545"));
console.log('web3 version=' + web3.version);

// 下面contractAbi是通过solcjs编译得到的
contractAbi = [{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]

// 下面contractBytecode是通过solcjs编译得到的
contractBytecode = '0x' + '608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a72305820fc40facace75159ce9071fb0b9c2ef08afa7650019cf3276f911c3a1cc29eb390029'

// 创建合约
const myContract = new web3.eth.Contract(contractAbi, {data: contractBytecode})

// 部署合约的账号(部署合约时需要消耗gas,会从这个账号中扣除)
const deployAddr = '0xe4F394093E0Dc30d28A216dAEce79A6D0D537042'
// 上面账户的密码(这个密码是生成或导入帐号时指定的)
const password = '12345678'

// 解锁账号
try {
    web3.eth.personal.unlockAccount(deployAddr, password, 600);
} catch(e) {
    console.log(e);
    return;
}

// 发布合约
myContract.deploy()
    .send({from: deployAddr,      // 指定部署合约的帐号
           gasPrice: 6000000000,  // 指定gas price
           gas: 112149            // 指定gas值
          })    // send() returns a Promise & EventEmit
    .on('transactionHash', function(transactionHash){
        console.log("deploy transaction hash: ", transactionHash)
    })
    .on('receipt', function(receipt){
        console.log("deploy receipt: ", receipt)
    })
    .on('confirmation', function(confirmationNum, receipt){
        console.log("got confirmations number: ", confirmationNum)
    })
    .then(function(myContractInstance){
        console.log("deployed successfully.")
        console.log("myContractInstance.options.address = ", myContractInstance.options.address)
    })
    .catch(err => {
        console.log("Error: failed to deploy, detail:", err)
    })

运行部署合约的程序,可能得到下面输出:

web3 version=1.0.0-beta.36
deploy transaction hash:  0xad3b569955ce8ca4be421adb2c5cba366791d2ba9944f4e0966fd54f708f4ebe
got confirmations number:  0
deploy receipt:  { blockHash: '0x95875d8749353dd22ddd51ee005e23a8bce75503272615c286c520547b2dc852',
  blockNumber: 4241261,
  contractAddress: '0x92021f848472af2fD85c0b8Ef6Ea89CeDc6294B0',
  cumulativeGasUsed: 225499,
  from: '0xe4f394093e0dc30d28a216daece79a6d0d537042',
  gasUsed: 112149,
  logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
  status: true,
  to: null,
  transactionHash: '0xad3b569955ce8ca4be421adb2c5cba366791d2ba9944f4e0966fd54f708f4ebe',
  transactionIndex: 4,
  events: {} }
deployed successfully.
myContactInstance.options.address =  0x92021f848472af2fD85c0b8Ef6Ea89CeDc6294B0
got confirmations number:  1
got confirmations number:  2
got confirmations number:  3
got confirmations number:  4
got confirmations number:  5
got confirmations number:  6
got confirmations number:  7
got confirmations number:  8
got confirmations number:  9
got confirmations number:  10
got confirmations number:  11
got confirmations number:  12
got confirmations number:  13
got confirmations number:  14
got confirmations number:  15
got confirmations number:  16
got confirmations number:  17
got confirmations number:  18
got confirmations number:  19
got confirmations number:  20
got confirmations number:  21
got confirmations number:  22
got confirmations number:  23
got confirmations number:  24

在上面输出中,我们可以看到新合约的地址为: 0x92021f848472af2fD85c0b8Ef6Ea89CeDc6294B0 ,后面会用到这个地址。

注意:前面代码中,设置了gasPrice为6000000000,gas为112149;但这两个值并不是固定的,可以通过下面代码获得:

web3.eth.getGasPrice().
    then((averageGasPrice) => {
        console.log("Average gas price: " + averageGasPrice);   // 输出实例:6000000000
    }).
    catch(function(err) {
        console.log(err);
    });

myContract.deploy().estimateGas().
    then((estimatedGas) => {
        console.log("Estimated gas: " + estimatedGas);          // 输出实例:112149
    }).
    catch(function(err) {
        console.log(err);
    });

4.4 调用合约

要调用合约可以使用 call()send()

myContract.methods.myMethod([param1[, param2[, ...]]]).call(options[, callback])
myContract.methods.myMethod([param1[, param2[, ...]]]).send(options[, callback])

其中 call() 适用于constant类型的方法(如前面例子中的get方法),而后者 send() 适用于其他方法(有交易或会修改storage的方法,如前面例子中的set方法)。

call() 的返回值(Promise解决后)就是合约中方法的返回值,然而 send() 的返回值(Promise解决后)是这次交易的信息,包括transactionHash和blockHash等,即使合约中方法有返回值也将被忽略。所以,非constant类型的方法建议不要有返回值,出错了就直接抛异常挂掉;如果真的想返回什么,建议通过事件的方式来通知调用者。

4.4.1 send(适用于非constant类型的方法)

下面是调用前面合约中set方法(它不是constant类型的方法,需要使用 send() 来调用)的实例:

var Web3 = require("web3");
var web3 = new Web3();

// 连接到以太坊节点
web3.setProvider(new Web3.providers.HttpProvider("http://localhost:8545"));

contractAbi = [{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]

// 合约地址
const contractAddress = '0x92021f848472af2fD85c0b8Ef6Ea89CeDc6294B0'

// 创建合约(使用已经存在的合约,需要指定abi和合约地址)
const myContract = new web3.eth.Contract(contractAbi, contractAddress)

// 调用合约的账号(调用合约时需要消耗gas,会从这个账号中扣除)
const userAddr = '0xe4F394093E0Dc30d28A216dAEce79A6D0D537042'
const password = '12345678'

// 解锁账号
try {
    web3.eth.personal.unlockAccount(userAddr, password, 600);
} catch(e) {
    console.log(e);
    return;
}

// 调用合约myContract的set方法
myContract.methods.set(20)
    .send({from: userAddr,
           gasPrice: 6000000000,  // 指定gas price
           gas: 112149            // 指定gas值
          })    // send() returns a Promise & EventEmit
    .on('transactionHash', function(transactionHash){
        console.log("transaction hash: ", transactionHash)
    })
    .on('receipt', function(receipt){
        console.log("receipt: ", receipt)
    })
    .on('confirmation', function(confirmationNum, receipt){
        console.log("got confirmations number: ", confirmationNum)
    })
    .then(function(myContractInstance) {
        console.log(myContractInstance)
    })
    .catch(err => {
        console.log("Error: failed to call set method, detail:", err)
    })

4.4.2 call(适用于constant类型的方法)

下面是调用前面合约中get方法(它是constant类型的方法,可以使用 call() 来调用)的实例:

var Web3 = require("web3");
var web3 = new Web3();

// 连接到以太坊节点
web3.setProvider(new Web3.providers.HttpProvider("http://localhost:8545"));

contractAbi = [{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]

// 合约地址
const contractAddress = '0x92021f848472af2fD85c0b8Ef6Ea89CeDc6294B0'

// 创建合约(使用已经存在的合约,需要指定abi和合约地址)
const myContract = new web3.eth.Contract(contractAbi, contractAddress)

// 调用合约myContract的get方法
myContract.methods.get()
    .call()
    .then(console.log)     // 输出get方法的返回值,如20
    .catch(err => {
        console.log("Error: failed to call method get, detail:", err)
    })

4.5 什么是Gas

每个参与到网络的节点都会运行EVM作为区块验证协议的一部分。每个网络中的全节点都会进行相同的计算并储存相同的值。合约执行会在所有节点中被多次重复,而且任何人都可以发布执行合约,这使得合约执行的消耗非常昂贵,所以为防止以太坊网络发生蓄意攻击或滥用的现象,以太坊协议规定交易或合约调用的每个运算步骤都需要收费。这笔费用以Gas(也就是俗称的燃料)作为单位计数。

每个EVM中的命令都被设置了相应的gas消耗值。如果用gasUsed表示所有被执行命令的gas消耗值总和,则有:

交易费 = gasUsed * gasPrice

注:用户可以自行设置的gasPrice的值。由于网络拥堵等多种情况,此价格会动态变化,可根据交易字节数和当前区块中打包的平均值进行大概的预估。可以使用 web3.eth.getGasPrice() 得到gasPrice的估计值。

参考:https://blog.csdn.net/wo541075754/article/details/79042558

5 Bitcoin VS. Ethereum

下面简单地列举一些比特币和以太坊的不同之处。

不同点一:和比特币相比, 以太坊支持完善的智能合约 ,可以方便地在以太坊上开发DApp。
不同点二:它们产生区块的速度不一样:比特币约10分钟一个区块;而以太坊约15秒一个区块。
不同点三:比特币采用“UTXO”(区块链网络本身并不直接保存余额等信息)而以太坊才是采用的我们经常用的“账户余额模型”。对于以太坊账户,网络中直接保存着以下几个状态:

  • nonce:外部账户为交易次数,合约账户为创建的合约序号。
  • balance:此地址的以太币余额。
  • storageRoot:账户存储内容组成的默克尔树根的哈希值。
  • codeHash:账户EVM代码的hash值。合约账户即为合约代码的哈希值,外部账户为空字符串的哈希值。

Author: cig01

Created: <2018-09-02 日 00:00>

Last updated: <2018-11-11 日 20:21>

Creator: Emacs 25.3.1 (Org mode 9.1.4)