实操:如何在私有区块链上编写、部署以及与以太坊进行交互的智能合约
原文:How To Write, Deploy, and Interact with Ethereum Smart Contracts on a Private Blockchain
作者:jack_schultz
//genesis.json { "alloc": {}, "config": { "chainID": 72, "homesteadBlock": 0, "eip155Block": 0, "eip158Block": 0 }, "nonce": "0x0000000000000000", "difficulty": "0x4000", "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000", "coinbase": "0x0000000000000000000000000000000000000000", "timestamp": "0x00", "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "extraData": "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa", "gasLimit": "0xffffffff" }
如果希望对字段有一个完整的解释,看看这个堆栈溢出的解答。这个案例中的difficulty是很低的,因为不希望在测试网络上等待很长的时间,区块才能被挖掘出来,然后gasLimit 的值高到允许区块中的一个节点可以完成的工作量能够处理每个交易。
去打开一个终端,确保geth(以太坊客户端)以任何适用于你的操作系统的方式进行安装,然后cd(dos命令)到保存genesis.json的文件夹。运行以下命令,将初始化该节点的区块链。
$ geth --datadir "/Users/USERNAME/Library/PrivEth" init genesis.json
-datadir指定区块链所有数据的位置。在Mac操作系统上,默认是 ~/Library/Ethereum目录。由于有多个节点在运行,所以不能让它们共享相同的数据文件夹,因此需要具体指定。Linux和Windows机器具有不同的默认datadir,所以请查看这些数据一般应该位于何处。
用genesis.json文件运行完初始化命令之后,去检查那个--datadir目录,会看到一堆文件,所以随意四处看看吧。现在没有必要,但是最终还是要去看看。
对于这样一个区块链,需要多个节点。要使区块链成为peers,它们需要拥有相同的创始文件。所以要从同一个目录运行和上面相同的命令,但是这次使用了不同的datadir。
geth --datadir "/Users/USERNAME/Library/PrivEth2" init genesis.json
这里所有的代码,将在同一个目录下工作。代码是一样的,但是使用命令行选项,可以通过命令行参数区分这些进程。
初始化两个节点的链。
当通过一个不同的--datadir运行geth,无论从哪里运行命令,都将运行单独的节点。只要记得每次指定--datadir,那么它就不会回到默认值。另外请注意,我更改了这些datadirs的名称,所以会在屏幕截图中看到不同的名称。
打开控制台
到目前为止,已经做了三件事。1)在选择的工作目录中创建了一个genesis.json 文件,2)为一个节点选择一个目录存储区块链,并初始化第一个区块,3)为另外一个节点选择一个不同的目录存储区块链。很少的代码和一些命令。
下一步能够登录到每个节点的geth控制台。控制台将启动geth进程并运行它,也给了在终端上运行一些web3命令的方法。
geth --datadir "/Users/jackschultz/Library/EthPrivLocal" --networkid 72 --port 30301 --nodiscover console
这里还有更多的选择。
-networkid与genesis.json文件中的类似,在这里所需要做的是确保不使用网络ID 1-4。
-port指定.ipc文件将要用到的端口。这就是使用web3.js库连接数据库的方式,默认端口是30303。所以将它保留在那个区域,但这是第一个节点,所以它的端口是30301。
nodiscover告诉geth最初不要找peers。这一点在这个案例中确实很重要。这是一个私有网络。不希望节点在没有指定的情况下尝试连接到其它节点,不希望这些节点在没有告诉它们的情况下被发现。
在第一个geth节点运行的情况下,在有第二个—datadir的不同终端运行相同的命令,节点在不同的端口上运行。
启动控制台。为每个节点创建初始Coinbase帐户
当用上面的命令运行控制台时,想要创建主coinbase帐户。如果感到好奇,使用密码短语“passphrase”,将来Node应用程序会用到“passphrase”。
> personal.listAccounts [] > personal.newAccount() Passphrase: Repeat passphrase: 0x538341f72db4b64e320e6c7c698499ca68a6880c > personal.listAccounts [“0x538341f72db4b64e320e6c7c698499ca68a6880c”]
在另一个节点的控制台中运行相同的命令。
创建新的帐户。
由于这是该节点创建的第一个帐户,因此会看到它也列在其中
> eth.coinbase 0x538341f72db4b64e320e6c7c698499ca68a6880c
在控制台上抓取的另一条信息
> personal.listWallets [{ accounts:[{ address:“0x538341f72db4b64e320e6c7c698499ca68a6880c”, url:“keystore:///Users/jackschultz/Library/EthPrivLocal/keystore/UTC--2017-12-09T16-21-48.056824000Z--538341f72db4b64e320e6c7c698499ca68a6880c” }], status:“locked”, url:“keystore:///Users/jackschultz/Library/EthPrivLocal/keystore/UTC--2017-12-09T16-21-48.056824000Z--538341f72db4b64e320e6c7c698499ca68a6880c” }]
在那里会看到更多有关帐户的信息,而不是只有地址。还会看到帐户信息的存储位置,它会在指定的--datadir。所以如果仍然好奇数据是如何存储在文件系统中的,那就去查看一下目录。
以Peers连接节点
有多个节点正在运行,需要以peers连接它们。首先检查我们是否有peers
> admin.peers []
好难过。这是我们期望的,在非1-4网络ID和nodiscover的标志上启动控制台。这意味着需要告知每个节点用特定的命令连接到另一个节点。通过分享enode 地址的方式来做。
> admin.nodeInfo.enode “enode:// 13b835d68917bd4970502b53d8125db1e124b466f6473361c558ea481e31ce4197843ec7d8684011b15ce63def5eeb73982d04425af3a0b6f3437a030878c8a9 @ [:]:30301 discport = 0”
这是geth用来连接到不同节点的enode信息,在这些不同的节点它们能够分享交易和成功挖掘信息。
要使用这个URL连接节点,需要调用addPeer函数。
如果要复制从其中一个节点admin.nodeInfo.enode的返回值,请在另一个节点中运行以下命令。
> admin.addPeer(“enode:// 13b835d68917bd4970502b53d8125db1e124b466f6473361c558ea481e31ce4197843ec7d8684011b15ce63def5eeb73982d04425af3a0b6f3437a030878c8a9 @ [::]:30301?discport = 0”)
这告知一个节点如何到达另一个节点,并请求另一个节点连接起来,它们都将成为彼此的peers。如需检验,请在两个节点上运行admin.peers命令,将看到它们连接在一起。代码如下:
> admin.peers [{ caps: ["eth/63"], id: "99bf59fe629dbea3cb3da94be4a6cff625c40da21dfffacddc4f723661aa1aa77cd4fb7921eb437b0d5e9333c01ed57bfc0d433b9f718a2c95287d3542f2e9a8", name: "Geth/v1.7.1-stable-05101641/darwin-amd64/go1.9.1", network: { localAddress: "[::1]:30301", remoteAddress: "[::1]:50042" }, protocols: { eth: { difficulty: 935232, head: "0x8dd2dc7968328c8bbd5aacc53f87e590a469e5bde3945bee0f6ae13392503d17", version: 63 } } }]
要添加peer,只需要告诉一个节点连接到另一个节点,然后检查另一个节点,就会看到如下输出:
Peers on peers。
检查余额并挖掘
既然节点连接起来了,就不是钱的事了。在开始挖掘之前,检查一下主账户的余额。
> eth.getBalance(eth.coinbase) 0 >
再一次如此悲伤。由于没有把这个帐户分配给创始区块,需要开始为这些账户挖矿。
在控制台中,运行miner.start()为这个节点开始挖掘,然后运行miner.stop()可以停止挖掘。在挖掘时,不仅要看账号得到多少以太币,还要观察两个节点之间点对点的交互。
在下面的图片中,会看到检查了两个节点各自的主帐户余额。然后在节点1上开始挖掘,让它运行大约5秒,然后在7个完整区块之后停止挖掘。检查另一边的余额,有35个以太币,在控制台中这个数字代表Wei。在另一个节点上,将会看到它收到了从节点1挖掘的7个区块的信息。
开始挖掘。
交易
使用智能合约需要专门的交易,但在实现这一点之前,要知道如何创建一个交易,将以太币发送到另一个帐户。
在一个节点上,采用coinbase账户并解锁它。
> coinbaseAddress = eth.coinbase > personal.unlockAccount(coinbaseAddress) Unlock account 0x554585d7c4e5b5569158c33684657772c0d0b7e1 Passphrase: True
现在从另一个节点的coinbase帐户复制地址,并回到未解锁的帐户节点
> hisAddress = "0x846774a81e8e48379c6283a3aa92e9036017172a"
在此之后,sendTransaction命令有点简单。
> eth.sendTransaction({from: eth.coinbase, to: hisAddress, value: 100000000}) INFO [12-09|10:29:36] Submitted transaction fullhash=0x776689315d837b5f0d9220dc7c0e7315ef45907e188684a6609fde8fcd97dd57 recipient=0x846774A81E8E48379C6283a3Aa92E9036017172A "0x776689315d837b5f0d9220dc7c0e7315ef45907e188684a6609fde8fcd97dd57"
还有一件需要注意的事,而且会很容易混淆的,就是为什么这些数字的值有那么多0。这是因为值是用wei来表示的,所以不必处理可能在不同系统上引起问题的浮点数。这将与gas(一个与计算步骤大致相当的测量法。每笔交易都需要包括一个Gas限制和一个愿意为每个Gas支付的费用;矿工可以选择进行交易和收费)一起发挥作用 ,需要开始指定合同部署和交易。
如果想知道用这个值发送了多少以太币,命令如下:
> web3.fromWei(100000000,'ether') “0.0000000001”
要使交易发送,并且看到不同余额的差异,需要在节点中启动矿工,然后在挖掘了一个区块后停止,现在检查余额以查看变化。
> miner.start() ............... > miner.stop() > web3.eth.getBalance(eth.coinbase) 59999999999900000000 > web3.eth.getBalance(hisAddress) 100000000
接下来看看下面的这张巨幅图片。同样,节点1在左边,节点2在右边。所以首先检查每个节点上各自coinbase账户的余额。在节点1上,复制节点2的地址,发送交易,然后从接收到提交的交易的节点登录,接着开始挖掘。会发现节点8 有txs=1,这意味着它在那个区块挖掘了一笔交易。再多挖几个区块以后,停止挖掘。检查节点1的帐户余额。有12个区块,每个区块奖励5以太币,但后来却付出了100000000wei。
现在,回到节点2,检查其coinbase帐户的余额,余额是0。然后,记得重新启动过节点1的控制台,并没有将两个节点设置为peers。因此,打印节点1的enode,作为一个peer将其添到节点2。在添加peer后,会看到节点2接收到错过的块,包括1个交易。然后再次检查余额,发现它有100000000Wei。
这是如何在本地发送以太币。
间歇
到这里,差不多完成了一半的工作!在一个拥有本地运行的私有以太坊区块链的终端上工作,拥有账户的两个节点,彼此是peers,并且可以来回发送交易。
这相当不错,所以可以花一点时间冷静下来,有一个更好的理解。但是在此刻,请继续前进。
在Remix上编写一个合约
继续!随着geth节点的运行,下一步就是签订合约。
当写这样的文章时,需要花很长时间来选择一个简单而有价值的例子。当试图选择一种合约来使用时,情况亦是如此。我决定摆在这里的是人们可以回答是/否或真/假的问题。
下面是Solidity(是以太坊中用于开发智能合约的编程语言,目前开发智能合约用的最多的是Solidity)合约的最终v1代码。在看代码之前,有一些注意事项:
- 在这个例子中,只使用全局变量来解决问题,是谁问了这个问题,谁回答了这个问题,以及答案的值。Solidity也有可以存储数据的结构,但是本文在讨论部署而不是Solidity,所以不要太深入。
- 使用 uints来存储是/否的答案,而不是bools。在Solidity中,如果有将地址链接到bool的映射,则默认值为FALSE。对于一个uint,默认值是零。这有了必要的三种状态,在这里可以用一个enum,但正如我所说,尽量保持简单。
- answerQuestion方法在逻辑和if语句中都有些复杂。如果想了解如何调整变量,请仔细阅读它。
-
有一个get函数,返回所有想要在页面上显示合约状态的信息。可以分开来分别返回不同的信息,但是不妨把它们放在一起,而不必多次查询。
-在合约中不仅有其它方式存储这些数据,还有很多其它的方式来编写它!例如,可以列出所有投票为true或false的账户,然后循环查询它们是否已经回答。pragma solidity ^0.4.0; contract Questions { //global variables that aren't in a struct mapping(address => uint) public answers; //integer where 0 means hasn't answered, 1 means yes, 2 means no string question; address asker; uint trues; uint falses; /// __init__ function Questions(string _question) public { asker = msg.sender; question = _question; } //We need a way to validate whether or not they've answered before. //The default of a mapping is function answerQuestion (bool _answer) public { if (answers[msg.sender] == 0 && _answer) { //haven't answered yet answers[msg.sender] = 1; //they vote true trues += 1; } else if (answers[msg.sender] == 0 && !_answer) { answers[msg.sender] = 2; //falsity falses += 1; } else if (answers[msg.sender] == 2 && _answer) { // false switching to true answers[msg.sender] = 1; //true trues += 1; falses -= 1; } else if (answers[msg.sender] == 1 && !_answer) { // true switching to false answers[msg.sender] = 2; //falsity trues -= 1; falses += 1; } } function getQuestion() public constant returns (string, uint, uint, uint) { return (question, trues, falses, answers[msg.sender]); } }
把这个合约保存在contracts/Question.sol中,而不是在本地进行编译,使用Remix来处理大量的错误和代码警告,以及编译所需的信息。
要查看编译信息,在右上角的“编译”选项卡上单击详细信息按钮,就会看到一堆信息弹出。要寻找的数据是byteCode和ABI。右下方正是要模仿的web3的部署信息!但是,不是从一个单一的行上输入巨大的字符串,而是要从一个json文件中导入信息。必须把数据分开。
//childContractv1.json { "abi": [{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"answers","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getQuestion","outputs":[{"name":"","type":"string"},{"name":"","type":"uint256"},{"name":"","type":"uint256"},{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_answer","type":"bool"}],"name":"answerQuestion","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_question","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}], "byteCode": "0x6060604052341561000f57600080fd5b6040516106d23803806106d28339810160405280805182019190505033600260006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508060019080519060200190610082929190610089565b505061012e565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106100ca57805160ff19168380011785556100f8565b828001600101855582156100f8579182015b828111156100f75782518255916020019190600101906100dc565b5b5090506101059190610109565b5090565b61012b91905b8082111561012757600081600090555060010161010f565b5090565b90565b6105958061013d6000396000f300606060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680635e9618e71461005c578063eff38f92146100a9578063f9e049611461014c575b600080fd5b341561006757600080fd5b610093600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610171565b6040518082815260200191505060405180910390f35b34156100b457600080fd5b6100bc610189565b6040518080602001858152602001848152602001838152602001828103825286818151815260200191508051906020019080838360005b8381101561010e5780820151818401526020810190506100f3565b50505050905090810190601f16801561013b5780820380516001836020036101000a031916815260200191505b509550505050505060405180910390f35b341561015757600080fd5b61016f60048080351515906020019091905050610287565b005b60006020528060005260406000206000915090505481565b610191610555565b600080600060016003546004546000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200............................600460008282540392505081905550610550565b60016000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541480156104e3575080155b1561054f5760026000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550600160036000828254039250508190555060016004600082825401925050819055505b5b5b5b50565b6020604051908101604052806000815250905600a165627a7a7230582043defebf8fa91b1cd010927004a7ff4816a1040b9cabd4ddd22122a9816742ff0029" }
继续直接复制这个文件,到Remix上并与Remxi的编译器一起工作。值得一提的是byteCode,需要确保字符串以“0x”开始。当从Remix复制字节码字段时,只能得到数字。
NodeJS时间
上面每次说到节点,意思是geth / blockchain节点。在这里,将再次看到“node”这个词,但是当看到大写字母N时,它的意思是NodeJS。
已经将v1合约编译并存储在一个文件中。现在需要运行一个Node(这个Node首字母大写了,所以它代表NoteJS)实例。将有四个端点:
- GET’/’将会有一个表单提出一个新的问题,
- POST’/ questions / new’在区块链上部署新问题合约,
- GET’/ questions?address = 0xXXXX …’将依据当前的答案显示问题以及一个发送或更新答案的表单,
-POST’/ questions?address = 0xXXXX …’处理应答。
部署问题
前言,在进入区块链之前,从来没用使用过Node,所以有一些语法和实践可能会在这里无效。对于代码,会通过与区块链交互的三个端点,首先是部署新问题的post请求。这些代码需要连接到本地运行的geth。
const Web3 = require('web3'); const net = require('net'); const compiledContract = require('./contracts/contractv1'); web3IPC = '/Users/jackschultz/Library/PrivEth/geth.ipc'; let web3 = new Web3(web3IPC, net); const byteCode = compiledContract.byteCode; const QuestionContract = new web3.eth.Contract(compiledContract.abi); web3.eth.getCoinbase(function(err, cba) { coinbaseAddress = cba; console.log(coinbaseAddress); }); const coinbasePassphrase = 'passphrase'; app.post('/', (req, res) => { const question = req.body.question; web3.eth.personal.unlockAccount(coinbaseAddress, coinbasePassphrase, function(err, uares) { QuestionContract.deploy({data: byteCode, arguments: }).send({from: coinbaseAddress, gas: 2000000}) .on('receipt', function (receipt) { console.log("Contract Address: " + receipt.contractAddress); res.redirect('/questions?address=' + receipt.contractAddress); }); }); });
当到达端点时,从主体获取请求后的第一步是解锁正在部署的帐户。这是必要的,不模仿别人。一旦得到回调,将部署合约,其中交易的数据是整个的字节码,然后将问题字符串传递给合约中的init函数。指定从coinbase地址发送它,并说明要投入2000000wei(如果想知道它到底有多小,那就是0.000000000002 以太币)。
但现在唯一感兴趣的是“收据”,合约被挖掘以后,它的地址在哪里。就用户界面而言,这样写的方式是在重定向到问题的页面之前,页面会挂起,等待合约被挖掘。对于广泛使用的DAPP(Decentralized App,去中心化的应用程序)来说,这可能不是一个好主意,因为公共以太坊挖掘区块平均约为14.5秒。但是这里的私有区块链上,把难度设置的如此之低,以至于区块很快被挖掘完,所以这不是问题。
检视问题
现在既然有一个问题存在,就想继续讨论它!使用web3.utils.isAddress函数来验证地址不仅是一个有效的十六进制字符串,而且还验证校验和是有效的,确保它是一个存在的地址。
然后getQuestion 方法返回一个结果,这是一个返回值的字典。在本文的例子中,这是一个问题,true的数量,false的数量,以及运行节点的人是否回答了这个问题。
app.get('/questions', function(req, res) { const contractAddress = req.query.address; if (web3.utils.isAddress(contractAddress)) { QuestionContract.options.address = contractAddress; const info = QuestionContract.methods.getQuestion().call(function(err, gqres) { //using number strings to get the data from the method const question = gqres['0']; const trues = gqres['1']; const falses = gqres['2']; const currentAnswerInt = parseInt(gqres['3'], 10); data = {contractAddress: contractAddress, question: question, currentAnswerInt: currentAnswerInt, trues: trues, falses: falses}; res.render('question', data); }); } else { res.status(404).send("No question with that address."); } });
回答问题
当发布这个问题url时,经过大部分相同的过程验证输入,验证地址,然后用所需参数调用answerQuestion方法。随着问题创建函数的出现,将让浏览器挂起,直到有更新交易的区块被挖掘出来。
app.post('/questions', function(req, res) { const contractAddress = req.query.address; const answerValue = req.body.answer == 'true' ? true : false; if (web3.utils.isAddress(contractAddress)) { web3.eth.personal.unlockAccount(coinbaseAddress, coinbasePassphrase, function(err, uares) { QuestionContract.options.address = contractAddress; QuestionContract.methods.answerQuestion(answerValue).send({from: coinbaseAddress, gas: 2000000}) .on('receipt', function (receipt) { console.log(`Question with address ${contractAddress} updated.`); res.redirect('/questions?address=' + contractAddress); } ); }); } });
HTML
至于HTML,不打算费心把它贴在这里,因为它很简单。我不喜欢使用CSS模板,因为像这样在后台提交并不重要。当谈论到运行代码时,会看到以下基本界面的屏幕截图。
运行代码
现在所有的代码都在那里。控制台上有四个选项卡打开。两个正在运行geth
geth --datadir /Users/jackschultz/Library/PrivEth --networkid 40 --port 30301 --nodiscover console geth --datadir /Users/jackschultz/Library/PrivEth2 --networkid 40 --port 30302 --nodiscover console
另外两个正在运行Node应用程序,连接到单独的geth过程,并运行在不同的本地主机端口上。添加了配置文件,将它们命名为主文件和辅助文件,指向ipc和该节点应当运行的端口。
NODE_ENV=primary node app.js NODE_ENV=secondary node app.js
在这里放一些图片,让阅读的人可以更多地了解我在屏幕上看到的内容。在此基础上,打开浏览器并开始交互。首先是进入主页,可以问一个问题。
他们会吗??
然后当点击提交按钮时,会看到Node应用程序的日志记录,在geth控制台中,将启动矿工,然后在这个交易完成后停止它。
是时候回答问题了。
回答的话,要提交表单,然后开始和停止挖矿。当自己做这件事的时候,一件有趣的事情就是在提交答案之前先启动矿工,这样就可以了解在创始块中定义的这个小难度级别的挖掘速度。
检查下面的终端。在顶级Node终端中,将看到有关验证地址的一些日志记录,然后当重定向到同一页面但具有更新信息时记录。在geth控制台中,可以看到交易何时提交,以及这笔交易是在哪个区块进行的。
显然他们会。
现在从主节点回答了这个问题,接着看看第二个节点。
在图片的右侧,会看到前两个终端显示Node和geth交互,然后底部是主要的geth,可以看到它接收到一个交易的区块,因为这两个geth节点是peers。在端口4002上的节点回答问题后,重新加载了端口4001上的页面,可以看到下图的结果。
当然会的。
为了证明可以切换回false,把端口4002的答案改成了false(这是错误的,因为雄鹿队肯定会打入季后赛),然后可以看到控制台记录了所经历的信息。
截图后,改回答案为true。
结论
如果已经看到了这里,并且让自己的代码运行起来了,恭喜。 像大多数帖子一样,这比我最初想象的要长得多。这样做的目的是完成并解释智能合约的所有步骤,而不是只给出中间的某个地方。
- 发表于 2017-12-26 15:55
- 阅读 ( 1123 )
- 分类:以太坊
你可能感兴趣的文章
相关问题
0 条评论
作家榜 »
- 社区运营-小链142 文章
- 社区运营-小以67 文章
- 于中阳Mercina-zy56 文章
- 涂晶54 文章
- 吴寿鹤34 文章
- 李晓琼17 文章
- Alexander12 文章
- baidang20112 文章