区块链18区块链以太坊

以太坊文档整理

2017-09-03  本文已影响1659人  趁风卷

本文是对以太坊文档 Ethereum Frontier GuideEthereum Homestead 的整理。Fontier 和 Homestead 分别是以太坊的第 1 和 第 2 个版本。

本文使用 go 语言编写的命令行客户端 geth

geth 的命令可以分为 CLI 命令和 JavaScript 控制台交互式命令,约定如下

1. 安装和运行节点

1.1 安装客户端

geth 的安装教程请参见 Building Ethereum

1.2 同步区块

「同步」的意思是把网络上的区块全部下载到本地,以同步到网络的最新状态。使用客户端前必须先同步区块。

同步命令如下

更优雅的同步方式

数据存放目录

主网络区块数据的默认存放目录是 ~/Library/Ethereum(Mac OS X)

不同客户端是可以共用区块数据的。用 geth 同步的区块数据,可以在 Ethereum Wallet 或 Mist 客户端里直接使用。

导入已有的区块文件

如果本地已有区块文件,可以将其导入

启动节点

1.3 启动客户端

启动客户端的方式如下

更常用的是启动客户端,并进入控制台模式:geth --datadir <path> console 2>console.log。同时可以另开窗口,用 tail -f console.log 浏览日志。

更复杂的启动命令

