以太坊设计与实现系列之一:开发环境搭建

1,609 阅读10分钟

编译安装 go-ethereum

Go 语言开发环境

首先确保本机已安装 Go 开发环境:

$ go version

go version go1.15 windows/amd64

下载 go-ethereum 源码

git clone git@github.com:ethereum/go-ethereum.git

切换到稳定版分支:

git checkout v1.10.2

编译安装

进入代码目录 cd go-ethereum 执行编译:

$ make all

env GO111MODULE=on go run build/ci.go install
...
...

如果是 Windows 环境,可以在 mingw-w64.org/ 下载安装 mingw32-make ,然后执行编译:

$ mingw32-make all

env GO111MODULE=on go run build/ci.go install
...
...

或者直接执行 Go 安装脚本:

go run build/ci.go install

Tips: 本文就是在 Windows 10 环境下完成演示

编译完成后,可以在源码的 build/bin 目录下查看可执行文件:

$ cd build/bin/ && ls

abidump.exe  
abigen.exe  
bootnode.exe  
checkpoint-admin.exe  
clef.exe  
devp2p.exe  
ethkey.exe  
evm.exe  
faucet.exe  
geth.exe  
p2psim.exe  
puppeth.exe  
rlpdump.exe
...

查看 geth 版本信息:

$ ./geth.exe version

Geth
Version: 1.10.2-stable
Git Commit: 97d11b0187b4695ccf44e3b71b54155fe405a36f
Architecture: amd64
Go Version: go1.15
Operating System: windows
...

恭喜!我们已经完成了 go-ethereum 的安装

使用 Clef 管理 Ethereum 账户

在上 part 中,我们已经完成了 clef 程序的编译安装,clefgo-ethereum 专门用来管理账户的辅助程序,可以创建账户和完成签名:

$ ./clef.exe h

NAME:
   Clef - Manage Ethereum account operations

   Copyright 2013-2021 The go-ethereum Authors

USAGE:
   clef.exe [options] command [command options] [arguments...]

COMMANDS:
   init                               Initialize the signer, generate secret storage
   attest                             Attest that a js-file is to be used
   setpw                              Store a credential for a keystore file
   delpw                              Remove a credential for a keystore file
   newaccount                         Create a new account
   gendoc                             Generate documentation about json-rpc format
   help, h                            Shows a list of commands or help for one command

FLAGS OPTIONS:
  --loglevel value                    ...
  ...

初始化 Clef

初始化脚本会创建主密钥,需要提供 10 位及以上长度的密码完成加密:

$ ./clef.exe init

...
Enter 'ok' to proceed:
> ok
...

创建账户

创建账户过程中,同样需要提供 10 位及以上长度的密码完成加密:

$ ./clef.exe newaccount

...
Enter 'ok' to proceed:
> ok
...

## New account password

Please enter a password for the new account to be created (attempt 0 of 3)
...
Generated account 0x46e7328724A7...

创建完成后,可以在默认的 keystore 目录下看到账户文件,以 Windows 系统为例:

$ ls -la ~/AppData/Local/Ethereum/keystore

UTC--2021-05-07T07-02-47.142238200Z--46e7328724a7...

文件内容示例:

{
  "address": "46e7328724a7...",
  "crypto": {
    ...
  },
  "id": "...",
  "version": 3
}

Tips: 可以用同样的方法创建多个账户,下文配置创世区块的时候可以给这些账户预分配余额

Ethereum 节点

为了方便演示,我们将启动只有一个节点的本地私有 Ethereum 网络

创世区块

创世区块配置 genesis.json,其中 alloc 配置项可以设置预分配账户余额,建议使用上 part clef 创建的账户,节点启动后这些账户上就会有 10080 个 ETH 余额,方便开发调试:

{
  "config": {
    "chainId": 1337, // 一般约定本地私有网络 chianid = 1337
    "homesteadBlock": 0, // homesteadBlock 区块高度,私有网络只需 != nil 即可,例如:0
    "eip150Block": 0, // 同上
    "eip155Block": 0, // 同上
    "eip158Block": 0, // 同上
    "byzantiumBlock": 0, // 同上
    "constantinopleBlock": 0, // 同上
    "petersburgBlock": 0, // 同上
    "istanbulBlock":0, // 同上
    "muirGlacierBlock":0, // 同上
    "berlinBlock":0 // 同上
    // 注意:如果阅读此文时有更多的分叉区块,都需要配置上,不同区块高度后的验证签名算法可能不一样
    // 如果交易发送方使用了 berlinBlock 之后新的签名算法,节点无法验证的话会报错:invalid sender
  },
  "alloc": {
	  "248672bac919...":{"balance":"10080000000000000000000"}, // 注意单位 1 eth = 10^18 wei
	  "46e7328724a7...":{"balance":"10080000000000000000000"}
  },
  "nonce": "0x0000000000000042",
  "difficulty": "0x020000",
  "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "coinbase": "0x0000000000000000000000000000000000000000",
  "timestamp": "0x00",
  "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "extraData": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa",
  "gasLimit": "0x4c4b40"
}

初始化创世区块:

