Here are the rules: if you read this post all the way through, you have to deploy a smart contract on your private Ethereum blockchain yourself. I give you all the code I used here in Github so you have no excuses not to.
But if you don’t follow the rules and you only want to read, hopefully this helps give a perspective of starting with nothing and ending with a blockchain app.
By the end, you’ll have started a local private Ethereum blockchain, connected two different nodes as peers, written and compiled a smart contract, and have a web interface that allows users to ask questions, deploy the questions on the blockchain, and then lets the users answer.
If you’re confused, run into an error, or want to say something else, go ahead an write a comment, get in contact, or say something on Twitter.
Oh, and here’s the Github repo, so go ahead and fork it (if you don’t want to copy paste all the code here) and then if you make updates you want to share, I’ll throw this in the README.
To create a single node, we need the following genesis.json
, which represents the initial block on the private blockchain.
//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" }
If you want a somewhat full explanation of the fields, look at this Stack Overflow answer. The big ones in our case here are difficulty
being low, because we don’t want to have to wait long for blocks to be mined on our test network, and then gasLimit
being high to allow the amount of work that can be done by a node in the block to be able to process every transaction.
Go ahead and open a terminal, make sure geth
is installed in whatever way works for your OS, and then cd
into the folder that you have your genesis.json
file saved. Running run the following command will initialize the blockchain for this node.
$ geth --datadir "/Users/USERNAME/Library/PrivEth" init genesis.json
–datadir specifies where we want the all the data for the blockchain to be located. On a mac, the default is ~/Library/Ethereum. Since we have multiple nodes running, we can’t have them sharing the same data folder, so we’re going to specify. Linux and Windows machines have different default datadirs, so take a look at those to see in general where they should be located.
After running this init command with the genesis.json
file we want to use, go checkout that --datadir
directory. You’ll see a bunch of files, so feel free to poke around. Not necessary right now, but you’ll want to look around there eventually.
For this to be a blockchain, we need more than one node. For blockchains to become peers, we need them to have the same genesis file. So we’re going to run the same command as above, from the same directory, but this time with a different datadir
.
geth --datadir "/Users/USERNAME/Library/PrivEth2" init genesis.json
With all the code here, we’re going to be working in the same directory. The code is the same, but with the command line options, we’ll be able to separate these processes by the command line arguments.
When running geth
with a different --datadir
, you’ll be running separate nodes no matter where you ran the command from. Just remember to specify the --datadir
each time so it doesn’t fall back to default. Also note that I changed the names for these datadirs
, so watch out if you see different names in the screenshots.
So far, we’ve done three things. 1) Created a genesis.json
file in a working directory of your choosing, 2) picked a directory to store the blockchain for one node and initialized the first block, and 3) picked a different directory to store the blockchain for the other node. Very little code and a few commands.
The next step to be able to log into the geth console for each node. The console will start the geth process and run it, but also give us a way to run some web3 commands in the terminal.
geth --datadir "/Users/jackschultz/Library/EthPrivLocal" --networkid 72 --port 30301 --nodiscover console
There are a couple more options here.
–networkid is similar to in the genesis.json
file, where all we want here is to make sure we’re not using network ids 1-4.
–port specifies which port our .ipc file will be using. That’s the way we’ll connect with the database using the web3.js library. The default port is 30303, so we’ll keep it in that area, but this is our first node, so 30301 it is.
–nodiscover tells geth
to not look for peers initially. This is actually important in our case. This is a private network. We don’t want nodes to try to connect to other nodes without me specifying, and we don’t want these nodes to be discovered without us telling them.
With the first geth
node running, run the same command in a different terminal with the second --datadir
and and different --port
you’ll have nodes running.
When you have the console running from the command above, we want to create our main coinbase
account. If you’re curious, I used the passphrase ‘passphrase’. You’ll see we need that in our Node app down the road.
> personal.listAccounts [] > personal.newAccount() Passphrase: Repeat passphrase: 0x538341f72db4b64e320e6c7c698499ca68a6880c > personal.listAccounts [ "0x538341f72db4b64e320e6c7c698499ca68a6880c" ]
Run the same commands in the other node’s console as well.
Since this is the first account this node has created, you’ll see it’s also listed in
> eth.coinbase 0x538341f72db4b64e320e6c7c698499ca68a6880c
Another piece of information you can grab on the console is by running
> 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" }]
There you’ll see more information about the accounts instead of only the address. You’ll also see where that account information is stored, and it’ll be in the --datadir
you specified. So if you’re still curious how the data is stored in your filesystem, go checkout the directory now.
We have multiple nodes running, and we’ll need to connect them as peers. First we’ll check to see if we have peers.
> admin.peers []
So sad. This is what we expected where we started the console on a non 1-4 network id and the nodiscover
flag. This means that we need to tell each node to connect to the other node with a specific command. The way we do this is by sharing the enode
address.
> admin.nodeInfo.enode "enode://13b835d68917bd4970502b53d8125db1e124b466f6473361c558ea481e31ce4197843ec7d8684011b15ce63def5eeb73982d04425af3a0b6f3437a030878c8a9@[::]:30301?discport=0"
This is the enode
information that geth
uses to connect to different nodes where they’re able to share information about transactions and successful mining.
To connect the nodes using this url, we want to call the function addPeer
.
If we copy the return value of the admin.nodeInfo.enode
from one of the nodes, run the following command in the other node.
> admin.addPeer("enode://13b835d68917bd4970502b53d8125db1e124b466f6473361c558ea481e31ce4197843ec7d8684011b15ce63def5eeb73982d04425af3a0b6f3437a030878c8a9@[::]:30301?discport=0")
This tells one node how to get to the other node, will ask the other node to link up, and they’ll both become each other’s peers. To check, run the admin.peers command on both nodes and you’ll see they’re together. Something like:
> 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 } } }]
To add the peer, you only need to tell one node to connect to the other node so check the other node and you’ll see output like this.
Now that the nodes are connected, we’re not in the realm of money. Before we mine, we want to check the balances of our main account.
> eth.getBalance(eth.coinbase) 0 >
Again, so sad. Since we didn’t allocate ethers to this account on the genesis block, we need to start mining to get some for these accounts.
When in the console, we run miner.start()
for the node to start mining, and then miner.stop()
for it to stop. When mining, not only are we looking to see how many ethers the accounts get, we also want to watch the interaction of two nodes that are peers.
In the picture below, you’ll see I checked the balance of each main accounts for both nodes. Then on node 1, I started the mining, let it run for ~5 seconds, and then stopped the mining after 7 full blocks. I check the balance on that side and have 35 ether, where the number in the console represents Wei. On the other node, you’ll see that it received information of the 7 blocks that were mined from node 1.
Working with smart contracts requires special transactions, but before getting that far, we want to know how to create transactions that send ether to the other account.
On one node, let’s take the coinbase
account and unlock it.
> coinbaseAddress = eth.coinbase > personal.unlockAccount(coinbaseAddress) Unlock account 0x554585d7c4e5b5569158c33684657772c0d0b7e1 Passphrase: True
Now copy the address from the other node’s coinbase
account, and back in the node with the unlocked account,
> hisAddress = "0x846774a81e8e48379c6283a3aa92e9036017172a"
After this, the sendTransaction command is somewhat simple.
> eth.sendTransaction({from: eth.coinbase, to: hisAddress, value: 100000000}) INFO [12-09|10:29:36] Submitted transaction fullhash=0x776689315d837b5f0d9220dc7c0e7315ef45907e188684a6609fde8fcd97dd57 recipient=0x846774A81E8E48379C6283a3Aa92E9036017172A "0x776689315d837b5f0d9220dc7c0e7315ef45907e188684a6609fde8fcd97dd57"
One other thing to note, and something that you’ll be decently confused with a lot, is why these numbers for value are huge in terms of zeros. This is because values are represented in Wei, so we don’t have to deal with floating point numbers which could cause issues on different systems. This will come into play with gas
that we’ll need to start specifying for contract deployment and transactions.
If you’re wondering how few ether we’re sending with that value,
> web3.fromWei(100000000, 'ether') "0.0000000001"
To get the transaction to send, and to see the difference in balances, we need to start the miner in a node, and then stop it after a block is mined. Now check the balances to see the change
> miner.start() ............... > miner.stop() > web3.eth.getBalance(eth.coinbase) 59999999999900000000 > web3.eth.getBalance(hisAddress) 100000000
Alright, check out this giant picture below. Again, node 1 is on the left, node 2 on the right. So I first check balances for each coinbase account on each node. The on node 1, I copy node 2’s address, send the transaction, and then logging from the node that it has received a submitted transaction. Then I start the mining. You’ll see that node 8 has txs=1
meaning it’s mined a transaction into that block. After a few more blocks, I stop the mining. I check the balance of node 1’s account. We have 12 blocks with rewards of 5 ether each, but then gave away 100000000 Wei.
Now, I go back to node 2, check the balance of its coinbase
account and see that it’s 0. Then I remembered I restarted the console for node 1 and didn’t set the two nodes back as peers. So I print the enode
of node 1, add that as a peer for node 2. You’ll see right after adding the peer, node 2 receives the blocks it missed, including 1 transaction. Then I check the balance again and it knows it has 100000000 Wei.
At this point, we’re about half done! We’ve worked in a terminal having a private Ethereum blockchain running locally, two nodes that have accounts, are peers with each other, and can send transactions back and forth.
That’s pretty good, so if you want to take a second to calm down and get a slightly better understanding, go ahead. But at some point, we want to move on.
Moving on! With the geth
nodes running, the next step is getting into contracts.
When writing posts like this, it takes a long time to pick a simple yet worthwhile example. And that was the case for me when trying to pick a type of contract to use. The one I decided to throw in here is one where people are able to answer yes / no, or true / false questions.
The final v1 code for the Solidity contract is below. A few notes before you look at it.
uint
s to store the yes / no answers instead of bool
s. In Solidity, if I have a mapping that links addresses to a bool
, the default is FALSE. For a uint
, the default is zero. This lets us have the three states we need. I could have used an enum
here, but like I said, we’re staying simple.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]); } }
I store this contract in contracts/Question.sol, but instead of doing the compiling locally, I use Remix which handles a bunch of things, in terms of tons of errors and warnings of the code, as well as compiling the required information.
To see the compiling information, on the upper right “compile” tab, click the details button and you’ll see a bunch of information pop up. The data we’re looking for is the byteCode and ABI. Right below that is the web3 deploy information which is exactly what we’re going to mimic! But rather than having giant strings on a single line, we’re going to import the information from a json file. Gotta keep that data separate.
//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" }
Go ahead and straight copy this file, but I’d say go to Remix and work with the compiler they have there so you can get a feel for that as well. Quick thing to mention is for the byteCode
, you need to make sure that string starts with “0x”. When you copy the byte code field from Remix you only get the numbers.
From above, every time I said node, I meant the geth
/ blockchain node. Here, we’ll be seeing the word “node” again, but when you see the capital N, we mean NodeJS.
We have the v1 contract compiled and stored in a file. Now we need to get a Node instance running. There are four endpoints we’re going to have.
Deploying Question
Preface, before going into blockchains I hadn’t used Node in forever, so some of the syntax and practices might be off here. For the code, I’ll go through the three endpoints that talk to the blockchain. The first is a post request to deploy a new question. I threw the code that’s needed to connect to your locally running geth
as well.
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); }); }); });
When we hit the endpoint, the first step, after grabbing the request from the body, is to unlock the account that we’re deploying from. This is necessary so we’re not impersonating someone else. Once we get the callback, we’re going to deploy the contract where the data of the transaction is the entire byteCode, and then we pass in the question string for the init function in the contract. We specify we’re sending it from the coinbase
address, and saying that we’re investing 2000000 Wei (which is 0.000000000002 ether if you’re wondering how small it is).
There are more than a few callbacks we can use here, but the only one we’re interested in right now is the ‘receipt’, where we’re given the contract’s address after it’s been mined. In terms of UI, the way this is written is that the page will hang, waiting for the contract to be mined, before redirecting to the question’s page. This probably isn’t a good idea at all for a wide use DAPP because mining blocks on the public Ethereum averages ~14.5 seconds. But here on our private blockchain, we set the difficulty to be so low that blocks are mined very quickly, so it isn’t an issue.
Viewing Question
Now that we have a question that exists, we want to go ahead and view it! We use the web3.utils.isAddress
function to verify that the address is not only a valid hex string, but also verifies that the check sum is valid which makes sure it’s an existing address.
Then our getQuestion
method returns a result that’s a dictionary of the return values. In our case, that’s the question, the number of trues, number of falses, and also whether or not the person running the node has answered the question yet.
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."); } });
Answering the Question
When we post to that question url, we go through much of the same process of validating the input, validating the address, and then calling the answerQuestion
method with the required parameters. Along with the question creation function, we’re going to have the browser hang until the block with the update transaction is mined.
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
As for the HTML, I’m not going to bother posting it here because it’s quite simple. I didn’t bother to use a css template because it doesn’t matter in a backend post like this. You’ll see screenshots of the basic interface below while I talk about running the code.
Running The Code
Now all the code is out there. You have four tabs on the console open. Two are running 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
and the other two are running the Node apps, connected to separate geth
processes, and running on different localhost ports. I added config files, named them primary and secondary to point to the ipc and port that Node should run on.
NODE_ENV=primary node app.js
NODE_ENV=secondary node app.js
I threw in some pictures here so people reading can know more about what I’m seeing on my screen. On that, lets go to the browser and start interacting. First up is going to the home page where you can ask a question.
Then when you hit the submit button, you’ll see the logging from the Node app, in the geth console you’ll start the miner and then stop it after the block with this transaction is complete.
To answer, you’ll submit the form, then start and stop the mining. When you’re doing this yourself, a fun thing to do is start the miner before submitting the answer form so you can get a sense of how quickly blocks are mined with this small level of difficulty defined in the genesis block.
Check out the terminal below. In the top Node terminal you’ll see some logging about validating the address, and then logging when we’re redirected to the same page but with updated information. In the geth
console, you can see when the transaction was submitted, along with which block the transaction was mined on.
Now that we answered the question from the primary node, let’s check out the secondary one.
On the right side of the picture, you’ll see the top two terminals showing the Node and geth
interactions, and then on the bottom is the primary geth
which you can see that it received blocks with a transaction in it because the two geth
nodes are peers. After the question was answered by the node on port 4002, I reloaded the page on port 4001 and we can see the result.
Just to show that we can switch back to false, I changed the answer from port 4002 to false (which is wrong, cause the Bucks are definitely going to make the playoffs), and then you can see the console logging the information of what went through.
If you’ve gotten this far and have the code running yourself, big cong. Like most of these posts, this is much longer than I had initially imagined it being. The goal with this is to go through and explain all the steps of a smart contract rather than somewhere in the middle.
原文地址: https://bigishdata.com/2017/12/15/how-to-write-deploy-and-interact-with-ethereum-smart-contracts-on-a-private-blockchain/