geth --identity "MyNodeName" --rpc --rpcport "8080" --rpccorsdomain "*" --datadir "./ethdev" --port "30303" --nodiscover --rpcapi "db,eth,net,web3" --networkid 1999(参考:Command line parameters

监控

在控制台里,使用这些命令检查连接状态

此外,还有一些网站供你查看以太坊主网络的状态

1.4 测试网络

以太坊公有的测试网络有 Ropsten 和 Rinkeby。除此之外,你可以搭建自己的私有网络,即只能本地访问的私网。

下面介绍 3 种测试网络的搭建方式

Ropsten网络

参考:ethereum/ropsten

Rinkeby网络

参见 Rinkeby: Ethereum Testnet - Connect Yourself,有 archive, full, light, embedded 4种模式

私有网络

搭建私有网络,需要先新建创世块参数文件 genesis.json

{
    "config": {
        "chainId": 15,
        "homesteadBlock": 0,
        "eip155Block": 0,
        "eip158Block": 0
    },
    "difficulty": "10000",
    "gasLimit": "2100000",
    "alloc": {
        "7df9a875a174b3bc565e6424a0050ebc1b2d1d82": { "balance": "300000" },
        "f41c74c9ae680c1aa78f42e5647a62f353b7bdde": { "balance": "400000" }
    }
}

接下来使用命令 geth --datadir ./ethdev init <genesis.json> console 初始化测试网络,并进入控制台。

该测试网络只有你一个人,你需要自己挖坑来记录交易。

参考

2. 账户管理

2.1 创建账户

新建账户

导入账户

修改密码

2.2 导入钱包

创建多签名钱包,请参见Creating a Multi-Signature Wallet in Mist

2.3 查看账户信息

列出所有账户

查看账户余额

下面的代码可以打印所有的账户余额

function checkAllBalances() { 
var i =0; 
eth.accounts.forEach( function(e){
     console.log("  eth.accounts["+i+"]: " +  e + " \tbalance: " + web3.fromWei(eth.getBalance(e), "ether") + " ether"); 
i++; 
})
};

小技巧:可以把代码存到文件中。进入 geth 的控制台后,用 > loadScript(<loadfile.js>) 导入文件中的函数。

2.4 发送交易

以发起一个 0.01 个 ether 的转账交易为例

> var sender = eth.accounts[0];
> var receiver = eth.accounts[1];
> var amount = web3.toWei(0.01, "ether")

> eth.sendTransaction({from:sender, to:receiver, value: amount, gas: gasAmount})  //`gas` 不是必须的

之后会让你输入密码

或者也可以先用 personal.unlockAccount(sender, <passphrase>) 解锁账户,再发送交易。

3. 挖矿

3.1 介绍

挖矿会有挖矿奖励

以太坊使用 Ethash 的 PoW 算法

3.2 CPU 挖矿

挖矿

发放奖励:eth.etherbase(也叫 coinbase)是一个地址,挖矿奖励会发到这个地址里。改变 etherbase 的方式如下

其他

下面的函数可以统计不同地址的出块数量

function minedBlocks(lastn, addr) {
  addrs = [];
  if (!addr) {
    addr = eth.coinbase
  }
  limit = eth.blockNumber - lastn
  for (i = eth.blockNumber; i >= limit; i--) {
    if (eth.getBlock(i).miner == addr) {
      addrs.push(i)
    }
  }
  return addrs
}
// scans the last 1000 blocks and returns the blocknumbers of blocks mined by your coinbase 
// (more precisely blocks the mining reward for which is sent to your coinbase).   
minedBlocks(1000, eth.coinbase);
//[352708, 352655, 352559]

3.3 其他挖矿方式

4. 接口

4.1 命令行接口

CLI 命令已介绍得差不多了。

可以去 Command Line Options 查阅具体的命令。

4.2 JSON RPC API

JSON-RPC 是一种无状态、轻量级的 RPC 协议,规定了通信的数据结构和规则。以太坊客户端使用 JSON-RPC 和其他客户端通信。

比如 MetaMask 钱包就是通过 JSON-RPC 和以太坊客户端进行通信的。

对于不同的以太坊客户端,默认 JSON-RPC 地址如下

geth 是 go 客户端,因此 JSON-RPC 为 http://localhost:8545

常用命令

细节请参阅 JSON RPC

4.3 使用 Ðapp 的 JavaScript API

你可以使用 web3.js 库所提供的对象,来搭建 Ðapp。

细节请参阅 JavaScript API

4.4 JavaScript 控制台

一般操作都在控制台模式下进行。

详细命令请参阅 JavaScript Console

5. 智能合约

5.1 对智能合约进行基本概念的介绍;后面的小节依次介绍合约的编写、编译和部署。

参考

5.1 介绍

5.1.1 账户

以太坊有 2 种账户

当合约收到交易时,以太坊虚拟机(EVM)会根据它收到的参数,来执行内部的代码。

5.1.2 交易与消息

交易(Transaction):EOA发送给其他账户(EOA或合约)的签名过的消息

消息(Message):合约发给其他合约的消息

两者的不同就在于发送方的不同

5.1.3 gas

ether 是以太坊中的货币,用于支付 EVM 的计算。

以太坊中货币最小的单位是 wei。

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

其他:Ether 供应量

Gas 被认为是网络资源的不变花费。我们希望发送每笔交易的真实成本总是保持不变,所以 Gas 不能被发行,否则价格会有波动。

反之,我们发行 ether。当 ether 价格上升时,Gas 价格就对应下降,以保持真实成本不变

参考:Ether

5.1.4 账户交互的例子

合约通常为这 4 种目的服务

合约能充当不同的角色,因此我们希望合约可以多交互。下面举一个合约交互的例子

这个例子有 5 个合约

Alice 完成赌注,需要这么做

  1. Alice 发送消息给 Bet Contract,其消息是「接受赌约,并把自己的 50 个 GavCoin 存到 Bet Contract 的账户下」
  2. Bet Contract 发送消息给 GavCoin Contract,把 Alice 的 50 个 GavCoin 存到自己的账户(即合约地址)里

Bob 想完成赌注,需要这么做

  1. Bob 的 EOA 发送消息给 Forwarding Contract,该消息包含了 Bob 的 EOA 的 ECDSA 和 Lamport 签名,以及消息「接受赌约,并把自己的 50 个 GavCoin 存到 Bet Contract 的账户下」。
  2. Forwarding Contract 发送消息给 Lamport Contract,要求验证 Lamport 签名
  3. 若验签成功,Lamport Contract 返回 1 给 Forwarding Contract。Forwarding Contract 发送消息给 Bet Contract
  4. Bet Contract 发送消息给 GavCoin Contract,把 Bob 的 50 个 GavCoin 存到自己的账户(即合约地址)里

Bet Contract 将自动执行赌约

  1. Bet Contract 每隔一定周期发消息给 Weather Contract,查询 San Francisco 的当前气温
  2. 一旦气温超过 35°C,Bet Contract 发消息给 GavCoin Contract,把自己账户里的 100 GavCoin 发到 Bob 的账户里

5.2 编写合约

学习 solidity 的资源

接下来以合约 greeter.sol 为例(来自 Contract Tutorial

pragma solidity ^0.4.10;

contract mortal {
    /* Define variable owner of the type address*/
    address owner;

    /* this function is executed at initialization and sets the owner of the contract */
    function mortal() { owner = msg.sender; }

    /* Function to recover the funds on the contract */
    function kill() { if (msg.sender == owner) suicide(owner); }
}

contract greeter is mortal {
    /* define variable greeting of the type string */
    string greeting;
    
    /* this runs when the contract is executed */
    function greeter(string _greeting) public {
        greeting = _greeting;
    }

    /* main function */
    function greet() constant returns (string) {
        return greeting;
    }
}

5.3 编译合约

合约以「以太坊虚拟机(EVM)字节码」的形式存在与区块中,因此需要对源文件进行编译得到字节码。

有几种编译方式

5.3.1 安装 solc 编译器

下面是 Mac OS 平台上的 solc 编译器安装方式。其他平台请参考 Building from Source

现在敲入命令 solc 就可以调用编译器了

5.3.2 编译

编译后,我们会得到 2 个东西,用来部署合约:

常用编译命令

为了方便部署,我们一般使用这个命令

echo "var compilerOutput=`solc --optimize --combined-json abi,bin greeter.sol`" > greeter_compiled.js

这将生成一个 greeter_compiled.js 文件,看起来会像这个样子(已经过格式化)

var compilerOutput =
{
  "contracts": {
    "greeter.sol:greeter": {
      "abi": "[{\"constant\":false,\"inputs\":[],\"name\":\"kill\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"greet\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"name\":\"_greeting\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"}]",
      "bin": "6060604052341561000f57600080fd5b6040516103173803806103178339810160405280805160008054600160a060020a03191633600160a060020a03161790559190910190506001818051610059929160200190610060565b50506100fb565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106100a157805160ff19168380011785556100ce565b828001600101855582156100ce579182015b828111156100ce5782518255916020019190600101906100b3565b506100da9291506100de565b5090565b6100f891905b808211156100da57600081556001016100e4565b90565b61020d8061010a6000396000f300606060405263ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166341c0e1b58114610047578063cfae32171461005c57600080fd5b341561005257600080fd5b61005a6100e6565b005b341561006757600080fd5b61006f610127565b60405160208082528190810183818151815260200191508051906020019080838360005b838110156100ab578082015183820152602001610093565b50505050905090810190601f1680156100d85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6000543373ffffffffffffffffffffffffffffffffffffffff908116911614156101255760005473ffffffffffffffffffffffffffffffffffffffff16ff5b565b61012f6101cf565b60018054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156101c55780601f1061019a576101008083540402835291602001916101c5565b820191906000526020600020905b8154815290600101906020018083116101a857829003601f168201915b5050505050905090565b602060405190810160405260008152905600a165627a7a723058202a04be9bc62f62ece115fc346e0b98ea5019ba1d0199402c0883c957096ac9790029"
    },
    "greeter.sol:mortal": {
      "abi": "[{\"constant\":false,\"inputs\":[],\"name\":\"kill\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"}]",
      "bin": "6060604052341561000f57600080fd5b60008054600160a060020a033316600160a060020a031990911617905560b98061003a6000396000f300606060405263ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166341c0e1b58114603b57600080fd5b3415604557600080fd5b604b604d565b005b6000543373ffffffffffffffffffffffffffffffffffffffff90811691161415608b5760005473ffffffffffffffffffffffffffffffffffffffff16ff5b5600a165627a7a723058208d5720dc8ecd1ce214bdca4a93dd356c2894a206b14d349dba56a43e49ac2ae80029"
    }
  },
  "version": "0.4.17-develop.2017.8.28+commit.2b3a49f7.Darwin.appleclang"
}

