[prev: Canonical NYC Subway Station Names] [home]

Provisioning a Local Private Ethereum Network with Puppeth

Introduction

This article is aimed at developers who know the basics of Ethereum and would like to test out their contracts on a test network. It is also aimed at developers frustrated with the amount of outdated information about Ethereum on the internet.

The new Puppeth tool released with Geth1.6 allows developers to easily set up private networks with Ethereum in order to test out their contracts without spending real ether. This article will go through the process of setting up two connected nodes that can access the same contract. Variables in {braces} should be replaced before running commands.

Some of the below information is from Frank Wang's notes from the 17Apr2017 Taipei Ethereum Meetup.

Instructions

  1. Install geth1.6 or later, which comes with puppeth.
  2. Create the following directory structure and enter the testnet directory.
    testnet
    |-- node1
    |-- node2
  3. Create two accounts on different nodes. Passwords are not necessary for a local network.
    $ geth --datadir node1 account new
    $ geth --datadir node2 account new
    geth will print out the address for each user, which is like an identifier. Denote these hexadecimal values as {ADDR1} and {ADDR2} respectively.
  4. Create a new blockchain genesis file with puppeth. This specifies the first block of the new blockchain, as well as gives the users some ether to start with. Some of the output below has been truncated for brevity.
    $ puppeth
    Please specify a network name to administer (no spaces, please)
    > testnet
    
    What would you like to do? (default = stats)
    1. Show network stats
    2. Configure new genesis
    3. Track new remote server
    4. Deploy network components
    > 2
    
    Which consensus engine to use? (default = clique)
    1. Ethash - proof-of-work
    2. Clique - proof-of-authority
    > 2
    
    How many seconds should blocks take? (default = 15)
    > 10
    
    Which accounts are allowed to seal? (mandatory at least one)
    
    (addresses from the account creation above, replace with your own)
    > 0x{ADDR1}
    > 0x{ADDR2}
    > 0x<ENTER>
    
    Which accounts should be pre-funded? (advisable at least one)
    > 0x{ADDR1}
    > 0x{ADDR2}
    > 0x<ENTER>
    
    Specify your chain/network ID if you want an explicit one (default = random)
    > <ENTER>
    
    Anything fun to embed into the genesis block? (max 32 bytes)
    > <ENTER>
    
    What would you like to do? (default = stats)
    1. Show network stats
    2. Save existing genesis
    3. Track new remote server
    4. Deploy network components
    > 2
    
    Which file to save the genesis into? (default = testnet.json)
    > <ENTER>
    
    What would you like to do? (default = stats)
    1. Show network stats
    2. Save existing genesis
    3. Track new remote server
    4. Deploy network components
    > <CTRL-C>
  5. Now start both nodes. Open four terminals in the testnet directory, which I'll label serv1, serv2, console1, console2. Below, shell prompts will be prefixed with the name of the terminal.
    serv1$ geth --datadir node1 init testnet.json
    serv1$ geth --datadir node1 --port 3000
    serv2$ geth --datadir node2 init testnet.json
    serv2$ geth --datadir node2 --port 3002
    You could use any distinct, unused ports.
  6. Now start two consoles and connect them to the node servers via IPC.
    console1$ geth attach ipc:node1/geth.ipc
    console2$ geth attach ipc:node2/geth.ipc
  7. Connect the two nodes.
    console2> admin.nodeInfo.enode
    {ENODE-URL}
    console1> admin.addPeer({ENODE-URL})
    If everything went well, net.peerCount should be 1 in both consoles. Note that if you're running node2 on another computer, you'll need to replace [::] in the enode URL with the IP address of node2.
  8. Start mining. In both consoles, run
    > personal.unlockAccount(eth.coinbase)
    > eth.defaultAccount = eth.coinbase
    > miner.start()
    You should see some mining-related output on the serv terminals. Note you may have to occasionally re-unlock your account throughout this process.
  9. Create a Solidity contract. There is a brief tutorial and full documentation available. I'm also including a simple contract file for publishing Merkle tree root hashes below.
    pragma solidity ^0.4.11;
    
    contract MerkleProofs {
        address public publisher;
        string public publisher_name;
        struct MerkleProof {
            bytes32 root_hash;
            uint timestamp;
        }
        MerkleProof[] public merkle_proofs;
        event ProofPublished(bytes32 root_hash, uint timestamp);
        function MerkleProofs(string input_publisher_name) {
            publisher = msg.sender;
            publisher_name = input_publisher_name;
        }
        function publishProof(bytes32 input_root_hash) {
            require(msg.sender == publisher);
            merkle_proofs.push(MerkleProof({
                root_hash: input_root_hash,
                timestamp: block.timestamp
            }));
            ProofPublished(getLatestProofRootHash(), getLatestProofTimestamp());
        }
        function getLatestProofRootHash() constant returns (bytes32 root_hash) {
            require(merkle_proofs.length > 0);
            root_hash = merkle_proofs[merkle_proofs.length - 1].root_hash;
        }
        function getLatestProofTimestamp() constant returns (uint timestamp) {
            require(merkle_proofs.length > 0);
            timestamp = merkle_proofs[merkle_proofs.length - 1].timestamp;
        }
    }
  10. Compile the contract file. Since the geth console compiler has been deprecated, an easy way to do this is with solc or the online Solidity IDE. Paste your contract, click the Contract tab, and copy the text in the cell labeled Web3 deploy. If there are any constructor variables (like input_publisher_name in the example), make sure to insert values for them in the deploy code. Compiled example below.
    var input_publisher_name = "Widgets Corporation"
    var mp_sol_merkleproofsContract = web3.eth.contract([{"constant":true,"inputs":[],"name":"publisher","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"merkle_proofs","outputs":[{"name":"root_hash","type":"bytes32"},{"name":"timestamp","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"getLatestProofRootHash","outputs":[{"name":"root_hash","type":"bytes32"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"input_root_hash","type":"bytes32"}],"name":"publishProof","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"getLatestProofTimestamp","outputs":[{"name":"timestamp","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"publisher_name","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"inputs":[{"name":"input_publisher_name","type":"string"}],"payable":false,"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"root_hash","type":"bytes32"},{"indexed":false,"name":"timestamp","type":"uint256"}],"name":"ProofPublished","type":"event"}]);
    var mp_sol_merkleproofs = mp_sol_merkleproofsContract.new(
    input_publisher_name,
    {
        from: web3.eth.accounts[0], 
        data: '0x6060604052341561000c57fe5b60405161068b38038061068b833981016040528080518201919050505b33600060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508060019080519060200190610080929190610088565b505b5061012d565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106100c957805160ff19168380011785556100f7565b828001600101855582156100f7579182015b828111156100f65782518255916020019190600101906100db565b5b5090506101049190610108565b5090565b61012a91905b8082111561012657600081600090555060010161010e565b5090565b90565b61054f8061013c6000396000f30060606040523615610076576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680638c72c54e14610078578063aa45a36f146100ca578063becceac91461010d578063d32933de1461013b578063e5660c471461015f578063f4890bfd14610185575bfe5b341561008057fe5b61008861021e565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34156100d257fe5b6100e86004808035906020019091905050610244565b6040518083600019166000191681526020018281526020019250505060405180910390f35b341561011557fe5b61011d610278565b60405180826000191660001916815260200191505060405180910390f35b341561014357fe5b61015d6004808035600019169060200190919050506102c2565b005b341561016757fe5b61016f6103d9565b6040518082815260200191505060405180910390f35b341561018d57fe5b610195610423565b60405180806020018281038252838181518152602001915080519060200190808383600083146101e4575b8051825260208311156101e4576020820191506020810190506020830392506101c0565b505050905090810190601f1680156102105780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60028181548110151561025357fe5b906000526020600020906002020160005b915090508060000154908060010154905082565b6000600060028054905011151561028f5760006000fd5b60026001600280549050038154811015156102a657fe5b906000526020600020906002020160005b506000015490505b90565b600060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561031f5760006000fd5b6002805480600101828161033391906104c1565b916000526020600020906002020160005b6040604051908101604052808560001916815260200142815250909190915060008201518160000190600019169055602082015181600101555050507fefe2b279a82d87ce91dfb1a32840f3e856af29c46cb0cce3a93238e9f4e011246103a9610278565b6103b16103d9565b6040518083600019166000191681526020018281526020019250505060405180910390a15b50565b600060006002805490501115156103f05760006000fd5b600260016002805490500381548110151561040757fe5b906000526020600020906002020160005b506001015490505b90565b60018054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156104b95780601f1061048e576101008083540402835291602001916104b9565b820191906000526020600020905b81548152906001019060200180831161049c57829003601f168201915b505050505081565b8154818355818115116104ee576002028160020283600052602060002091820191016104ed91906104f3565b5b505050565b61052091905b8082111561051c57600060008201600090556001820160009055506002016104f9565b5090565b905600a165627a7a72305820ca59050af82d77eb81a4d30c75832dbf0cc4431b34d8b1ee4ac73191602e49f40029', 
        gas: '4700000'
    }, function (e, contract){
        console.log(e, contract);
        if (typeof contract.address !== 'undefined') {
         console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
        }
    })
  11. Enter the code into console 1's terminal. If all has gone well, you should shortly see a message that the contract mp_sol_merkleproofs (your variable name might be different) has been mined. Running
    console1> mp_sol_merkleproofs.address
    {CONTRACT-ADDR}
    should return a hex address and not be undefined. If this doesn't work, make sure you've connected your nodes properly and try running miner.stop(); miner.start() in both consoles.
  12. Use the contract. Only node1 can publish because node1 constructed the contract.
    console1> mp_sol_merkleproofs.publishProof("0xdeadbeef")
    Wait a while for the transaction to process, then
    console1> mp_sol_merkleproofs.getLatestProofRootHash()
    0xdeadbeef00000000000000000000000000000000000000000000000000000000
  13. Now connect the other node to the contract. In node2's terminal, paste only the contract ABI line from the compiled code output by the web compiler and initialize the contract to point to the one created by node1.
    console2> var mp_sol_merkleproofsContract = web3.eth.contract([{"constant":true,"inputs":[],"name":"publisher","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"merkle_proofs","outputs":[{"name":"root_hash","type":"bytes32"},{"name":"timestamp","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"getLatestProofRootHash","outputs":[{"name":"root_hash","type":"bytes32"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"input_root_hash","type":"bytes32"}],"name":"publishProof","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"getLatestProofTimestamp","outputs":[{"name":"timestamp","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"publisher_name","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"inputs":[{"name":"input_publisher_name","type":"string"}],"payable":false,"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"root_hash","type":"bytes32"},{"indexed":false,"name":"timestamp","type":"uint256"}],"name":"ProofPublished","type":"event"}]);
    console2> var mp_sol_merkleproofs = mp_sol_merkleproofsContract.at({CONTRACT-ADDR})
    console2> mp_sol_merkleproofs.getLatestProofRootHash()
    0xdeadbeef00000000000000000000000000000000000000000000000000000000
    Now both nodes have access to the contract. However, if you try publishing a proof with node2, this will fail because the code only allows the creator to publish proofs (note the line require(msg.sender == publisher);).
  14. Next steps may be to create action handlers for the events emitted by the contract, deploy a more complex contract, connect more nodes, or explore the more advanced features of puppeth.

Have anything to add? Let me know at surya at [this domain name].