美文网首页
以太坊私有链搭建

以太坊私有链搭建

作者: Nino_Lau | 来源:发表于2018-11-04 20:20 被阅读0次

    此文是区块链项目的期末 🔥 热身报告,用来帮助我们熟悉以太坊开发环境。实验主要包括:

    • 以太坊的安装、私有链创世区块的搭建;
    • block 各个字段和日志的输出解释;
    • 在私有链部署简单的智能合约;
    • 对交易字段进行解释。

    以太坊的安装

    我是在 Mac 上搭建的 Ethereum 环境,搭建的过程很简单:

    brew tap ethereum/ethereum
    brew install ethereum
    geth --help 
    

    如果能成功显示输出帮助,则表示已经成功安装。

    私有链环境搭建

    配置文件

    以太坊支持自定义创世区块,要运行私有链,我们就需要定义自己的创世区块,创世区块信息写在一个 json 格式的配置文件中。首先将下面的内容保存到一个 json 文件中,例如 genesis.json。(直接从官网复制,“chainId” 为 0, 但是会在合约部署出现问题,所以在这里改为 666

    {
      "config": {
            "chainId": 666,
            "homesteadBlock": 0,
            "eip155Block": 0,
            "eip158Block": 0
        },
      "alloc"      : {},
      "coinbase"   : "0x0000000000000000000000000000000000000000",
      "difficulty" : "0x00000001",
      "extraData"  : "",
      "gasLimit"   : "0xffffff",
      "nonce"      : "0x0000000000000042",
      "mixhash"    : "0x0000000000000000000000000000000000000000000000000000000000000000",
      "parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
      "timestamp"  : "0x00"
    }
    

    初始化私有链

    准备好创世区块配置文件后,需要初始化区块链,将上面的创世区块信息写入到区块链中。首先要新建一个目录用来存放区块链数据,假设新建的数据目录为~/nino/shuju,genesis.json 保存在~/nino中,此时目录结构应该是这样的:

    nino
    ├── shuju
    └── genesis.json
    

    进入 ~/nino 目录下,执行命令:

    geth -datadir shuju init genesis.json
    

    上面的命令的主体是 geth init,表示初始化区块链,命令可以带有选项和参数,其中--datadir选项后面跟一个目录名,这里为 shuju,表示指定数据存放目录为 shujugenesis.jsoninit命令的参数。运行上面的命令,会读取 genesis.json 文件,根据其中的内容,将创世区块写入到区块链中。如果看到以下的输出内容,说明初始化成功了。

    WARN [11-04|10:34:14] No etherbase set and no accounts found as default 
    INFO [11-04|10:34:14] Allocated cache and file handles         database=/Users/nino/shuju/geth/chaindata cache=16 handles=16
    INFO [11-04|10:34:14] Writing custom genesis block 
    INFO [11-04|10:34:14] Successfully wrote genesis state         database=chaindata                                           hash=5e1fc7…d790e0
    INFO [11-04|10:34:14] Allocated cache and file handles         database=/Users/nino/shuju/geth/lightchaindata cache=16 handles=16
    INFO [11-04|10:34:14] Writing custom genesis block 
    INFO [11-04|10:34:14] Successfully wrote genesis state         database=lightchaindata                                           hash=5e1fc7…d790e0
    

    初始化成功后,会在数据目录~/nino/shuju 中生成 geth 和 keystore 两个文件夹。(其中geth/chaindata中存放的是区块数据,keystore中存放的是账户数据)

    nino
    ├── shuju
    │   ├── geth
    │   │   └── chaindata
    │   │       ├── 000002.ldb
    │   │       ├── 000003.log
    │   │       ├── CURRENT
    │   │       ├── LOCK
    │   │       ├── LOG
    │   │       └── MANIFEST-000004
    │   └── keystore
    └── genesis.json
    

    启动私有链

    初始化完成后,就有了一条自己的私有链,之后就可以启动自己的私有链节点并做一些操作,在终端中输入以下命令即可启动节点:

    geth -datadir shuju -networkid 2018 -rpc -rpcaddr 你的IP -rpccorsdomain "*" console
    

    上面命令的主体是geth console,表示启动节点并进入交互式控制台,--datadir选项指定使用data0作为数据目录,--networkid选项后面跟一个数字 2018,表示指定这个私有链的网络id为2018。网络id在连接到其他节点的时候会用到,以太坊公网的网络 id 是1,为了不与公有链网络冲突,运行私有链节点的时候要指定自己的网络 id。运行上面的命令后,就启动了区块链节点并进入了Javascript Console:

    INFO [11-04|16:51:13.102] Starting P2P networking 
    INFO [11-04|16:51:15.224] UDP listener up                          self=enode://1d464c3eeff748edaf49ff582c4face5a42be81ec6be37d2044c408935dbb76d95a2bb66b38fcf874cb59a124238cd00c86ce0fad96499490ef24f573272cee5@[::]:30303
    INFO [11-04|16:51:15.225] RLPx listener up                         self=enode://1d464c3eeff748edaf49ff582c4face5a42be81ec6be37d2044c408935dbb76d95a2bb66b38fcf874cb59a124238cd00c86ce0fad96499490ef24f573272cee5@[::]:30303
    INFO [11-04|16:51:15.227] IPC endpoint opened                      url=/Users/nino/shuju/geth.ipc
    INFO [11-04|16:51:15.227] HTTP endpoint opened                     url=http://172.19.93.219:8545  cors=* vhosts=localhost
    Welcome to the Geth JavaScript console!
    
    instance: Geth/v1.8.14-stable/darwin-amd64/go1.11
    INFO [11-04|16:51:15.314] Etherbase automatically configured       address=0x78d2E183cb87bEb22fb8d09ef34675F8bbAc8e15
    coinbase: 0x78d2e183cb87beb22fb8d09ef34675f8bbac8e15
    at block: 95 (Sun, 04 Nov 2018 16:27:03 CST)
     datadir: /Users/nino/shuju
     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
    

    这是一个交互式的 Javascript 执行环境,在这里面可以执行 Javascript 代码,其中 > 是命令提示符。在这个环境里也内置了一些用来操作以太坊的 Javascript 对象,可以直接使用这些对象。这些对象主要包括:

    对象 用途
    eth 包含一些跟操作区块链相关的方法
    net 包含以下查看 P2P 网络状态的方法
    admin 包含一些与管理节点相关的方法
    miner 包含启动、停止挖矿的一些方法
    personal 主要包含一些管理账户的方法
    txpool 包含一些查看交易内存池的方法
    web3 包含了以上对象,还包含一些单位换算的方法

    私有链节点操作

    创建账户

    刚搭建好的私有链是没有账户的,输入:

    > eth.accounts
    

    发现账户为空,接下来使用personal对象来创建一个账户:

    > personal.newAccount("账户密码")
    

    我们通过这种方法再创建账户:

    > eth.accounts
    ["0x78d2e183cb87beb22fb8d09ef34675f8bbac8e15", "0x6f1f96c9131ad12f1ddcd9d195fdce6d0790b063"]
    

    账户默认会保存在数据目录的 keystore 文件夹中。查看目录结构,发现shuju/keystore中多了两个文件(分别对应于两个账户),这两个文件就对应刚才创建的两个账户,这是 json 格式的文本文件,可以打开查看,里面存的是私钥经过密码加密后的信息。比如 "...8e15" :

    {"address":"78d2e183cb87beb22fb8d09ef34675f8bbac8e15","crypto":{"cipher":"aes-128-ctr","ciphertext":"c1a6c65452655416e0dde61ac1e45c7a98c36d224d9a6ce2b688bb64d44c00f0","cipherparams":{"iv":"57cc60fb258e5e2bfc0adef32ed18324"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"fb65ff0c0dc5bf41f27897b12ce617c5cc971ea3ab809bb0105a33c374d7f3bc"},"mac":"aee378c2c16e566eb569f8da2ea2d2edc75f6d772e9dd276d6e1507e402494da"},"id":"515b2084-8f92-4053-9e5b-15ce917a66a3","version":3}
    

    此时的目录结构如下:

    shuju
    ├── geth
    │   ├── chaindata
    │   ├── LOCK
    │   ├── nodekey
    │   └── nodes
    ├── geth.ipc
    ├── history
    └── keystore
        ├── UTC--2018-11-04T08-23-15.866675000Z--78d2e183cb87beb22fb8d09ef34675f8bbac8e15
        └── UTC--2018-11-04T09-03-50.634292000Z--6f1f96c9131ad12f1ddcd9d195fdce6d0790b063
    

    查看余额

    查看账户余额:

    > eth.getBalance(eth.accounts[0])
    0
    > eth.getBalance(eth.accounts[1])
    0
    

    目前两个账户的以太币余额都是0,要使账户有余额,可以从其他账户转账过来,或者通过挖矿来获得以太币奖励。

    节点挖矿

    启动挖矿:

    > miner.start(1)
    

    其中start的参数表示挖矿使用的线程数。第一次启动挖矿会先生成挖矿所需的 DAG 文件,这个过程有点慢,等进度达到100%后,就会开始挖矿,此时屏幕会被挖矿信息刷屏。

    如果想停止挖矿,在 js console中输入:

    > miner.stop()
    

    输入的字符会被挖矿刷屏信息冲掉,没有关系,只要输入完整的miner.stop()之后回车,即可停止挖矿。

    挖到一个区块会奖励5个以太币,挖矿所得的奖励会进入矿工的账户,这个账户叫做coinbase,默认情况下 coinbase 是本地账户中的第一个账户:

    > eth.coinbase
    "0x78d2e183cb87beb22fb8d09ef34675f8bbac8e15"
    

    挖到区块以后,账户 0 里面应该就有余额了:

    > eth.getBalance(eth.accounts[0])
    475000000000000000000
    

    getBalance()返回值的单位是weiwei是以太币的最小单位,1个以太币=10的18次方个wei。要查看有多少个以太币,可以用web3.fromWei()将返回值换算成以太币:

    > web3.fromWei(eth.getBalance(eth.accounts[0]),'ether')
    475
    

    发起交易

    账户每隔一段时间就会被锁住,要发送交易,必须先解锁账户。由于我们要从账户 0 发送交易,所以要解锁账户 0:

    > personal.unlockAccount(eth.accounts[0])
    Unlock account 0x78d2e183cb87beb22fb8d09ef34675f8bbac8e15
    Passphrase: 
    true
    

    可以通过发送一笔交易,从账户 0 转移 5 个以太币到账户 1(⚠️ 最后显示的交易 ID 十分重要,我们将会用这个 ID 来查找交易):

    > amount = web3.toWei(5,'ether')
    "5000000000000000000"
    > eth.sendTransaction({from:eth.accounts[0],to:eth.accounts[1],value:amount})
    INFO [11-04|17:28:06.737] Setting new local account                address=0x78d2E183cb87bEb22fb8d09ef34675F8bbAc8e15
    INFO [11-04|17:28:06.738] Submitted transaction                    fullhash=0x67ef39ca8d8c8bd3dec2e96342a3ccffaaa3385140a0d8ded4c24b20b3be854d recipient=0x6F1f96c9131aD12f1dDcd9D195Fdce6d0790b063
    "0x67ef39ca8d8c8bd3dec2e96342a3ccffaaa3385140a0d8ded4c24b20b3be854d"
    

    此时交易已经提交到区块链,返回了交易的 hash,但还未被处理,这可以通过查看 txpool 来验证:

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

    其中有一条 pending 的交易,pending 表示已提交但还未被处理的交易。

    确认交易

    要使交易被处理,必须要挖矿。这里我们启动挖矿,然后等待挖到一个区块之后就停止挖矿:

    > miner.start(1); admin.sleepBlocks(1); miner.stop();
    

    如下显示表示挖矿成功:

    INFO [11-04|17:31:12.566] Updated mining threads                   threads=1
    INFO [11-04|17:31:12.566] Transaction pool price threshold updated price=18000000000
    INFO [11-04|17:31:12.572] Commit new mining work                   number=96 uncles=0 txs=0 gas=0 fees=0 elapsed=6.319ms
    INFO [11-04|17:31:12.573] Commit new mining work                   number=96 uncles=0 txs=1 gas=21000 fees=0.000378 elapsed=6.867ms
    INFO [11-04|17:31:16.781] Successfully sealed new block            number=96 hash=ddc2bb…215191 elapsed=4.208s
    INFO [11-04|17:31:16.782] 🔨 mined potential block                  number=96 hash=ddc2bb…215191
    INFO [11-04|17:31:16.782] Commit new mining work                   number=97 uncles=0 txs=0 gas=0     fees=0        elapsed=171.356µs
    true
    

    txpool中pending的交易数量应该为0了,说明交易已经被处理了:

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

    此时,交易已经生效,账户一应该已经收到了5个以太币了:

    > web3.fromWei(eth.getBalance(eth.accounts[1]),'ether')
    5
    

    区块数量

    查看当前区块总数:

    > eth.blockNumber
    96
    

    所以说目前我们的私有链上有 96 个区块。

    查看交易

    通过交易hash查看交易:

    > eth.getTransaction("0x67ef39ca8d8c8bd3dec2e96342a3ccffaaa3385140a0d8ded4c24b20b3be854d")
    {
      blockHash: "0xddc2bbbeaa686cc5c9c6b2d3d8a9f942a53a9051bc06e9d649e4de1e3a215191",
      blockNumber: 96,
      from: "0x78d2e183cb87beb22fb8d09ef34675f8bbac8e15",
      gas: 90000,
      gasPrice: 18000000000,
      hash: "0x67ef39ca8d8c8bd3dec2e96342a3ccffaaa3385140a0d8ded4c24b20b3be854d",
      input: "0x",
      nonce: 0,
      r: "0xb852d78a0483ba05a21e3701c74bd86ed2dd52867b59258170e7feb8ba3f0414",
      s: "0x3c752d3d485e1f7ed16bec78629be07bf377c4de9b041b11388263d943a66fd3",
      to: "0x6f1f96c9131ad12f1ddcd9d195fdce6d0790b063",
      transactionIndex: 0,
      v: "0x557",
      value: 5000000000000000000
    }
    

    交易中各个字段的含义

    • hash是交易的哈希值,nonce用来区别同一用户发出的不同交易的标记;
    • blockHash是封装事务(交易)的区块哈希值,blockNumber是该区块在我们构建的私有链的序号;
    • fromto 字段规定了交易的双方,value字段规定了交易的数额;
    • gas 表示这个交易允许消耗的最大 Gas 数量,gasPrice表示发送者愿意支付给矿工的 Gas 价格;
    • rsV 为交易签名的三个部分,由发送者的私钥对交易哈希进行签名生成。

    查看区块

    通过区块号查看区块:

    > eth.getBlock(96) 
    > eth.getBlock("0xddc2bbbeaa686cc5c9c6b2d3d8a9f942a53a9051bc06e9d649e4de1e3a215191")
    {
      difficulty: 131072,
      extraData: "0xd78301080e846765746886676f312e31318664617277696e",
      gasLimit: 15275265,
      gasUsed: 21000,
      hash: "0xddc2bbbeaa686cc5c9c6b2d3d8a9f942a53a9051bc06e9d649e4de1e3a215191",
      logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
      miner: "0x78d2e183cb87beb22fb8d09ef34675f8bbac8e15",
      mixHash: "0x4b72e2f521d9a45d53920d0af22c575eeb2ad489bab41d0b509584b86bd096ff",
      nonce: "0x0dc5a55c24b91e34",
      number: 96,
      parentHash: "0xff22f6f888c08e290b292ad105b9c72b4ff206c57e78a8393d38a94b4ea6ddd3",
      receiptsRoot: "0xa45d92ea3058f83814c424a8d4bdf5c3cd65742545569c3f3ede75b2ca2e6be1",
      sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
      size: 651,
      stateRoot: "0x346e3b94dfaebfb891f5d9d41204b83ed173230fa9d6e62016e9f3453f7cbcaf",
      timestamp: 1541323872,
      totalDifficulty: 12844361,
      transactions: ["0x67ef39ca8d8c8bd3dec2e96342a3ccffaaa3385140a0d8ded4c24b20b3be854d"],
      transactionsRoot: "0x9fae89d7136c768294f0eb495f140308ee858f2e54de9b537f89970a4e917fee",
      uncles: []
    }
    

    区块中各个字段的含义

    • difficulty 是挖出该区块的难度,totalDifficulty是目前挖矿的难度积累, nonce 是用来 PoW 的无意义6位哈希数;

    • hash 是区块的哈希值,mixHash 256位 Hash 值,当与 nonce 组合时,证明此区块已经执行了足够的计算;

    • size为区块的大小,miner 挖掘出这个区块的作者地址;

    • number 区块的序号,等于其父区块的 number +1,timestamp 区块应被创建的时间戳,由共识算法确定;

    • gasLimit 区块的 gas 消耗的理论上限,gasUsed 是实际消耗的 gas 总和;

    • parentHash父区块的指针,uncles 叔区块 是用来对孤立区块进行奖励的一个 Header 数组,sha3Uncles叔区块 们的安全散列;

    • receiptsRoot Block 中的 "Receipt Trie”的根节点的 RLP 哈希值,Block 的所有 Transaction 执行完后会生成一个 Receipt 数组,这个数组中的所有 Receipt 被逐个插入一个 MPT 结构中,形成"Receipt Trie";

    • stateRoot StateDB 中的“state Trie”的根节点的 RLP 哈希值,每个账户以 stateObject 对象表示,账户以 Address 为唯一标示,其信息在相关交易的执行中被修改,所有账户对象可以逐个插入一个Merkle Patrica Trie(MPT)结构里,形成“state Trie”;

    • transactionRoot Block 中 “tx Trie”的根节点的 RLP 哈希值,Block 的成员变量 transactions 中所有的 tx 对象,被逐个插入一个 MPT 结构,形成“tx Trie”;

    • logsBloom Bloom 过滤器,用来快速判断一个参数 Log 对象是否存在于一组已知的 Log 集合中;

    • extraData 区块创建者可以记录一些与该区块有关的信息,长度小于等于32字节即可。

    智能合约

    合约定义

    用 Solidity 定义一个智能合约 Nino.propose()

    pragma solidity ^0.4.18;
    
    contract Nino {
        string word;
        
        function Nino(string _word) public {
            word = _word;
        }
    
        function propose() constant public returns (string) {
            return word;
        }
    }
    

    Remix 文本框复制这段代码,进行调试,编译无误后点击 Detail,

    [图片上传失败...(image-ce368b-1541334018261)]

    可以从弹出窗口找到 WEB3DEPLOY,把红线部分的话换成 Nino 的 propose:

    image

    部署合约

    复制以上内容,因为部署合约本质上也是一种交易,在部署前需要把部署者 account[1] 解锁(Unlock)

    > personal.unlockAccount(eth.accounts[1])
    Unlock account 0x6f1f96c9131ad12f1ddcd9d195fdce6d0790b063
    Passphrase: 
    true
    

    并把其输入 JS Console:

    var _word = "I'm 16340154, please give me a high score";
    var ninoContract = web3.eth.contract([{"constant":true,"inputs":[],"name":"propose","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_word","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]);
    var nino = ninoContract.new(
       _word,
       {
         from: web3.eth.accounts[1], 
         data: '0x608060405234801561001057600080fd5b506040516102a83803806102a8833981018060405281019080805182019291905050508060009080519060200190610049929190610050565b50506100f5565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061009157805160ff19168380011785556100bf565b828001600101855582156100bf579182015b828111156100be5782518255916020019190600101906100a3565b5b5090506100cc91906100d0565b5090565b6100f291905b808211156100ee5760008160009055506001016100d6565b5090565b90565b6101a4806101046000396000f300608060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063c198f8ba14610046575b600080fd5b34801561005257600080fd5b5061005b6100d6565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561009b578082015181840152602081019050610080565b50505050905090810190601f1680156100c85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b606060008054600181600116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001161561010002031660029004801561016e5780601f106101435761010080835404028352916020019161016e565b820191906000526020600020905b81548152906001019060200180831161015157829003601f168201915b50505050509050905600a165627a7a7230582001962db1111855ac0f746def769de3d5475e132807426af7c33f7abe7c417a060029', 
         gas: '4700000'
       }, function (e, contract){
        console.log(e, contract);
        if (typeof contract.address !== 'undefined') {
             console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
        }
     })
    

    即使复制上去了之后,我们还不能着急运行,我们还要进行挖矿把 pending 的交易封装成块miner.start()让交易得到确认才能运行合约

    > miner.start(1); admin.sleepBlocks(1); miner.stop();
    INFO [11-04|20:06:17.040] Updated mining threads                   threads=1
    INFO [11-04|20:06:17.040] Transaction pool price threshold updated price=18000000000
    INFO [11-04|20:06:17.040] Commit new mining work                   number=162 uncles=0 txs=0 gas=0       fees=0          elapsed=124.139µs
    INFO [11-04|20:06:17.041] Commit new mining work                   number=162 uncles=0 txs=1 gas=242793  fees=0.004370274 elapsed=517.207µs
    INFO [11-04|20:06:18.184] Successfully sealed new block            number=162 hash=3bae74…9bb332 elapsed=1.143s
    INFO [11-04|20:06:18.192] 🔗 block reached canonical chain          number=157 hash=787e98…9ebcc5
    INFO [11-04|20:06:18.192] 🔨 mined potential block                  number=162 hash=3bae74…9bb332
    INFO [11-04|20:06:18.192] Commit new mining work                   number=163 uncles=0 txs=0 gas=0       fees=0           elapsed=161.24µs
    true
    > null [object Object]
    

    终于成功了 ✌️:

    Contract mined! address: 0x0ea4925971b634f91310d8d1d61fad8734b9a09f transactionHash: 0xb2ac1920b1fddd2d6b032ac9204ac963a8eaf0e793a0b8803ce5433e8c85ae68
    

    那么 Nino 的 proposal 到底是什么呢???🤔️

    > nino.propose()
    "I'm 16340154, please give me a high score"
    

    *希望它能够实现,因为它为了部署这个合约可是花了钱呢 *💰:

    > eth.getBalance(eth.accounts[1])
    4979037452000000000                     // < 5000000000000000000
    

    参考&鸣谢

    “喝水不忘挖井人”,这里特别感谢一下几个博主的优秀博文:

    相关文章

      网友评论

          本文标题:以太坊私有链搭建

          本文链接:https://www.haomeiwen.com/subject/tmedxqtx.html