简介
以太坊合约就是以太坊区块链特定账户地址上的一串代码(functions)和数据(state)。合约账户不仅可以相互间通讯,还可以执行几乎所有的图灵完备计算。以太坊区块链上的合约代码是特定的二进制形式,被称作以太坊虚拟机(EVM)二进制代码。
然而,合约通常都是用一些高级语言进行开发编写,并被翻译成二进制代码,然后上传到区块链上。现有的以太坊开发高级语言包括:类似JavaScript的Solidity、类似Python的Serpent、类似Lisp的LLL等。本文以最受欢迎的Solidity为例。
Solidity是面向对象的高级以太坊开发语言,受到C++、Python和JavaScript的影响,支持静态类型、继承、类库和复杂的自定义特性。一个典型的合约开发过程包含:编写、编译、发布、调用等过程。
命令行工具Geth
以太坊网络命令行客户端有基于Go实现的Geth和基于C++实现的Eth。Geth主要用于web开发、Dapp前端搭建,而Eth主要用于GPU挖矿。除此之外,二者功能几乎完全一致。本文以简单易用、广受欢迎的Geth为例。
安装与启动
安装:
brew tap ethereum/ethereum
brew install ethereum
运行:
geth console
默认会连接到以太坊主网上,而主网不仅运行缓慢,还非常昂贵,转账、部署或调用合约通常均需消耗以太币,因此不能直接用于开发。
搭建私有测试网络
我们需要搭建私有测试网络,只有你自己一个成员,你来负责所有的出块、交易验证、执行智能合约等任务,可以帮助我们低成本、简单快速地开发合约。
初始化
用genesis.json初始化创世区块,并设置datadir目录:
geth --datadir ~/.ethereum_private init ~/dev/genesis.json
{
"config": {
"chainId": 12345,
"homesteadBlock": 0,
"eip155Block": 0,
"eip158Block": 0
},
"nonce": "0x0000000000000033",
"timestamp": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"gasLimit": "0xffffffff",
"difficulty": "0x100",
"mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x3333333333333333333333333333333333333333",
"alloc": {}
}
其中指定了chainid、gaslimit等配置。
初始化提交结果如下:
INFO [02-13|16:08:23] Maximum peer count ETH=25 LES=0 total=25
INFO [02-13|16:08:23] Allocated cache and file handles database=/Users/shanyao/.ethereum_private/geth/chaindata cache=16 handles=16
INFO [02-13|16:08:23] Writing custom genesis block
INFO [02-13|16:08:23] Persisted trie from memory database nodes=0 size=0.00B time=7.737µs gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [02-13|16:08:23] Successfully wrote genesis state database=chaindata hash=76bc36…4efba1
INFO [02-13|16:08:23] Allocated cache and file handles database=/Users/shanyao/.ethereum_private/geth/lightchaindata cache=16 handles=16
INFO [02-13|16:08:23] Writing custom genesis block
INFO [02-13|16:08:23] Persisted trie from memory database nodes=0 size=0.00B time=1.16µs gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [02-13|16:08:23] Successfully wrote genesis state database=lightchaindata hash=76bc36…4efba1
启动
用该datadir启动私有网络,并通过进程间通讯路径(ipc path)绑定一个console,如下:
geth --fast --cache 512 --ipcpath ~/Library/Ethereum/geth.ipc --networkid 12345 --datadir ~/.ethereum_private console
结果如下:
INFO [02-13|16:09:20] Maximum peer count ETH=25 LES=0 total=25
INFO [02-13|16:09:20] Starting peer-to-peer node instance=Geth/v1.8.2-stable/darwin-amd64/go1.10
INFO [02-13|16:09:20] Allocated cache and file handles database=/Users/shanyao/.ethereum_private/geth/chaindata cache=384 handles=1024
WARN [02-13|16:09:20] Upgrading database to use lookup entries
INFO [02-13|16:09:20] Database deduplication successful deduped=0
INFO [02-13|16:09:20] Initialised chain configuration config="{ChainID: 123456 Homestead: 0 DAO: <nil> DAOSupport: false EIP150: <nil> EIP155: 0 EIP158: 0 Byzantium: <nil> Constantinople: <nil> Engine: unknown}"
INFO [02-13|16:09:20] Disk storage enabled for ethash caches dir=/Users/shanyao/.ethereum_private/geth/ethash count=3
INFO [02-13|16:09:20] Disk storage enabled for ethash DAGs dir=/Users/shanyao/.ethash count=2
INFO [02-13|16:09:20] Initialising Ethereum protocol versions="[63 62]" network=12345
INFO [02-13|16:09:20] Loaded most recent local header number=0 hash=76bc36…4efba1 td=256
INFO [02-13|16:09:20] Loaded most recent local full block number=0 hash=76bc36…4efba1 td=256
INFO [02-13|16:09:20] Loaded most recent local fast block number=0 hash=76bc36…4efba1 td=256
INFO [02-13|16:09:20] Regenerated local transaction journal transactions=0 accounts=0
INFO [02-13|16:09:20] Starting P2P networking
INFO [02-13|16:09:22] UDP listener up self=enode://42b0eec728cad819259d9685807bf78105450b6b28cd39d70417aa65ffebd3ac744b63d0446174bc657c9a42de2f0f9bc0f9589998c960177b844d5a962d8da9@[::]:30303
INFO [02-13|16:09:22] RLPx listener up self=enode://42b0eec728cad819259d9685807bf78105450b6b28cd39d70417aa65ffebd3ac744b63d0446174bc657c9a42de2f0f9bc0f9589998c960177b844d5a962d8da9@[::]:30303
INFO [02-13|16:09:22] IPC endpoint opened url=/Users/shanyao/Library/Ethereum/geth.ipc
Welcome to the Geth JavaScript console!
instance: Geth/v1.8.2-stable/darwin-amd64/go1.10
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
{
datadir: "/Users/shanyao/.ethereum_private",
nodeInfo: {
enode: "enode://42b0eec728cad819259d9685807bf78105450b6b28cd39d70417aa65ffebd3ac744b63d0446174bc657c9a42de2f0f9bc0f9589998c960177b844d5a962d8da9@[::]:30303",
id: "42b0eec728cad819259d9685807bf78105450b6b28cd39d70417aa65ffebd3ac744b63d0446174bc657c9a42de2f0f9bc0f9589998c960177b844d5a962d8da9",
ip: "::",
listenAddr: "[::]:30303",
name: "Geth/v1.8.2-stable/darwin-amd64/go1.10",
ports: {
discovery: 30303,
listener: 30303
},
protocols: {
eth: {
config: {...},
difficulty: 256,
genesis: "0x76bc36aeb54afba3cb6dc2a5d13818edabeb7f119f8bf61ee127854b624efba1",
head: "0x76bc36aeb54afba3cb6dc2a5d13818edabeb7f119f8bf61ee127854b624efba1",
network: 12345
}
}
},
peers: [],
addPeer: function(),
clearHistory: function(),
exportChain: function(),
getDatadir: function(callback),
getNodeInfo: function(callback),
getPeers: function(callback),
importChain: function(),
removePeer: function(),
sleep: function github.com/ethereum/go-ethereum/console.(*bridge).Sleep-fm(),
sleepBlocks: function github.com/ethereum/go-ethereum/console.(*bridge).SleepBlocks-fm(),
startRPC: function(),
startWS: function(),
stopRPC: function(),
stopWS: function()
}
查看自己的node url如下:
> admin.nodeInfo.enode
"enode://42b0eec728cad819259d9685807bf78105450b6b28cd39d70417aa65ffebd3ac744b63d0446174bc657c9a42de2f0f9bc0f9589998c960177b844d5a962d8da9@[::]:30303"
note:输入web3可以查看所有全部命令和结构;这些命令的前缀也均是web3,即输入admin与web3.admin等效。
简单用法
创建账户
> personal.newAccount("pk1")
"0xfb6565558c9fb3ef0eb657d5540bcd2859824e7f"
> personal.newAccount("pk2")
"0x9652670122f63ea19398f93d088ccb458b16cbf7"
这里的pk即是你的账户密码,必须牢记,一旦丢失将无法找回。
查看账户
> eth.accounts
["0xfb6565558c9fb3ef0eb657d5540bcd2859824e7f", "0x9652670122f63ea19398f93d088ccb458b16cbf7"]
> eth.accounts[0]
"0xfb6565558c9fb3ef0eb657d5540bcd2859824e7f"
其中最先创建的账户通常被称为primary account。
查看余额
> var primaryAccount = web3.eth.accounts[0]
undefined
> web3.eth.getBalance(primaryAccount)
0
显然,目前账户的余额为0,让我们开启矿工web3.miner.start(),挖一会儿挣点币:
> web3.miner.start()
INFO [02-13|16:29:24] Updated mining threads threads=0
INFO [02-13|16:29:24] Transaction pool price threshold updated price=18000000000
INFO [02-13|16:29:24] Starting mining operation
null
> INFO [02-13|16:29:24] Commit new mining work number=1 txs=0 uncles=0 elapsed=187.297µs
INFO [02-13|16:29:27] Successfully sealed new block number=1 hash=f59c04…83a194
INFO [02-13|16:29:27] ? mined potential block number=1 hash=f59c04…83a194
INFO [02-13|16:29:27] Commit new mining work number=2 txs=0 uncles=0 elapsed=113.91µs
INFO [02-13|16:29:27] Successfully sealed new block number=2 hash=09f545…b0230d
INFO [02-13|16:29:27] ? mined potential block number=2 hash=09f545…b0230d
过一会儿,另开窗口,并停止挖矿:
ali-186590cc4a7f-2:ethereum shanyao$ geth attach
Welcome to the Geth JavaScript console!
instance: Geth/v1.8.2-stable/darwin-amd64/go1.10
coinbase: 0xfb6565558c9fb3ef0eb657d5540bcd2859824e7f
at block: 99 (Wed, 13 Feb 2019 16:31:07 CST)
datadir: /Users/shanyao/.ethereum_private
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
> web3.miner.stop()
true
再次查看账户余额:
> web3.eth.getBalance(primaryAccount)
565000000000000000000
用命令行创建合约
创建一个简单的hello world合约
编写
代码如下:
pragma solidity >=0.4.22 <0.6.0;
contract Mortal {
/* Define variable owner of the type address */
address owner;
/* This constructor is executed at initialization and sets the owner of the contract */
constructor() public { owner = msg.sender; }
/* Function to recover the funds on the contract */
function kill() public { if (msg.sender == owner) selfdestruct(msg.sender); }
}
contract Greeter is Mortal {
/* Define variable greeting of the type string */
string greeting;
/* This runs when the contract is executed */
constructor(string memory _greeting) public {
greeting = _greeting;
}
/* Main function */
function greet() public view returns (string memory) {
return greeting;
}
}
其中有Greeter和Mortal两个合约,而Greeter继承自Mortal,除了具有Mortal的自杀函数kill外,新增了问候函数greet。需要说明的是,以太坊中的合约默认情况下是永恒不朽且无人掌控的,即一旦被部署到区块链上,即使是合约的作者也不再拥有特权来控制合约。也就是说,合约是由作者定义的,但合约的执行和其提供的服务,则由整个以太坊网络来支撑。只要整个网络仍然存在,合约就会一直存在并运行下去,除非其中写了自毁程序,并被触发,合约才会消失。
编译
目前最便捷的开发合约方法是用在线IDE REMIX:https://remix.ethereum.org/
粘贴进来即会自动编译,如下:
注意右侧编译结果Details中的ABI、ByteCode等关键信息。
其中的ABI(Application Binary Interface)如下:
[
{
"constant": false,
"inputs": [],
"name": "kill",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "greet",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"name": "_greeting",
"type": "string"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
}
]
部署
其中的WEB3DEPLOY是部署合约的js代码,如下:
var _greeting = "hello world!"; /* var of type string here */ ;
var greeterContract = web3.eth.contract([{"constant":false,"inputs":[],"name":"kill","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"greet","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_greeting","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]);
var greeter = greeterContract.new(
_greeting,
{
from: web3.eth.accounts[0],
data: '0x608060405234801561001057600080fd5b506040516103c73803806103c78339810180604052602081101561003357600080fd5b81019080805164010000000081111561004b57600080fd5b8281019050602081018481111561006157600080fd5b815185600182028301116401000000008211171561007e57600080fd5b5050929190505050336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555080600190805190602001906100dc9291906100e3565b5050610188565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061012457805160ff1916838001178555610152565b82800160010185558215610152579182015b82811115610151578251825591602001919060010190610136565b5b50905061015f9190610163565b5090565b61018591905b80821115610181576000816000905550600101610169565b5090565b90565b610230806101976000396000f3fe608060405260043610610046576000357c01000000000000000000000000000000000000000000000000000000009004806341c0e1b51461004b578063cfae321714610062575b600080fd5b34801561005757600080fd5b506100606100f2565b005b34801561006e57600080fd5b50610077610162565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100b757808201518184015260208101905061009c565b50505050905090810190601f1680156100e45780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610160573373ffffffffffffffffffffffffffffffffffffffff16ff5b565b606060018054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156101fa5780601f106101cf576101008083540402835291602001916101fa565b820191906000526020600020905b8154815290600101906020018083116101dd57829003601f168201915b505050505090509056fea165627a7a7230582083829c23804682d77517133b1aba64edd8cb8cc98a0f9cd40fc419024eb92ce00029',
gas: '4700000'
}, function (e, contract){
console.log(e, contract);
if (typeof contract.address !== 'undefined') {
console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
}
})
将该代码粘贴到geth windows中,提交给私有区块链网络。
在此之前需要先解锁账户,如下:
> personal.unlockAccount(web3.eth.accounts[0],"pk1")
true
否则会报错:Error: authentication needed: password or unlock undefined
部署合约结果如下:
INFO [02-13|17:15:47] Submitted contract creation fullhash=0x5cafc176adea94dafee6ae1a6daa8b1ece894666ccac85f7858b16af5e15cdbe contract=0x579756a30442b508e2F6A0a4b4D1F4d871f9B906
这说明合约创建交易已经提交,但此时区块链尚未挖矿,该交易尚未被打包到区块链中,整个区块链网络还无法看到。这里必须手动再次启动挖矿(主网上是永不停歇地持续挖矿的,这里私有测试网络仅有一个节点,便于清晰演示,每次需手动启动或停止挖矿),几秒钟即能看到:
Contract mined! address: 0x579756a30442b508e2f6a0a4b4d1f4d871f9b906 transactionHash: 0x5cafc176adea94dafee6ae1a6daa8b1ece894666ccac85f7858b16af5e15cdbe
这说明该合约已经成功部署到了区块链网络。
可以通过eth.getCode(greeter.address)查看确认:
> eth.getCode(greeter.address)
"0x608060405260043610610046576000357c01000000000000000000000000000000000000000000000000000000009004806341c0e1b51461004b578063cfae321714610062575b600080fd5b34801561005757600080fd5b506100606100f2565b005b34801561006e57600080fd5b50610077610162565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100b757808201518184015260208101905061009c565b50505050905090810190601f1680156100e45780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610160573373ffffffffffffffffffffffffffffffffffffffff16ff5b565b606060018054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156101fa5780601f106101cf576101008083540402835291602001916101fa565b820191906000526020600020905b8154815290600101906020018083116101dd57829003601f168201915b505050505090509056fea165627a7a7230582083829c23804682d77517133b1aba64edd8cb8cc98a0f9cd40fc419024eb92ce00029"
此时,该合约已经可以执行了。
调用
> greeter.greet();
"hello world!"
该调用过程并未改变区块链状态,因此立即返回且并未消耗gas。
清理
合约代码将永久保存在区块链上,除非被自我毁灭。因此废弃的合约需要执行摧毁过程,这需要往区块链发送交易,且付费来执行。如下:
> personal.unlockAccount(web3.eth.accounts[0],"pk1")
true
> greeter.kill.sendTransaction({from:eth.accounts[0]})
INFO [02-13|19:43:17] Submitted transaction fullhash=0xd66e3f444948f6bc7dfbfda1fb680c29c8173b061212d2457ef5553c71e8600c recipient=0x579756a30442b508e2F6A0a4b4D1F4d871f9B906
"0xd66e3f444948f6bc7dfbfda1fb680c29c8173b061212d2457ef5553c71e8600c"
再次查看合约代码,如下:
> eth.getCode(greeter.address)
"0x"
可见合约代码已清理完成。
总结
至此,你已经简单掌握了以太坊合约开发的大致过程。本文基本上参考了官方文档,做了部分补充和删减,仅仅只是做了入门性的介绍,主要目的是让大家了解以太坊合约的大致流程,有一个基本的认识。更深入的教程请参考以太坊官方文档。
参考
Building a smart contract using the command line:https://www.ethereum.org/greeter
Command line tools for the Ethereum Network:https://www.ethereum.org/cli
ETH docs (contracts):http://ethdocs.org/en/latest/contracts-and-transactions/contracts.html
Solidity docs: https://solidity.readthedocs.io/en/latest/