2009年,比特币诞生,距离现在已经过去10年了。区块链技术也从一开始的不为人知,到如今的名扬天下。使用区块链技术,可以制造无须任何国家背书的金融产品,让区块链技术为世人所认知。作为继承和改造的产品以太坊,它推出的全球化编程技术又极大的推动了区块链技术的发展。
以太坊作为改进者,它增加了智能合约的功能,全球爱好者都可以编写自己的智能合约,在全球的以太坊网络中运行,它有一个美好的梦想就是让全球都实行契约化。那么到底什么是智能合约呢?
什么是智能合约?
具体什么是智能合约,我们还是先看一下以太坊智能合约的英文:smart contract,可以分为两部分,智能于合约。对于合约就很好理解了,合约就是合同,事先约定好的规则以及事情发生后双方要付出和接受的代价等。智能的含义就是这个合约(合同)是可以自动被触发和执行的,不用担心违约和赖账的问题,任何内容都写在合同中,一目了然。这样带来的好处就是双方即使没有彼此建立信任也可以完成交易。
智能合约该如何编写呢?
开发以太坊智能合约门槛并不高,目前互联网也有大量的学习资源,只不过自己筛选需要一些过程。那么如开发智能合约呢?从目前的区块链技术发展来看,可以用solidity,python,golang,c++,c#等语言开发智能合约,但从最初的智能合约产生来说,solidity还是更原汁原味一些,solidity其实就是一门语言,把它当成一门新的语言学习就好了。学习一门新的语言的过程无外乎是下面这样的步骤:
- 环境安装,运行环境,IDE等
- 基础语法
- 小案例学习
- 项目实战
对于solidity的学习也会包含这些内容,但毕竟区块链是特殊的技术,智能合约的学习也会比其他内容多一些特殊性,比如你要了解区块链的底层原理,你要了解智能合约的运行原理,还有由于智能合约离钱太近了,你还要学习如何编写安全的智能合约,最后就是要吃透一些智能合约讨论出来的标准,比如毁誉参半的ERC20,ERC721等等。
以太坊开发环境安装
智能合约编辑环境 -- remix
remix其实就是一个集成在网页中的在线编辑环境,不过对网络有一定的要求,相对来说更新的更及时一些,如果在线环境不能使用,可以使用本地环境安装,在后面的geth安装示例中有介绍,不过推荐使用在线环境,本地会经常有问题。 remix在线环境:http://remix.ethereum.org
以太坊客户端安装 -- geth
在这里主要就是geth的安装,针对不同平台都可以安装,推荐用类unix系统。具体安装可以参考
也有一些人比较喜欢使用ganache作为以太坊客户端的学习工具,相对来说图形化比命令行好接受一些。
geth 私链搭建
geth是以太坊提供的客户端软件,用它你可以加入到以太坊主网中,也可以自己搭建私链,对于学习者来说,一般都是自己搭建私链。
- 创世块配置- genesis.json
{
"config": {
"chainId": 18,
"homesteadBlock": 0,
"eip155Block": 0,
"eip158Block": 0
},
"alloc" : {},
"coinbase" : "0x0000000000000000000000000000000000000000",
"difficulty" : "0x2",
"extraData" : "",
"gasLimit" : "0xffffffff",
"nonce" : "0x0000000000000042",
"mixhash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
"parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
"timestamp" : "0x00"
}
-
创世块参数
- chainId 私有网络ID
- difficulty 挖矿难度,越低挖矿速度越快
- coinbase 默认的挖矿账户,如果不设定,则使用默认第一个账户
- gasLimit gas消耗限制,如果过小可能会影响合约发布和运行,建议设为:0xffffffff
-
初始化
geth init genesis.json --datadir ./data
如果是首次启动,或者genesi.json文件发生变,那么必须初始化,data 是存放数据的目录,可以不存在,建议找一个独立目录。
- 启动
geth --datadir ./data --networkid 18 --port 30303 --rpc --rpcport 8545 --rpcapi 'db,net,eth,web3,personal' --rpccorsdomain '*' --gasprice 0 console 2> 1.log
- 参数介绍
- --datadir 指定数据路径
- --networkid 指定私链ID,与配置文件chainId一样
- --port p2p端口,节点之间通信采用的端口
- --rpc 所有rpc相关都是代表远程调用的参数设计,可以设定ip,端口(此端口为对外提供http服务端口),api等
- --gasprice 设定gas的价格,如果gas为0则不消耗以太币
简易教程
可以下载本人github工程,直接用脚本启动即可,三步就够了
$git clone https://github.com/yekai1003/rungeth
$cd rungeth
$geth init genesis.json --datadir ./data
$./rungeth.sh
geth启动后,我们会进入到一个管理台当中,在其中可以创建账户,解锁账户,挖矿,查询余额等等操作。
geth命令行客户端操作
geth启动后,进入管理台,会看到如下的信息:
ykdeMac-mini:eth yekai$ ./rungeth.sh
localhost:rungeth yekai$ ./rungeth.sh
Welcome to the Geth JavaScript console!
instance: Geth/v1.9.6-stable/darwin-amd64/go1.13.1
at block: 0 (Thu, 01 Jan 1970 08:00:00 CST)
datadir: /Users/yekai/eth/rungeth/data
modules: admin:1.0 debug:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0
>
- 创建账户
注意123是密码,返回的字符串才是以太坊的账户地址
> eth.accounts
[]
> personal.newAccount("123")
"0xdc78ec60eba4eb3bf8a497d94223615d43352c7e"
> eth.accounts
["0xdc78ec60eba4eb3bf8a497d94223615d43352c7e"]
eth.accounts 是查看当前有哪些账户。
- 余额查看
> acc0=eth.accounts[0]
"0xdc78ec60eba4eb3bf8a497d94223615d43352c7e"
> eth.getBalance(acc0)
0
- 挖矿
一开始账户肯定没有钱,需要挖一挖,才能有钱。
> miner.start(1)
null
> eth.getBalance(acc0)
0
> eth.getBalance(acc0)
0
> eth.getBalance(acc0)
0
> eth.getBalance(acc0)
0
> eth.getBalance(acc0)
20000000000000000000
> miner.stop()
null
miner.start(1) 启动挖矿,其中的1可以不写,代表的是实际挖矿的线程数量。
- 解锁账户
账户有钱后就可以挥霍了,但是在remix环境使用之时,必须先要解锁。
> personal.unlockAccount(acc0,"123")
true
如果解锁命令失败,则需要在geth启动的时候添加参数,这也是以太坊新版客户端的一个更新。
--allow-insecure-unlock
智能合约开发
先来看一个简单的例子,编写一个最简单的智能合约:
pragma solidity^0.5.11;
contract Person {
string public name ;
function setName(string memory _name) public {
name = _name;
}
}
第一句是交代编译器版本,要使用大于等于0.5.11的编译器对本合约进行编译,编译器版本不能超过0.5.的范围。
- contract Person是定义合约的名字
- setName是一个公共函数,任何人可以调用,目的是修改内部成员name
- name与_name是内部成员和形参的区别,在以太坊中却非常重要,内部成员name是要存储在全球计算机上,需要消耗gas,_name是临时存储,不会消耗gas,当然任何指令的执行都需要消耗gas。
在remix部署可以看到如下效果:
image我们看到一个黄颜色的setName函数和一个蓝颜色的name函数,黄颜色的每次调用都要收取gas,蓝颜色的则不需要提供gas。可能也有人奇怪name函数是怎么出来的,只要我们把变量声明为public,它就会自动生成。
我们可以先调用一下name看看效果,结果什么都没有。
image我们用setName来修改一下名字:
image之后我们再来点击name,就可以看到效果了。
image以上就是一个最简单的智能合约运行,怎么样,感觉操作复杂吗?
合约案例:每人一个去中心化ID
pragma solidity^0.5.11;
pragma experimental ABIEncoderV2; //支持结构体变量
contract PersonDID {
address owner;
struct Person {
bytes32 DID;
string name;
string sex;
uint256 age;
bool isExists;
}
//内部一个自动增长的ID
uint256 autoID;
//地址==>人,每个地址一个人
mapping(address=>Person) persons;
//构造函数,constructor是关键字
constructor() public {
autoID = 1;
owner = msg.sender;
}
//将个人与ID绑定
function bindID(string memory _name, string memory _sex, uint256 _age) public {
//验证该人员是否已经绑定过了
require(!persons[msg.sender].isExists,"caller must not exists");
//assert(!persons[msg.sender].isExists,"caller must not exists");
//利用hash函数计算一个DID的值
bytes32 DID = keccak256(abi.encode(now, autoID));
Person memory p = Person(DID, _name, _sex, _age, true);
persons[msg.sender] = p;
autoID ++;
}
//验证该DID是否属于调用者
function verifyPeron(bytes32 _DID) public view returns (Person memory) {
Person storage p = persons[msg.sender];
require(_DID == p.DID );//调用者的DID与传入的DID相等
return p;
}
//获取DID
function getDID() public view returns (bytes32) {
return persons[msg.sender].DID;
}
}
智能合约如何调用
首先要明确,智能合约是运行在以太坊上的智能合同,所以要调用智能合约必然要和以太坊打交道。至于调用的方式,可以采用两种:rpc和ipc。
- ipc 本地套接字调用
- rpc 远程调用
我们都知道以太坊是存储数据的,当然就很容易联想到数据库也是存储数据的,数据库对外提供了多种语言的访问机制,以太坊同样如此。对于以太坊智能合约以及相关api的调用,可以使用java,js,python,golang等多种语言,只要相应研究对应的sdk即可。
接下来我们主要介绍Go语言如何调用智能合约。
首先需要下载go版本的以太坊源码
$ cd $GOPATH/src
$ mkdir -p github.com/ethereum
$ cd github.com/ethereum/
$ git clone https://github.com/ethereum/go-ethereum.git
引用源码的rpc库
import "github.com/ethereum/go-ethereum/rpc"
创建一个账户
func NewAcct(pass string) {
//获得与geth的连接
cli, err := rpc.Dial("http://localhost:8545")
if err != nil {
log.Fatal("connet to geth error:", err)
}
//函数结束自动关闭连接
defer cli.Close()
var account string
//调用call方法,call可以调用personal.newAccount
err = cli.Call(&account, "personal_newAccount", pass)
if err != nil {
log.Fatal("call personal_newAccount error:", err)
}
fmt.Println("account=", account)
}
Go语言调用合约的步骤
image将源码文件获得abi,之后用abi编译成对应的Go代码,然后其实也就没有然后了,Go代码都给你了,调用就可以了,不过有些细节我们还是要注意的。
如果使用命令行安装的方式abigen程序会伴随安装,如果没有则需要借助源码安装的方式,下面的步骤可以作为参考。
yekaideMBP:~ yk$ cd $GOPATH/src/github.com/ethereum/go-ethereum/cmd/abigen/
yekaideMBP:abigen yk$ ls
main.go
yekaideMBP:abigen yk$ pwd
/Users/yk/src/gowork/src/github.com/ethereum/go-ethereum/cmd/abigen
yekaideMBP:abigen yk$ go build -i
yekaideMBP:abigen yk$ ls -l
total 20904
-rwxr-xr-x 1 yk staff 10694032 7 29 00:02 abigen
-rw-r--r-- 1 yk staff 5006 7 21 20:45 main.go
使用abigen将abi翻译成go(最好先将abigen拷贝到$PATH的某个路径下)
abigen --abi xx.abi --pkg pkgname --type apiname --out xx.go
abigen参数说明:
- abi 文件在 remix 部署时可以得到
- Pkg 指定的是编译成的 go 文件对应的 package 名称
- type指定的是go文件的入口函数,可以认为是类名
- out 指定输出go文件名称
接下来,我们我们之前编写的合约代码来转换为Go代码,然后加以调用。首先先要得到abi文件,这个可以在remix环境直接拷贝得到,其实如果安装了solc编译器,在本地编译也可,只不过需要随时关注版本更新的情况,不如在线方便。具体拷贝位置,如下图所示:
image将abi文件拷贝到我们的代码目录,然后保存为person.abi文件,执行下面的命令:
abigen -abi person.abi -out person.go -type person -pkg main
这样就可以得到person.go的Go源码文件,<mark style="box-sizing: border-box;">注意此文件为自动生成,不要修改!</mark>
接下来就是调用的问题了,该文件会给我们提供一个入口函数,叫NewPerson。
// NewPerson creates a new instance of Person, bound to a specific deployed contract.
func NewPerson(address common.Address, backend bind.ContractBackend) (*Person, error) {
contract, err := bindPerson(address, backend, backend, backend)
if err != nil {
return nil, err
}
return &Person{PersonCaller: PersonCaller{contract: contract}, PersonTransactor: PersonTransactor{contract: contract}, PersonFilterer: PersonFilterer{contract: contract}}, nil
}
这个函数的返回结果就是我们合约实例对象,通过合约实例对象,就可以调用合约内的方法了,关键是传递参数。
身份问题
因为合约函数在调用的时候需要确认身份,接下来,我们介绍如何签名,这个需要对钱包技术有一些了解,简单来说就是借助keystore+密码一起来构建身份。
//设置签名
func MakeAuth(addr, pass string) (*bind.TransactOpts, error) {
keystorePath := "/Users/yekai/eth/rungeth/data/keystore"
fileName, err := GetFileName(string([]rune(addr)[2:]), keystorePath)
if err != nil {
fmt.Println("failed to GetFileName", err)
return nil, err
}
file, err := os.Open(keystorePath + "/" + fileName)
if err != nil {
fmt.Println("failed to open file ", err)
return nil, err
}
auth, err := bind.NewTransactor(file, pass)
if err != nil {
fmt.Println("failed to NewTransactor ", err)
return nil, err
}
auth.GasLimit = 300000
return auth, err
}
核心思路就是找到geth运行后产生的keystore文件,读取后用密码进行解析得到私钥,此后就可以创作成数字身份,进行签名交易。
签名搞定后,就可以考虑调用合约函数的问题了。步骤如下:
- 连接到geth节点
- 通过合约地址构造合约实例
- 设置数字身份
- 通过身份,合约实例调用合约具体函数,注意各个参数传递
代码如下:
//1\. 连接到节点
cli, err := ethclient.Dial("http://localhost:8545")
if err != nil {
fmt.Println("Failed to Dial ", err)
return
}
//2\. 构造合约实例,注意传入合约地址
ins, err := NewPerson(common.HexToAddress("0x5ff2D46A84c4ABB525beF98DFd71C1Dc1060Fb8c"), cli)
if err != nil {
fmt.Println("Failed to NewPerson", err)
}
//3\. 用账户地址构建签名,传入密码
auth, err := MakeAuth("0xbd72018032e5f0b1a19943f5e7d6225e0ab99fbd", "123")
if err != nil {
fmt.Println("failed to MakeAuth")
return
}
//4\. 调用合约函数
tx, err := ins.BindID(auth, "wangyuyan", "woman", big.NewInt(35))
if err != nil {
fmt.Println("Failed to BindID", err)
return
}
//打印交易hash结果,注意此交易需要矿工确认后才会生效,所以不会立即在网络中体现
fmt.Println(tx.Hash().Hex())
如果不需要消耗gas,但还需要账户的情况下,这个时候就需要构造CallOpts
type CallOpts struct {
Pending bool
From common.Address
BlockNumber *big.Int
Context context.Context
}
此时不需要密码和keystore文件了,这样构造即可。
opt := &bind.CallOpts{
false,
common.HexToAddress("0xbd72018032e5f0b1a19943f5e7d6225e0ab99fbd"),
nil,
context.Background(),
}
之后仍然是调用合约函数,示例如下:
//4\. 调用合约函数
hash, err := ins.GetDID(opt)
if err != nil {
fmt.Println("Failed to GetDID", err)
return
}
data, err := ins.VerifyPeron(opt, hash)
fmt.Printf("%s , %s, %s, %d\n", common.ToHex(data.DID[:]), data.Name, data.Sex, data.Age)
总结一下,本文主要介绍了以太坊节点的搭建,命令行操作账户,包括创建,解锁,挖矿等等,之后又介绍了智能合约如何编写,如何部署,如何测试,以及如何通过Go语言调用智能合约,如何构造签名等知识点,希望对学习者有所帮助。
吐槽一下:以太坊升级对于开发者影响还是挺大的,比如这次升级就让编写本文的我有些无力吐槽,首先remix环境出现一些问题,调用智能合约会失败,当然Go语言调用时也出现了问题,好在经过摸索,发现变化并不大,还好还好!
网友评论