以太坊智能合约开发入门

简介

以太坊合约就是以太坊区块链特定账户地址上的一串代码(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

其中的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/

 
上一篇:Git常用命令


下一篇:当双11回归常态,数字化成为源氏木语另一赛场