以太坊私链账户下智能合约的部署与调用——使用RemixGolangGeth
Posted zj233333
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了以太坊私链账户下智能合约的部署与调用——使用RemixGolangGeth相关的知识,希望对你有一定的参考价值。
综述
智能合约调用是实现一个 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环境:
# 获取特定版本的golang文件
https://go.dev/dl/go1.17.5.linux-amd64.tar.gz
# 解压该文件到/usr/local/go,如果不存在该目录,应该先创建
sudo tar -C /usr/local/go -zxvf go1.17.5.linux-amd64.tar.gz
# 设置环境变量,go的根目录,即刚才解压的目录
export GOROOT=/usr/local/go
# 设置环境变量,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()
// Create an IPC based RPC connection to a remote node and an authorized transactor
// conn, err := ethclient.Dial("/home/ub/private-chain/geth.ipc")
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)
// Deploy a new awesome contract for the binding demo
// address, tx, Lottery, err := DeployLottery(auth, conn)// new(big.Int), "Contracts in Go!!!", 0, "Go!")
address, tx, _, err := DeployLottery(auth, conn)//, big.NewInt(1337), "Contracts in Go!!!", 0, "Go!")
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())
// Don't even wait, check its presence in the local pending state
time.Sleep(250 * time.Millisecond) // Allow it to be processed by the local node :P
/*
name, err := Lottery.Name(&bind.CallOptsPending: true)
if err != nil
log.Fatalf("Failed to retrieve pending name: %v", err)
fmt.Println("Pending name:", name)
*/
//fmt.Print("%x",Lottery)
其中,下列几个地方需要自行配置:
- 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(以上是关于以太坊私链账户下智能合约的部署与调用——使用RemixGolangGeth的主要内容,如果未能解决你的问题,请参考以下文章