Welcome
以前粗略了解过一些比特币和区块链的理论知识,利用本次休假的时间,趁机熟悉一下Ethereum 和 Solidity,尝试创建一个简单的只能合约。
What is geth
本文中所有的操作都在 geth 中运行,geth是一个全功能的以太坊节点,它使用golang语言实现。geth 为我们提供了区块信息查看、挖矿、运行智能合约等功能。
ethereum 和 bitcoin 的一些区别
1、ethereum 的里的脚本代码是图灵完备的,而比特币的脚本则是基于栈的,有一定局限性。
2、ethereum 基于账户模型,没有UXTO(Unspent Transaction Outputs)概念,交易速度会慢一些。
How to install geth
如果你已经安装了 homebrew Mac 上 安装 geth 非常容易:
brew tap ethereum/ethereum
brew install ethereum
其他的安装方式可以参考文档:
https://ethereum.gitbooks.io/frontier-guide/content/installing_linux.html
Run geth
在使用geth前有两个重要的参数需要指定:
1、 --datadir "~/ethmain" datadir 指定在哪里存储区块的数据(包含账户)
2、--networkid "1" 指定网络ID,约定1代表主网络也就是以太坊的公共网络, 通常用2代表测试网络, 我们可以指定任意非负ID,作为自己的私有链。
Create own private ethereum
本文的目标是创建一个私有链,并在该链上挖矿和运行一个简单的智能合约。
1、设置数据的存储目录为: “~/ethprivate”
2、网络ID :“9527”
Before create
在创建私有链前我们先创建几个外部账户,方便后续操作,以太坊的账号分为外部账户和合约账户两种,本次将创建两个外部账户。
使用 geth account 指令可以进行账户相关的操作。
使用 geth account new 指令创建账户,如果在js 控制台中(后面会说明) 则可以用 personal.newAccount("passphrase") 指令
geth --datadir "~/ethprivate" --networkid 9527 account new
指令运行后要求输入 passphrase 设置一个简单的口令即可 如:“12345”:
Your new account is locked with a password. Please give a password. Do not forget this password.
Passphrase:
Repeat passphrase:
Address: {0df066922b375d8641b992352958664ff26b570d}
运行 geth account list 可以查看已有的账户(在js控制台里使用 eth.accounts)
geth --datadir "~/ethprivate" --networkid 9527 account list
可以查看到刚才创建的账户
Account #0: {0df066922b375d8641b992352958664ff26b570d} keystore:///${HOME}/ethprivate/keystore/UTC--2018-02-13T03-27-14.788253000Z--0df066922b375d8641b992352958664ff26b570d
The genesis block
现在准备创建第一个区块,通常称该区块为创世块。在创世块里我们需要进行一些简单的配置,主要是挖矿的难度设置、发放一些货币到刚创建的账户中。通过一个json文件设置,下面是详细配置
{
"config": {
"chainId": 9527,
"homesteadBlock": 0,
"eip155Block": 0,
"eip158Block": 0
},
"nonce": "0x0000000000000042",
"timestamp": "0x2537",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"extraData": "0x2537",
"gasLimit": "0x80000000",
"difficulty": "0x10",
"mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x3333333333333333333333333333333333333333",
"alloc": {
"0df066922b375d8641b992352958664ff26b570d":{
"balance":"10"
}
}
}
将上面的配置保存为文件.json 的文件,然后执行初始化指令:
geth --datadir "~/ethprivate" --networkid 9527 init genesis.json
成功创建后会输出以下信息
INFO [02-13|11:51:54] Allocated cache and file handles database=${HOME}/ethprivate/geth/chaindata cache=16 handles=16
INFO [02-13|11:51:54] Writing custom genesis block
INFO [02-13|11:51:54] Successfully wrote genesis state database=chaindata hash=eb3e89…697b61
INFO [02-13|11:51:54] Allocated cache and file handles database=${HOME}/ethprivate/geth/lightchaindata cache=16 handles=16
INFO [02-13|11:51:54] Writing custom genesis block
INFO [02-13|11:51:54] Successfully wrote genesis state database=lightchaindata hash=eb3e89…697b61
The JavaScript Console
geth 提供了一个Javascript 运行环境,使用 geth console 命令可以进入JS Console。在JS Console可以使用更多的操作,后面的智能合约部署也将在 JS Console进行。
这里建议将标准错误重定向到另一个文件里
geth console 2>>geth.log
并在另一个终端使用 tail -f geth.log 监控输出。
geth --datadir "~/ethprivate" --networkid 9527 console 2>>geth.log
Welcome to the Geth JavaScript console!
instance: Geth/v1.7.3-stable/darwin-amd64/go1.9.4
coinbase: 0x0df066922b375d8641b992352958664ff26b570d
at block: 0 (Thu, 01 Jan 1970 10:38:47 CST)
datadir: ~/ethprivate
modules: admin:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0
查询一下第一个账户的余额,我们在创世块里向其预先分配了 10个币:
> eth.getBalance(eth.accounts[0])
10
查看一下创世块的信息
>eht.getBlock(0)
{
difficulty: 16,
extraData: "0x00",
gasLimit: 2147483648,
gasUsed: 0,
hash: "0xeb3e8947d2e01afbb6651c0276921f5f44b4c549a89942285716d935aa697b61",
logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
miner: "0x3333333333333333333333333333333333333333",
mixHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
nonce: "0x0000000000000042",
number: 0,
parentHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
receiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
size: 507,
stateRoot: "0x2b4a324f7683f0f14efef5b1f5c6ac9432b223da4e9ce56ecbc47482402c13d1",
timestamp: 9527,
totalDifficulty: 16,
transactions: [],
transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
uncles: []
}
输入 exit 可以退出 JS console;
Other Commands
进入 console 时能看到其加载的js modules
Welcome to the Geth JavaScript console!
instance: Geth/v1.7.3-stable/darwin-amd64/go1.9.4
coinbase: 0x0df066922b375d8641b992352958664ff26b570d
at block: 0 (Thu, 01 Jan 1970 10:38:47 CST)
datadir: ~/ethprivate
modules: admin:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0
admin 查看一些网络和 节点相关的信息
如:admin.nodeInfo
eth 进行一些区块相关的操作
如: eth.accounts() 可以查看当前的账户列表eth.getBlock(N) 可以查看具体的区块信息。 eth.getBalance() 查看某个账户的金额
- admin 查看一些网络和 节点相关的信息
如:admin.nodeInfo - personal 可以进行一些账号相关的操作,
如果控制台抛出以下错误
Error: authentication needed: password or unlock
at web3.js:3143:20
at web3.js:6347:15
at web3.js:5081:36
at web3.js:4137:16
at <anonymous>:1:1
说明某项命令需要解锁账户,可以通过:
personal.unlockAccount(eth.accounts[0],"passwd")
因为本身是JS 所以可以在控制台里直接查看某个Module对象支持的属性和方法。
The miner
通过miner 提供的函数可以进行挖矿,挖矿会将算出新的区块,并将交易池中的交易打包到区块里。挖矿的奖励默认发放到第一个账户中。
在控制台中输入下面指令启动挖矿。
miner.start()
在我们监控的 log 文件里能看到挖矿的信息
Commit new mining work number=8 txs=0 uncles=0 elapsed=139.017µs
INFO [02-13|13:47:08] Successfully sealed new block number=8 hash=39f5db…99b268
使用eth.blockNumber 可以看到当前的已经挖出了多少区块。
使用 eth.getBlock(N) 可以查看具体的区块信息。
> eth.getBalance(eth.accounts[0])
115000000000000000010
可以看到挖矿的奖励已经发放到第一个账户中。
使用
miner.stop()
可以停止挖矿。
Transactions
因为挖矿的奖励都投放到了第一个账户中,我们再创建一个账户,将部分奖励转到新账户上。这就是传说中的“转账”功能。
> eth.getBalance(eth.accounts[0])
150000000000000000010
> eth.getBalance(eth.accounts[1])
0
在js 控制台上的转账很简单,参考一下指令
> user1=eth.accounts[0]
"0x0df066922b375d8641b992352958664ff26b570d"
> user2=eth.accounts[1]
"0x87eaff177048b0931475efd0b7837b67c91b7232"
> personal.unlockAccount(user1,"12345")
true
> eth.sendTransaction({from:user1,to:user2,value:web3.toWei(100,"ether")})
转账前如果账户是被锁住的,需要使用密码进行解锁。
查询一下,指令执行后的结果
> eth.getBalance(user1)
150000000000000000010
> eth.getBalance(user2)
0
发现金额并没有减少, 原因是这个交易只是进入了交易池,还没有打包到区块里,也就是没有被真正的认可。
使用txpool.status 可以待交易的数据。
> txpool.status
{
pending: 1,
queued: 0
}
启动挖矿完成交易
> miner.start(1)
null
> miner.stop()
true
> eth.getBalance(user2)
3000000000000000000
> txpool.status
{
pending: 0,
queued: 0
}
查看挖矿时的日志输出:
INFO [02-22|10:32:09] Starting mining operation
INFO [02-22|10:32:09] Commit new mining work number=31 txs=1 uncles=0 elapsed=416.345µs
INFO [02-22|10:32:18] Successfully sealed new block number=31 hash=6b9c9d…0fd396
INFO [02-22|10:32:18] ? mined potential block number=31 hash=6b9c9d…0fd396
INFO [02-22|10:32:18] Commit new mining work number=32 txs=0 uncles=0 elapsed=102.669µs
INFO [02-22|10:32:19] Successfully sealed new block number=32 hash=94bdc9…2c48da
INFO [02-22|10:32:19] ? mined potential block number=32 hash=94bdc9…2c48da
可以看到在第31一个区块下有一个 “txs=1” 交易被打包。
第31个Block的信息:
> eth.getBlock(31)
{
difficulty: 131072,
extraData: "0xd883010703846765746887676f312e392e348664617277696e",
gasLimit: 2083415374,
gasUsed: 21000,
hash: "0x6b9c9d4fec6a61b4b4294393340613084e96f6b14c3931697225ecdb130fd396",
logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
miner: "0x0df066922b375d8641b992352958664ff26b570d",
mixHash: "0xe66e02b7e4e0797c5e125b42c2e6ac333de3d05c20c6050922652a1466c8b5f4",
nonce: "0x2edd4febb730901b",
number: 31,
parentHash: "0xb04cbb2b541c7a5240d7d5bef817ca8717fdf156fb1e1372971e63676c2a86e1",
receiptsRoot: "0x1b6be00820be77d7acc990b0fba653a758eeadd7018c9854f09645c0dd00a259",
sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
size: 653,
stateRoot: "0xe06d4c3ec4245c189ee415b965876c8c937f5321b7f8f1175e7f1bd97bcead31",
timestamp: 1519266729,
totalDifficulty: 4080784,
transactions: ["0x4a9e719868f7713cd450c9c69d0ad87995999fac1be27d119d1ba24b9bd31c96"],
transactionsRoot: "0x5ca299ef95bfc52addf5a3a344d71fff750562ab97316476a5b44954089aefd7",
uncles: []
}
看到 transactions 里包含了:["0x4a9e719868f7713cd450c9c69d0ad87995999fac1be27d119d1ba24b9bd31c96"] 这是该交易的hash索引。
> eth.getTransaction("0x4a9e719868f7713cd450c9c69d0ad87995999fac1be27d119d1ba24b9bd31c96")
{
blockHash: "0x6b9c9d4fec6a61b4b4294393340613084e96f6b14c3931697225ecdb130fd396",
blockNumber: 31,
from: "0x0df066922b375d8641b992352958664ff26b570d",
gas: 90000,
gasPrice: 18000000000,
hash: "0x4a9e719868f7713cd450c9c69d0ad87995999fac1be27d119d1ba24b9bd31c96",
input: "0x",
nonce: 0,
r: "0xfcb2fc5a36cd29b2af329714a1776b35fa6f45f8b0b68d4e6b08269e95fbde67",
s: "0x64afcee6177262bf84ce416ac9c9ff6983394f9a6c686ee8f72f3d6fb88619d7",
to: "0x87eaff177048b0931475efd0b7837b67c91b7232",
transactionIndex: 0,
v: "0x4a91",
value: 3000000000000000000
}
> eth.accounts
["0x0df066922b375d8641b992352958664ff26b570d", "0x87eaff177048b0931475efd0b7837b67c91b7232"]
从 from 、to 、value 的数据里可以看到刚才的转账信息。
至此已经成功完成一笔转账交易。
A sample contract
我们将定义一个简单的智能合约,该合约提供付费刻字功能,可以将用户的名字刻录到区块链里,类似于“到此一游” %>_<%
Writing a contract
下面是相关的solidity 代码,调用carve 时需要支付大于 costForCarve 的费用。
pragma solidity ^0.4.20;
contract CarveName {
address owner;
string[] public names;
mapping (address=>uint) nameToOwner;
uint costForCarve = 1 wei;
event OnNameCarve(uint nameId,string name);
function CarveName() public {
owner = msg.sender;
}
function carve(string _name) external payable returns (uint) {
require(msg.value>=costForCarve);
uint id = names.push(_name);
nameToOwner[msg.sender] = id;
OnNameCarve(id,_name);
return id;
}
function getNameById(uint _id) external view returns(string) {
return names[_id];
}
function setCost(uint _cost) external onlyOwner {
costForCarve = _cost;
}
function withdrawCash() external onlyOwner {
owner.transfer(this.balance);
}
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
}
Compiling and deploying
为了简单方便,本次先使用在线的编译器Solidity浏览器--Remix 进行编译。
编译后获得ABI
[{\"constant\":false,\"inputs\":[{\"name\":\"_cost\",\"type\":\"uint256\"}],\"name\":\"setCost\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"names\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_name\",\"type\":\"string\"}],\"name\":\"carve\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":true,\"stateMutability\":\"payable\",\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_id\",\"type\":\"uint256\"}],\"name\":\"getNameById\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"withdrawCash\",\"outputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"payable\":false,\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"nameId\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"name\",\"type\":\"string\"}],\"name\":\"OnNameCarve\",\"type\":\"event\"}]
对应的BYTECODE
606060405266038d7ea4c68000600355341.................
回到控制台,利用上面的数据部署合约。
1、通过获得的“ABI”创建合约对象,其实就是创建一个js 对象。
//将ABI 数据转换为json
> carveNameAbi=JSON.parse('....')
//创建 Contract 对象
> carveNameContract = eth.contract(carveNameAbi)
//输入bytecod,注意需要以16进制表示,即0x 开头
> carveNameBytecode="0x..............."
//预估一下部署合约损耗的Gas
> eth.estimateGas({data:carveNameBytecode})
615383
> eth.getBalance(eth.coinbase)
182000000000000000010
//部署合约,即提交一个交易
> contractInterface = carveNameContract.new({data:carveNameBytecode,gas:625383,from:eth.coinbase})
//以下是输出:
{
abi: [....],
address: undefined,
transactionHash: "0x30a506b4d5c970ca0316c03e840ab8094346f5d981b18d5e09ce477f208f938f"
}
> txpool.status
{
pending: 1,
queued: 0
}
此时合约的部署还在待交易池中,启动挖矿完成交易。查看挖矿时的输出日志输出:
Submitted contract creation fullhash=0x559ad19c5e70c433febef9eb494c827d831e41b15ca596d35ee20a546aed2b5e contract=0x75ce54374b6f9CC304Ec2a912dCd54078BD163cd
Starting mining operation
INFO [02-22|15:21:29] Commit new mining work number=42 txs=1 uncles=0 elapsed=561.894µs
INFO [02-22|15:21:29] Successfully sealed new block number=42 hash=4426e8…df6472
INFO [02-22|15:21:29] ? block reached canonical chain number=37 hash=dcbccd…141029
INFO [02-22|15:21:29] ? mined potential block number=42 hash=4426e8…df6472
可以看到交易已记录在第42个区块上,同时获得了合约的地址:contract=0x75ce54374b6f9CC304Ec2a912dCd54078BD163cd ,该地址在执行合约时使用。
完整的 ContractInterface 信息如下:
{
abi: [....],
address: "0x75ce54374b6f9cc304ec2a912dcd54078bd163cd",
transactionHash: "0x559ad19c5e70c433febef9eb494c827d831e41b15ca596d35ee20a546aed2b5e",
OnNameCarve: function(),
allEvents: function(),
carve: function(),
getNameById: function(),
names: function(),
setCost: function(),
withdrawCash: function()
}
The transactions about contract
在js 控制台,通过 eth.getBlock 查询区块上的交易信息,本例中合约部署的信息写在了第42个区块上,
使用eth.getBlock 查看区块的信息,找到交易的地址。
> eth.getBlock(42)
{
difficulty: 131072,
extraData: "0xd883010703846765746887676f312e392e348664617277696e",
gasLimit: 2069214973,
gasUsed: 615383,
.....
transactions: ["0x559ad19c5e70c433febef9eb494c827d831e41b15ca596d35ee20a546aed2b5e"],
transactionsRoot: "0xa731fe9030b335a49e46c557d37717fba5fc045fd39400575e58d21282da0eb7",
uncles: []
}
通过eth.getTransactionReceipt 查询交易的详细信息:
> eth.getTransactionReceipt("0x559ad19c5e70c433febef9eb494c827d831e41b15ca596d35ee20a546aed2b5e")
{
blockHash: "0x4426e8e1680f290432a571871f2b2f97bb83b0b6603874665bcab44792df6472",
blockNumber: 42,
contractAddress: "0x75ce54374b6f9cc304ec2a912dcd54078bd163cd",
cumulativeGasUsed: 615383,
from: "0x0df066922b375d8641b992352958664ff26b570d",
gasUsed: 615383,
........
transactionHash: "0x559ad19c5e70c433febef9eb494c827d831e41b15ca596d35ee20a546aed2b5e",
transactionIndex: 0
}
可以看到交易的详细信息里包含了合约的地址:contractAddress: "0x75ce54374b6f9cc304ec2a912dcd54078bd163cd",
Other people run The code
在使用他人发布的合约前需要通过 ABI 和 contract Address 可以获取创建合约对象,和部署合约代码不同的是,部署时使用new方法,而获取则使用 at 方法。
> contract=eth.contract(abi)
> interface = contract.at(address)
有了接口对象后就可以调用合约上面的方法了,方法的调用可以分成两种。
> interface.carve.call("Hello 9527",{from:eth.accounts[0],value:web3.toWei(1)})
1
这里使用 call 方式调用,此时只是本地调用,不会产生交易,不会损耗Gas。
> interface.carve.sendTransaction("Hello 9527",{from:eth.accounts[0],value:web3.toWei(1)})
"0xb20a62ce226e77f766cbb540ba7dccdd15981227cc1f9932db9db12affdbca15"
使用sendTransaction 的方式抵用合约函数,会创建一个交易。
启动挖矿,等待矿工打包交易完成,完成后可以通过下面的指令查询到已写入的数据
> interface.getNameById.call(0)
"Hello 9527"
至此一个简单的智能合约已运行完备。
Thinking about blockchain
Q:如何看待比特币、区块链 ?前景如何?
A: 我的回答是看不透。
第一次接触比特币大概是在2012年的时候(年轻不懂事?,错过了一次暴富的机会),最近区块链的技术又火了起来。
区块链通过技术手段,建立一种去中心权威的 共识 机制,在一定条件下这个共识是可信任的。现实世界中往往需要第三方的权威机构(如各种企业公司、银行、**部门等)来保证这份信任。第三方机构也是人管理的,未必就万无一失。各种虚拟货币就是利用其“可信任性”和相对的“稀缺性”,将自身转换为资源。资源的存在可以使人投入价值,进行买卖,也就演变成了现在的炒币。 网上还有一种说法:“以前的网络是信息互联,而比特币是价值互联”。
在我看来这种“共识”机制,会触发权力的变革。仔细想想一旦机制成熟,很多的权威机构将失去权力,甚至不复存在。可是“权力的欲望” 可比普通的“物质欲” 高级多了。
对开发者而言,编程的模式又多了一种"智能合约",你可以直接代码的运行中获利。一个成熟的公有链上运行着无数的智能合约,运行产生的费用一部分给了矿工,一部分给了合约的创造者。说不定会形成一个 "合约市场" 客户根据需求找寻合适的“合约”。
以上都是我自己的一些胡乱想法,愚见莫笑,区块链技术到底会如何?
参考链接
1、https://ethereum.gitbooks.io/
2、https://cryptozombies.io/zh/course/
3、https://ethereum.gitbooks.io/frontier-guide/content/jsre.html