$ ./geth.exe init ~/AppData/Local/Ethereum/genesis.json

INFO [05-07|15:39:59.549] Maximum peer count                       ETH=50 LES=0 total=50
INFO [05-07|15:39:59.574] Set global gas cap                       cap=25000000
...
INFO [05-07|15:39:59.623] Successfully wrote genesis state         database=lightchaindata hash="dcc595…7102aa"

启动节点

新开一个终端窗口:

$ ./geth.exe --nodiscover

INFO [05-07|15:45:11.014] Starting Geth on Ethereum mainnet...
...
INFO [05-07|15:45:11.401] Started P2P networking                   self="enode://eda1a8e2d4d...@127.0.0.1:30303?discpo
rt=0"
...

选项说明:

--nodiscover // 关闭 p2p 网络节点发现,确保不加入任何其他网络

连接节点

节点启动时会默认开启 ipc 进程间通信管道,也可以主动开启支持 http 或 WebSocket 协议的 rpc 远程调用服务

通过 ipc 服务 或者 rpc 接口,节点可以执行外部用户提供的管理、查询、交易等指令

这里我们先使用 ipc 连接已启动的节点

新开一个终端,进入 Geth JavaScript 控制台:

$ ./geth.exe attach \\.\pipe\geth.ipc

Welcome to the Geth JavaScript console!

instance: Geth/v1.10.2-stable-97d11b01/windows-amd64/go1.15
coinbase: 0x61d55d8015aa...
...
modules: admin:1.0 debug:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0

To exit, press ctrl-d
>

解锁账户

每次发送交易之前,发送方需要使用密钥完成签名

为了方便测试,可以使用密码提前解锁交易发送方账户,之后再发送交易的话 geth 会自动使用密钥对交易数据签名

> personal.unlockAccount("46e7328724a7...","012...",36000)

// 参数:账户地址,账户密码,解锁有效时长(单位 秒)

// 解锁成功
true

转账交易

接下来我们将执行交易相关的指令,看看 Ethereum 是如何处理交易的

查询余额

输入查询余额指令:

> eth.getBalance("46e7328724a...")

// 查询结果 注意单位为 wei (1 eth = 10^18 wei)
1.008e+22

使用创世区块预分配的账户作为查询参数,可以看到余额确实为预分配的数额

也可以转换为 ether 单位:

> web3.fromWei(eth.getBalance("46e7328724a..."),"ether")

// 查询结果 单位为 eth
10080

转账

输入转账指令:

// 转出 10 eth

> eth.sendTransaction({from:"46e7328724a7...",to:"248672bac9196446...", value: web3.toWei(10,"ether")})

// 返回结果:交易 hash

"0x25d0909d7fa61be3449c9c9f6c..."

注意:

虽然成功提交了交易,但是节点没有开启挖矿,所以不会打包交易产出新的区块

交易还在交易池等待打包

查看交易池状态:

> txpool.status
{
  pending: 1,
  queued: 0
}

启动挖矿

> miner.start(1)

// 参数表示启动 1 个线程,也可以 2/3/...

可以在 geth 终端看到出块日志:

INFO [05-07|17:45:18.367] Updated mining threads                   threads=1
INFO [05-07|17:45:18.367] Transaction pool price threshold updated price=1000000000
INFO [05-07|17:56:05.031] Etherbase automatically configured       address=0x61d55D8015Aa018Ea87f2D4F2A1Ee8Af1912a887
INFO [05-07|17:56:05.032] Commit new mining work                   number=1 sealhash="a29c2e…a33157" uncles=0 txs=0 gas=0 fees=0 elapsed=0s
INFO [05-07|17:56:05.033] Commit new mining work                   number=1 sealhash="a068c9…e980b4" uncles=0 txs=1 gas=21000 fees=2.1e-05 elapsed=1.009ms
INFO [05-07|17:56:12.566] Successfully sealed new block            number=1 sealhash="a068c9…e980b4" hash="cf056d…8cf707" elapsed=7.533s
INFO [05-07|17:56:12.566] 🔨 mined potential block                number=1 hash="cf056d…8cf707"
...

暂停挖矿

> miner.stop()

// 查看账户余额:

> eth.getBalance("248672bac91964465...")
1.009e+22 // 接收方: + 10 eth

> eth.getBalance("46e7328724a7b9...")
1.0069999979e+22 // 发送方: - 10 eth - 转账费(gas * gasPrice)

> eth.getBalance("0x61d55D8015Aa01...")
1.0190000021e+22 // 矿工: + 出块奖励(reward * blockNum) + 转账费(gas * gasPrice)

查看账户余额,可以确认转账成功

智能合约

以太坊上创建和执行合约同样是通过发送交易的方式,只不过交易携带的数据多了合约字节码或者调用方法等信息

编写合约

可以使用任何高级语言来编写智能合约,只要在部署的时候编译为以太坊虚拟机 EVM 可以执行的字节码即可

使用 Solidity (soliditylang.org/) 语言编写的智能合约示例:

// SPDX-License-Identifier: CC-BY-SA-4.0

// Version of Solidity compiler this program was written for
pragma solidity ^0.6.0;

