文章目录
综述一、环境准备二、搭建私链三、连接私链与MetaMask四、Remix部署合约五、几个重要的地址六、Golang部署合约七、调用合约
综述
智能合约调用是实现一个 DApp 的关键,一个完整的 DApp 包括前端、后端、智能合约及区块 链系统,智能合约的调用是连接区块链与前后端的关键。
智能合约的运行过程是后端服务连接某节点,将 智能合约的调用(交易)发送给节点,节点在验证了交易的合法性后进行全网广播,被矿工打包到 区块中代表此交易得到确认,至此交易才算完成。
就像数据库一样,每个区块链平台都会提供主流 开发语言的 SDK(Software Development Kit,软件开发工具包),由于 Geth 本身就是用 Go 语言 编写的,因此若想使用 Go 语言连接节点、发交易,直接在工程内导入 go-ethereum(Geth 源码) 包就可以了,剩下的问题就是流程和 API 的事情了。智能合约被调用的两个关键点是节点和 SDK。
本demo基于Ubuntu18.04 OS,golang_v1.17.5,geth_v1.10.13-stable搭建本地私链,基于Chrome Remix和solidity_v0.4.17开发智能合约代码,通过Metamask将Remix与本地私链进行连接,并将Lottery智能合约部署到本地私链,使用geth客户端新建账户,与智能合约进行交互。
一、环境准备
-
在VMWare上新建Ubuntu18.04虚拟机,虚拟机网络与本地进行桥接并同步网卡配置,使得主机的MetaMask能够同步虚拟机中的私链,主机网卡设置同步VMware Network Adapter VMnet1。 -
在Chrome上安装以太坊钱包MetaMask,新建自己的账户,在MetaMask上新建网络,网络名设置为private-chain,网络地址设置为虚拟机ip地址,端口为8545,链ID设置为1330。
RPC URL请确保和虚拟机的ip地址保持一直,端口默认使用8545,链ID与后文中genesis.json中的配置保持一致。使用MetaMask可以访问公网和测试网,几个测试网中Ropsten比较好用,每次发一个币,但是最近(2021/12/15)由于node4j出bug,Ropsten发笔机直接down掉,其它网络发币非常少,例如Koven每次只发0.0002个ether,Rinkerby发币审核机制非常麻烦,为了避免这些情况,自己在本地搭私链开发时最高效的,测试币随便发。
- 使用如下命令安装geth客户端:
sudo apt-get install software-properties-common
sudo add-apt-repository -y ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install ethereum
安装成功后使用 geth version 查看geth是否成功安装,本环境下的输出为:
Geth
Version: 1.10.13-stable
Git Commit: 7a0c19f813e285516f4b525305fd73b625d2dec8
Architecture: amd64
Go Version: go1.17.2
Operating System: linux
GOPATH=
GOROOT=go
- 使用如下命令安装golang环境:
https://go.dev/dl/go1.17.5.linux-amd64.tar.gz
sudo tar -C /usr/local/go -zxvf go1.17.5.linux-amd64.tar.gz
export GOROOT=/usr/local/go
export GOPATH=/home/ub/go
安装成功后使用 go version 查看geth是否成功安装,本环境下的输出为:
go version go1.17.5 linux/amd64
重要:GO的版本一定要选择v1.17.5以上,后面使用abigen 编译代码时需要用到。
-
安装Xshell7和Xftp7连接虚拟机中的Ubuntu18.04,方便在主机中使用软件进行开发。// 补充一下,如果直接选择在Ubuntu中进行开发,你会面临桌面上全是控制台的情况,难以分清哪个是做什么的,而使用Xshell会获得更好的交互性能,只需在Ubuntu shell中使用 ip a 获取ip地址,在xshell中直接连接即可,开发非常好用,也适合开多个虚拟机。 -
科学上网软件,虚拟机和主机中分别进行安装。有时候网络连接会报错,例如tcp端口被占用、网络无法连接,此时可以尝试禁用虚拟机防火墙,kill 占用tcp端口的进程,或者重启虚拟机。如果重启虚拟机也不行,就关闭主机,等待几分钟,内存全部清空后再开机重来。
二、搭建私链
在 ~/ 下使用 mkdir private-chain 新建私链文件夹,在私链文件夹中,使用vim genensis.json 新建创世区块配置文件,内容如下:
{
"config": {
"chainId": 1330,
"homesteadBlock": 0,
"eip150Block": 0,
"eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0,
"istanbulBlock": 0,
"ethash": {}
},
"nonce": "0x0",
"timestamp": "0x5ddf8f3e",
"extraData": "0x0000000000000000000000000000000000000000000000000000000000000000",
"gasLimit": "0x47b760",
"difficulty": "0x00002",
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x0000000000000000000000000000000000000000",
"alloc": { },
"number": "0x0",
"gasUsed": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000"
}
各个参数的解析如下: mixhash : 一个256位的哈希证明,与nonce相结合,已经对该块进行了足够的计算:工作量证明(PoW)。 nonce和mixhash的组合必须满足黄皮书4.3.4中描述的数学条件,它允许验证块确实已经加密地挖掘。
nonce:证明64位散列与混合散列相结合,在该块上进行了足够的计算:工作量证明(PoW)。 nonce和mixhash的组合必须满足黄皮书4.3.4中描述的数学条件,并允许验证块确实已经加密地挖掘。nonce是加密安全的挖掘工作证明,证明在确定该令牌值时已经花费了特定量的计算。 (Yellowpager,11.5。采矿工作证明)。
difficulty:标量值,对应于在该块的随机数发现期间应用的难度级别。它定义了挖掘目标,可以根据前一个块的难度级别和时间戳来计算。难度越高,Miner必须执行的统计更多计算才能发现有效块。此值用于控制区块链的块生成时间,将块生成频率保持在目标范围内。在测试网络上,我们将此值保持为低以避免在测试期间等待,因为在区块链上执行事务需要发现有效的块。
alloc:允许定义预先填充的钱包列表。这是以太坊特定功能,可以处理“以太网预售”时期。
coinbase:从该块的成功挖掘中收集的所有奖励(以太币)的160位地址已被转移。它们是采矿奖励本身和合同交易执行退款的总和,在创建新Block时,Miner的设置会设置该值。
timestamp:标量值等于此块开始时Unix time()函数的合理输出。该机制在块之间的时间方面强制实施稳态。最后两个块之间的较小周期导致难度级别的增加,从而导致找到下一个有效块所需的额外计算。如果周期太大,则减少了难度和到下一个块的预期时间。时间戳还允许验证链内的块顺序(黄皮书,4.3.4。(43))。简单地说,timestamp就是该私链启动时的时间。
parentHash:整个父块头的Keccak 256位哈希(包括其nonce和mixhash)。指向父块的指针,从而有效地构建块链。在Genesis块的情况下它为0。
extraData:可选,但最多32字节长的空间。
gasLimit:可选,为标量值,它等于每个gas支出的限制。gas通常需要设置得很高,以避免在测试期间受到此阈值的限制,但这并不表示我们不应该关注智能合约的gas消耗量。通常来说,过低的gas可能导致交易失败,过高的gas容易导致交易的可信度降低,一个合理的gas才能提高交易的效率。
使用命令 geth --datadir ./ init ./genesis.json 使用创世区块初始化私链配置,看到如下输出,说明私链搭建成功:
三、连接私链与MetaMask
使用命令:
geth --datadir ./ --networkid 1330 --http --http.addr [HTTP_ADDRESS] --http.vhosts "*" --http.port 8545 --http.api 'db,net,eth,web3,personal' --nodiscover --allow-insecure-unlock --http.corsdomain "*" console 2>>geth.log
运行本地私链,注意在之前版本中geth需要使用--rpc 连接本地网络,在新版本中则采用了--http ,在之后版本中各个参数的写法也很可能会更新,具体参考geth官方文档:
其中每个参数的作用为:
–datadir:geth当前的工作目录
–networkid:网络id,最好与chainID保持一致
–http:开启远程调用模式,相当于之前版本的 --rpc ,即使MetaMask能够与其建立连接
–http.addr:配置网络ip地址,例如 http://128.120.0.3
–http.port:定义ip端口,与MetaMask保持一致,默认为8545
–http.api:启用远程调用api,尽量多启用几个
–nodiscover:不发现本地结点,如果不设置这个就会在log里面一直looking for peers
–allow-insecure-unlock:允许解锁账户,这样才能交易
–http.corsdomain:定义网段上的哪些主机能发现
根据上述命令,geth日志更新到geth.log中,新建Shell,打开工作目录,使用命令tail -f geth.log 实时在控制台中跟踪日志。
私链按上述操作配置好后,在MetaMask中切换到本地网络private-chain,将MetaMask与本地私链进行连接。在Chrome中打开Remix编译器英文版(中文版有bug)。
如果此时MetaMask与Remix没有连接,手动选择 已连接的网站->手动连接到当前站点,此时本地私链、MetaMask、Remix已经连接到了同一网段中。
当私链运行起来后,如下是一些常用的命令:
eth.accounts
personal.newAccount()
eth.blockNumber
miner.start()
miner.stop()
eth.getBalance(eth.accounts[0])
web3.fromWei(eth.getBalance(eth.accounts[0]), "ether")
eth.getBlock(0)
personal.unlockAccount(eth.accounts[0])
eth.sendTransaction({from:eth.accounts[0], to:"ACCOUNT_ADDR",value:web3.toWei(3, "ether")})
eth.sendTransaction({from:eth.accounts[0], to:eth.accounts[1],value:web3.toWei(4, "ether")})
eth.getTransaction("")
eth.getCode("")
新建账户,miner.start(20) 开启20个线程进行挖矿,当新的区块被确认后,交易队列、合约部署等操作才能被执行。从accounts[0]中转10个ether到MetaMask中,用于合约部署和交易。核对矿工账号是否为当前账户:
> eth.coinbase == eth.accounts[0]
true
(注:如果线程开的太多,例如开400个,geth客户端将会非常卡,如果开的太少,例如1个,则挖矿的速度会很慢。经过测试,开20个可以保证挖矿速度的同时提高客户端的流畅性)
然后,为在geth中使用go部署合约,需要将MetaMask账户导入到私链中。在MetaMask界面获取私钥后,在~/private-chain/ 下使用命令vim metamask-sk 将私钥写入,然后使用如下命令将MetaMask账户导入本地私链:
geth account import ./metamask-sk
输入密码,观察到下列输出后导入成功:
获取私钥的方法为:
然后使用命令geth account list 查看导入的账户私钥存储位置:
使用如下命令将该文件转移到路径~/home/ub/private-key/keystore 下,待后面部署合约使用(路径根据自己的环境进行修改):
cp -r /home/ub/.ethereum/keystore/UTC--2021-12-14T14-52-22.064443917Z--68e9f0c38e31b5d4d25abefee28938ac263205a5 ./keystore/UTC--2021-12-14T14-52-22.064443917Z--68e9f0c38e31b5d4d25abefee28938ac263205a5
有时候,在MetaMask上进行交易会报错,可能是因为内部ID出了问题,需要重设一下账户,方法如下:
如果还不成功,尝试重启geth或重启虚拟机。
四、Remix部署合约
打开Remix,在contracts目录下新建Lottery.sol源文件,输入以下代码:
pragma solidity ^0.4.17;
contract Lottery {
address public manager;
address[] public players;
function Lottery() public {
manager = msg.sender;
}
function enter() public payable {
require(msg.value >= 0.0000000001 ether);
players.push(msg.sender);
}
function random() public view returns (uint) {
return uint(keccak256(block.difficulty, now, players.length));
}
function pickWinner() public restricted {
uint index = random() % players.length;
players[index].transfer(this.balance);
players = new address[](0);
}
modifier restricted() {
require(msg.sender == manager);
_;
}
function getPlayers() public view returns (address[]) {
return players;
}
}
选择对应的solidity版本进行编译,然后发布到本地私链中,选择以下按钮部署智能合约:
由于部署合约需要一定的gas,因此需要确保当前账户下拥有足够的ether。等待下一个区块被矿工确认后,部署即可完成:
五、几个重要的地址
- MetaMask钱包地址,即主账户地址,它是连接geth客户端、MetaMask与Remix的枢纽:
-
矿工地址:它是geth客户端中进行挖矿、转账的地址,从其中挖矿并转账到MetaMask用于合约部署和发布,整个私链的以太币都由矿工挖矿而来,默认为eth.accounts[0] -
合约地址:发布只能合约后,合约拥有本身的地址,任何调用该合约的方法本质上都是与合约地址进行交互,查看合约地址的方法为: -
用户地址:当智能合约部署到私链中后,可以在其中新建账户,使用账户与合约进行交互,每个账户地址都保存在eth.accounts中。
六、Golang部署合约
①参考geth官方文档,首先使用go version 检查环境下的go版本是否至少为以下版本,必须保证版本正确:
go version go1.17.5 linux/amd64
同时设置 go 的环境变量:
go env -w GOBIN=/Users/youdi/go/bin
go env -w GO111MODULE=on
如果遇到 go mod 报错提示,在工作目录下使用下列命令:
go mod init [xxx]
②在Remix中获得合约的ByteCode,复制在临时文件中,提取其中的object属性,然后在~/private-chain 下新建文件:vim Lottery.bin ,将object属性复制进去:
③在Remix中获得合约的ABI,然后在~/private-chain 下新建文件:vim Lottery.abi ,将该属性属性复制进去:
④使用命令:
abigen --abi Lottery.abi --pkg main --type Lottery --out Lottery.go --bin Lottery.bin
这将为 Lottery 合约生成一个类型安全的 Go 绑定,生成的 Lottery.go 中文件保存着合约绑定与部署的所有方法,新建一个 deploy.go 来调用其中的 API 进行合约部署,deploy.go中填写如下内容:
package main
import (
"fmt"
"log"
"math/big"
"strings"
"time"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/ethclient"
)
const key = "{\"address\":\"68e9f0c38e31b5d4d25abefee28938ac263205a5\",\"crypto\":{\"cipher\":\"aes-128-ctr\",\"ciphertext\":\"ffe83f793f03e5eb3d49abb5fc838ff65884a9c34a05b897f3032069694623a6\",\"cipherparams\":{\"iv\":\"710df5b6c32c8102e2bd983f7c46384c\"},\"kdf\":\"scrypt\",\"kdfparams\":{\"dklen\":32,\"n\":262144,\"p\":1,\"r\":8,\"salt\":\"92dc432c302bd34de65374e00d295d276531126f1e887c4a7e00d65359468340\"},\"mac\":\"ecda1dcc570f155c0fa0edd1d81c538469e0dbc557ad82add757cb8f23c07baf\"},\"id\":\"b575f386-fecd-449f-a673-1d62fbd4a386\",\"version\":3}"
func main() {
conn, err := ethclient.Dial("http://101.76.247.184:8545")
if err != nil {
log.Fatalf("Failed to connect to the Ethereum client: %v", err)
}
auth, err := bind.NewTransactorWithChainID(strings.NewReader(key), "123", big.NewInt(1330))
if err != nil {
log.Fatalf("Failed to create authorized transactor: %v", err)
}
address, tx, _, err := DeployLottery(auth, conn)
if err != nil {
log.Fatalf("Failed to deploy new Lottery contract: %v", err)
}
fmt.Printf("Contract pending deploy: 0x%x\n", address)
fmt.Printf("Transaction waiting to be mined: 0x%x\n\n", tx.Hash())
time.Sleep(250 * time.Millisecond)
}
其中,下列几个地方需要自行配置:
- const key 此属性为合约部署的账户,填写账户信息的.json配置文件,在当前目录下的 keystore/ 文件夹中,打开其中与MetaMask绑定的账户,复制其中的所有信息,打开 json文件浏览器,复制到其中并选择
删除空格并转义 ,将转义到的字符串复制到 key 变量中;
- conn, err := ethclient.Dial(“http://101.76.247.184:8545”) 函数的参数应该是本地私链的ip地址,可以从MetaMask中查看,也可以在启动 geth 的参数中进行设置:
-
auth, err := bind.NewTransactorWithChainID(strings.NewReader(key), "123", big.NewInt(1330))
该函数中的第二个元素为私链账户的密码,第三个元素为chainID。注意到geth官方文档中仍然使用了下列的错误写法:
使用此API会报错,翻看 go-ethereum 源代码可以发现,该接口早在2020年底就进行了舍弃,新版的函数接口应该是更新的 NewTransactorWithChainID
新的接口中增加了chainID参数,进一步保证了合约部署的安全性。此时,文件夹下的目录结构为:
完成上述修改后,在目录 ~/private-chain 下运行
go run *.go
编译所有代码,控制台中有以下输出:
Contract pending deploy: 0x2566a7db5d30634e20b77f556266de324239c250
Transaction waiting to be mined: 0xc991870c2a4779c0b42571bc3a28c214298fd8112637f351248e71ba52371ff8
表明合约以部署到本第私链中,合约地址为 0x2566a7db5d30634e20b77f556266de324239c250,已加入到交易队列中,等待当下一个块被矿工挖出来后,合约将被确认。
⑤回到运行私链的控制台中,输入以下命令测试合约是否部署成功:
eth.getCode("0x2566a7db5d30634e20b77f556266de324239c250")
若观察到以下输出说明部署成功:
至此,智能合约已使用 golang 部署到了本地私链中,合约地址为 MetaMask 绑定的地址。在此期间,可能会遇到 golang 版本不兼容或者是开发包不全的情况,需要对不全的包逐一使用 git clone 下载到本地。
七、调用合约
智能合约ABI介绍: ABI (Application Binary Interface) 应用程序二进制接口,如果理解 API 就很容易了解 ABI。简单来说,API 是程序与程序间互动的接口。这个接口包含程序提供外界存取所需的 functions、variables 等。ABI 也是程序间互动的接口,但程序是被编译后的 binary code。所以同样的接口,但传递的是 binary 格式的信息。所以 ABI 就要描述如何 decode/encode 程序间传递的 binary 信息。下图以 Linux 为例,描述 Linux 中 API、ABI 和程序的关系:
在 Ethereum 智能合约可以被大家使用前,必须先被部署到区块链上。
从智能合约的代码到使用智能合约,大概包含几个步骤:
1.编写智能合约的代码(一般是用 Solidity 写)
2.编译智能合约的代码变成可在 EVM 上执行的 bytecode(binary code)。同时可以通过编译取得智能合约的 ABI
3.部署智能合约,实际上是把 bytecode 存储在链上(通过一个transaction),并取得一个专属于这个合约的地址
4.如果要写个程序调用这个智能合约,就要把信息发送到这个合约的地址(一样的也是通过一个 transaction)。Ethereum 节点会根据输入的信息,选择要执行合约中的哪一个 function 和要输入的参数。而要如何知道這这个智能合约提供哪些 function 以及应该要传入什么样的参数?这些信息就是记录在智能合约的 ABI。
此时,本 demo 已使用两种方法在本地私链部署上了智能合约,一种是基于 MetaMask 钱包,另一种是基于 Golang ,下面对已部署的合约进行合约调用:
①在geth客户端定义该智能合约的interface,在钱包中可以找到abi,将abi复制到 jsonview 中去除空格并赋值到变量:
var abi = [{"constant":true,"inputs":[],"name":"manager","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"pickWinner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"random","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getPlayers","outputs":[{"name":"","type":"address[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"enter","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"players","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"}];
②定义该智能合约的地址:
var address = "0x2566a7db5d30634e20b77f556266de324239c250";
③取得智能合约的实例,通过abi和合约地址取得智能合约的实例:
var Lottery = web3.eth.contract(abi).at(address);
④调用合约函数:
在当前链下新建账户,使一共有6名账户,对每一名账户进行命名:
var user0 = web3.eth.accounts[0];
var user1 = web3.eth.accounts[1];
var user2 = web3.eth.accounts[2];
var user3 = web3.eth.accounts[3];
var user4 = web3.eth.accounts[4];
var user5 = web3.eth.accounts[5];
同时,为每一个账户预置资金,当做参与博彩游戏的本金。
当上述交易被确认后,让每一名玩家都参与到博彩游戏中来:
Lottery.enter.sendTransaction({from: user0, value:web3.toWei(1, "ether"), gas: 1000000});
Lottery.enter.sendTransaction({from: user1, value:web3.toWei(1, "ether"), gas: 1000000});
Lottery.enter.sendTransaction({from: user2, value:web3.toWei(1, "ether"), gas: 1000000});
Lottery.enter.sendTransaction({from: user3, value:web3.toWei(1, "ether"), gas: 1000000});
Lottery.enter.sendTransaction({from: user4, value:web3.toWei(1, "ether"), gas: 1000000});
Lottery.enter.sendTransaction({from: user5, value:web3.toWei(1, "ether"), gas: 1000000});
此时,账户 user2 拥有6名玩家投入的6个ether,他的任务是随机 pick 一位 winner 获得所有奖励,为此,为 user2 调用以下方法:
Lottery.random()
Lottery.getPlayers()
personal.unlockAccount(eth.accounts[2])
Lottery.pickWinner.sendTransaction({from: user2, gas: 1000000});
最后使用 web3.fromWei(eth.getBalance(eth.accounts[i), "ether") 查看每一名玩家的余额,发现 winner 是 1,即他获得了其余5名玩家的5个 ether:
其中,账户0是 coinbase,之前余额为190,账户2位 MetaMask 钱包地址,之前余额为59,其余账户之前余额均为4,账户2获得了其它5名玩家的5个ether,他最后的余额为 9。当然,每名玩家在交易的过程中要支付一定的 gas 手续费。至此,本 demo 功能基本完成,后续会尝试再从 web3.js 上寻求优化的空间。
最常使用的一些命令如下:
cd private-chain
geth --datadir ./ init ./genesis.json
cd private-chain
geth --datadir ./ --networkid 1330 --http --http.addr 101.76.247.184 --http.vhosts "*" --http.port 8545 --http.api 'db,net,eth,web3,personal' --nodiscover --allow-insecure-unlock --http.corsdomain "*" console 2>>geth.log
geth --dev --http --http.addr 101.76.247.184 --http.vhosts "*" --http.port 8545 --http.api 'db,net,eth,web3,personal' --nodiscover --allow-insecure-unlock --http.corsdomain "*" console 2>>geth.log
cd private-chain
tail -f geth.log
eth.accounts
personal.newAccount()
eth.blockNumber
miner.start()
miner.stop()
eth.getBalance(eth.accounts[0])
web3.fromWei(eth.getBalance(eth.accounts[0]), "ether")
eth.getBlock(0)
personal.unlockAccount(eth.accounts[0])
eth.sendTransaction({from:eth.accounts[0], to:"0x68e9F0c38e31b5D4D25AbEfEE28938Ac263205a5",value:web3.toWei(1, "ether")})
eth.sendTransaction({from:eth.accounts[0], to:eth.accounts[1],value:web3.toWei(4, "ether")})
eth.getTransaction("0x6d5dcddf009824bcd4fcd5afdf4a8713b08bbb6ab6209b2840756cac667bbce6")
eth.getCode("")
?2021 ZhouJin, Shandong University, elford233@gmail.com
Last edit time 2021/12/15
|