这个文件包含了 binabi 信息。

接下来要用该文件部署合约

5.4 部署合约

进入 geth 控制台里,执行下面的命令

> loadScript('greeter_compiled.js')  // 导入编译后生成的文件
true
> var _greeting = "Hello World!!!"  // 部署该合约需要的初始化参数
undefined
> var greeterContract = web3.eth.contract(JSON.parse(compilerOutput.contracts["greeter.sol:greeter"].abi));  // 要解析成 json 对象
undefined
> var gt = greeterContract.new(_greeting, {from: eth.accounts[0], data: "0x" + compilerOutput.contracts["greeter.sol:greeter"].bin, gas: 4700000},
    function (e, contract) {
        console.log(e, contract);
        if (typeof contract.address !== 'undefined') {
            console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
        }
    }
  );

总结一下,可以得到部署合约的一般命令(# 开头的变量是我们需要设置的)

> var Contract = web3.eth.contract(#abi);  // abi 是一个 json 对象
undefined
// 部署一个合约实例
> var deployed  = Contract.new(#args, {from: #sender, data: "0x" + #bin, gas: 4700000},   // args:合约的构造函数所需的参数。如果有多个参数,依次用,隔开 中;sender:合约的部署者地址;bin:二进制字节码,前面要加'0x';gas:部署需要花费的 gas 量
    function (e, contract) {  // 这是个回调函数,作用是合约部署成功后,通知你一声
        console.log(e, contract);
        if (typeof contract.address !== 'undefined') {
            console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
        }
    }
  );
null [object Object]
undefined

这里只是把你的合约广播出去,只有被区块记录后,才部署成功。

私有网络下,可以自己来挖新区块

> miner.start()
null
> null [object Object]
Contract mined! address: 0xdfb2938df1e4c80f309f7f09ceae871175b94a81 transactionHash: 0x49fad3bbb8fe00efa018fa8387d9ee2fef3a53dd5c841f2131b0afb5ba17f349  // 部署返回的合约地址和交易哈希值

更方便(偷懒)的方法:使用在线编译器里 Remix - Solidity IDE

由于 1.6 之后的 geth 控制台不能使用 solidity 编译器,而官方教程 Contract Tutorial 还是老版本,没有更新,于是饶了很多弯。最后参考 #14850How to compile Solidity contracts with Geth v1.6? 才弄懂。

5.5 使用合约

先体验一下合约的使用

> gt.greet()  // gt 是 5.4 里得到的合约实例
"Hello World!!!"
> gt.address  // 合约地址
"0xdfb2938df1e4c80f309f7f09ceae871175b94a81"
> eth.getCode(gt.address)
"0x60606....."
> gt.kill.sendTransaction({from:eth.accounts[0]})  // 销毁合约。销毁后就无法使用了

这里的 .greet().kill() 函数的用法不同,是因为

5.5.1 实例化

使用合约前,需要对其进行实例化。先前的 gt 就是一个实例。

实例化合约有 2 步

参考:web3.eth.contract

5.5.2 调用合约中的方法

根据是否会改变网络状态,可将方法分成调用(call) 和 交易(sendTransaction) 两种类型。它们的调用方式分别如下

若使用 contractInstance.method(param1 [, param2, ...] [, transactionObject] [, defaultBlock] [, callback]);,则 EVM 会自动根据方法类型,来选择使用 call()sendTransaction()

参考:Contract Methods

5.5.3 Event

参考:Contract Event

5.X 其他

除了 geth 控制台,还可以用 JSON-RPC 部署合约。具体参考 Accessing Contracts and Transactions

上一篇 下一篇

猜你喜欢

热点阅读