// Our first contract is a faucet!
contract Faucet {
    // Accept any incoming amount
    receive () external payable {}
    
    // Give out ether to anyone who asks
    function withdraw(uint withdraw_amount) public {

        // Limit withdrawal amount
        require(withdraw_amount <= 100000000000000000);

        // Send the amount to the address that requested it
        msg.sender.transfer(withdraw_amount);
    }
}

Tips: 使用在线 IDE remix.ethereum.org/ 方便开发调试智能合约

在 remix 上将合约编译生成字节码:

{
  "linkReferences": {},
  "object": "608060405234801561001057600080fd5b5060f48061001f6000396000f3fe608060405260043610601f5760003560e01c80632e1a7d4d14602a576025565b36602557005b600080fd5b348015603557600080fd5b50605f60048036036020811015604a57600080fd5b81019080803590602001909291905050506061565b005b67016345785d8a0000811115607557600080fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f1935050505015801560ba573d6000803e3d6000fd5b505056fea26469706673582212207a2c9ed47daa046769d1020e307564fedff522d6340096ab14fa5829bd5f230764736f6c634300060c0033",
  "opcodes": "PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0x10 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0xF4 DUP1 PUSH2 0x1F PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN INVALID PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x4 CALLDATASIZE LT PUSH1 0x1F JUMPI PUSH1 0x0 CALLDATALOAD PUSH1 0xE0 SHR DUP1 PUSH4 0x2E1A7D4D EQ PUSH1 0x2A JUMPI PUSH1 0x25 JUMP JUMPDEST CALLDATASIZE PUSH1 0x25 JUMPI STOP JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST CALLVALUE DUP1 ISZERO PUSH1 0x35 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x5F PUSH1 0x4 DUP1 CALLDATASIZE SUB PUSH1 0x20 DUP2 LT ISZERO PUSH1 0x4A JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST DUP2 ADD SWAP1 DUP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP PUSH1 0x61 JUMP JUMPDEST STOP JUMPDEST PUSH8 0x16345785D8A0000 DUP2 GT ISZERO PUSH1 0x75 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST CALLER PUSH20 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF AND PUSH2 0x8FC DUP3 SWAP1 DUP2 ISZERO MUL SWAP1 PUSH1 0x40 MLOAD PUSH1 0x0 PUSH1 0x40 MLOAD DUP1 DUP4 SUB DUP2 DUP6 DUP9 DUP9 CALL SWAP4 POP POP POP POP ISZERO DUP1 ISZERO PUSH1 0xBA JUMPI RETURNDATASIZE PUSH1 0x0 DUP1 RETURNDATACOPY RETURNDATASIZE PUSH1 0x0 REVERT JUMPDEST POP POP JUMP INVALID LOG2 PUSH5 0x6970667358 0x22 SLT KECCAK256 PUSH27 0x2C9ED47DAA046769D1020E307564FEDFF522D6340096AB14FA5829 0xBD 0x5F 0x23 SMOD PUSH5 0x736F6C6343 STOP MOD 0xC STOP CALLER ",
  "sourceMap": "169:405:0:-:0;;;;;;;;;;;;;;;;;;;"
}

其中,object 字段就是交易携带的字节码数据

部署合约

部署合约和转账交易方法一样,但参数只需 fromdata

> eth.sendTransaction({from:"46e7328724a7...",data:"0x608060405234801561001057600080fd5b5060f48061001f6000396000f3fe608060405260043610601f5760003560e01c80632e1a7d4d14602a576025565b36602557005b600080fd5b348015603557600080fd5b50605f60048036036020811015604a57600080fd5b81019080803590602001909291905050506061565b005b67016345785d8a0000811115607557600080fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f1935050505015801560ba573d6000803e3d6000fd5b505056fea26469706673582212207a2c9ed47daa046769d1020e307564fedff522d6340096ab14fa5829bd5f230764736f6c634300060c0033"})

// 交易 hash
"0x5a90c06d0af12b921fa62efddfe3376b07c1ab0b820b3626016c5ead959d42a1"

// 参数:
// from 交易发送方账户地址
// data 上 part 编译生成的字节码,注意:附加了 0x 开头,表示 16 进制数据

调用合约

调用合约和转账交易方法一样,但参数只需 from todata

> eth.sendTransaction({from:"46e7328724a...",to:"37F5A36e1...",data:"0x2e1a7d4d00000000000000000000000000000000000000000000000000000000000186a0"})

// 参数:
// from 交易发送方账户地址
// to 合约地址
// data 合约调用信息的 abi 编码,注意:附加了 0x 开头,表示 16 进制数据

可以使用 abidump 解码查看 abi 信息:

$ ./abidump.exe 0x2e1a7d4d00000000000000000000000000000000000000000000000000000000000186a0

Info: Transaction invokes the following method: "withdraw(uint256: 100000)"

小结

通过上述步骤,我们完成了以太坊本地开发环境的搭建,了解了创建账户、转账交易、部署和调用合约的基本过程

接下来我们将深入代码细节,看看这些指令是如何具体执行的

参考内容

(本文完)