本文主要介绍了如何使用truffle + Atom
进行以太坊密封拍卖智能合约的编写,以及如何使用ganache-cli
进行智能合约的交互测试。
1 Trueffle框架编写代码
相关细节可以查看另一篇文章以太坊公开拍卖智能合约(truffle + ganache-cli)。本文主要介绍合约实现,以及一些新的点。
1.1 建立项目
PS H:\TestContract> mkdir BlindAuction
目录: H:\TestContract
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2018/7/14 14:34 BlindAuction
PS H:\TestContract> cd BlindAuction
PS H:\TestContract\BlindAuction> truffle init
Downloading...
Unpacking...
Setting up...
Unbox successful. Sweet!
Commands:
Compile: truffle compile
Migrate: truffle migrate
Test contracts: truffle test
PS H:\TestContract\BlindAuction> cd contracts
PS H:\TestContract\BlindAuction\contracts> truffle create contract BlindAuction
-
\contracts
:存放智能合约源代码的地方,可以看到里面已经有一个sol
文件,我们开发的BlindAuction.sol
文件就存放在这个文件夹。 -
\migrations
:这是Truffle
用来部署智能合约的功能,待会儿我们会新建一个类似1_initial_migration.js
的文件来部署BlindAuction.sol
。 -
\test
:测试智能合约的代码放在这里,支持js
与sol
测试。 -
truffle-config.js
和truffle.js
:Truffle
的配置文件,需要配置要连接的以太坊网络。
1.2 创建合约
需求:
请实现一个拍卖协议,在该协议中,每个用户可以提交自己的出价。但是用户之间不能看到之间的出价,最后出价最高的人获得拍卖。
思路:
如何才能让大家互相看不到出价呢?我们可以让每个人把自己的出价加密一下,然后在一段时间内大家都给出加密后的出价。再出价结束后,给出一段时间让大家揭示自己的出价,并且从中选择最高的出价。
但是,我们依然可以从你传递的代币的数量判断你的出价。因此我们一个方案是大家只是支付定金,最后要补上全额。但是这个问题是,大家可以根据已经展示的用户的出价来判断自己是否展示自己的出价。
那么我们需要设计更加复杂的出价方案。一个方法是每个人都需要把自己的出价高于一个真实的出价值。同时允许用户虚假的出价,来混淆视听。但是这个都需要用户在揭秘的时候,得到属于自己的出价。
流程:
- 进入出价时刻
- 每个人可以提交自己的出价,且只能进行一次报价
- 用户只需要传递的是出价的一个hash
- 进入展示时刻
- 每个人给出自己之前提交的hash对应的真实出价列表
- 针对有效的出价,我们进行核算,选出胜者,退换押金
pragma solidity ^0.4.22;
contract BlindAuction {
struct Bid{
bytes32 blindBid;
uint deposit;
}
address public beneficiary; // 受益人
// uint public currentTime; // 当前时间
uint public biddingEnd; // 出价结束时间
uint public revealEnd; // 揭示价格结束时间
bool public ended; // 拍卖是否结束
//uint public testValue; // 仅仅是为了测试用
mapping(address => Bid) public bids; // 地址到竞标之间的映射
address public highestBidder; // 最高出价者
uint public highestBid; // 最高出价
// 允许撤回没有成功的出价
mapping(address => uint) pendingReturns;
event AuctionEnded(address winner, uint highestBid); // 拍卖结束的事件
/// 修饰符主要用于验证输入的正确
/// onlyBefore和onlyAfter用于验证是否大于或小于一个时间点
/// 其中‘_’是原始程序开始执行的地方
modifier onlyBefore(uint _time) { require(now < _time); _; }
modifier onlyAfter(uint _time) { require(now > _time); _; }
// 构建函数:保存受益人、竞标结束时间、公示价格结束时间
constructor(address _beneficiary, uint _biddingTime, uint _revealTime) public {
beneficiary = _beneficiary;
biddingEnd = now + _biddingTime;
revealEnd = biddingEnd + _revealTime;
}
function getCurrentTime() public returns (uint){
// currentTime = now;
return now;
}
// 由于truffle console中的web3.sha3()的返回值与solidity不同,在这里用一个测试函数
// 为了得到hash值
function Encryption(uint _value, uint nonce) public returns (bytes32){
//require (_values != 0 && _fake != 0 && _secret != 0);
return sha3(msg.sender, _value, nonce);
}
/// 给出一个秘密出价_blindBid = sha3(value, fake, secret)
/// 给出的保证金只在出价正确时给予返回
/// 有效的出价要求出价的ether至少到达value
/// 如果是我的话,我会限制一个人的出价都是有效的才进行进一步的操作,从而增加造假的难度
function bid(bytes32 _blindBid) public payable onlyBefore(biddingEnd) {
require(bids[msg.sender].blindBid == bytes32(0) );
bids[msg.sender] = Bid({blindBid: _blindBid, deposit:msg.value});
}
/// 公示你的出价,对于正确参与的出价,只要没有最终获胜,都会被归还
function reveal(uint _value, uint nonce) public onlyAfter(biddingEnd) onlyBefore(revealEnd) returns (uint){
require (_value != 0 && nonce != 0);
uint refund;
var bid = bids[msg.sender];
uint value = _value * 1 ether;
// bid不正确,不会退回押金
require(bid.blindBid == sha3(msg.sender, _value, nonce));
refund += bid.deposit;
if (bid.deposit >= value) {
// 处理bid超过value的情况
if(placeBid(msg.sender, value)){
refund -= value;
}
}
// 防止再次claimed押金
bid.blindBid = bytes32(0);
msg.sender.transfer(refund); // 如果之前没有置0,会有fallback风险
return refund;
}
// 这是个内部函数,只能被协约本身调用
function placeBid(address bidder, uint value) internal returns(bool success){
if(value <= highestBid){
return false;
}
if (highestBidder != 0){
// 返回押金给之前出价最高的人
pendingReturns[highestBidder] += highestBid;
}
highestBid = value;
highestBidder = bidder;
return true;
}
// 撤回过多的出价
function withdraw() public {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
// 一定要先置0,规避风险
msg.sender.transfer(amount);
}
}
// 拍卖结束,把代币发给受益人
function auctionEnd() public onlyAfter(revealEnd){
require(!ended);
ended = true;
emit AuctionEnded(highestBidder, highestBid);
beneficiary.transfer(highestBid);
}
function getBalance() public returns (uint) {
return this.balance;
}
function () public{
revert();
}
}
关于
modifier
修饰符:
继承这个modifier修饰的function加上一个特定的约束,在执行函数时,会先检查modifier中的内容。
特殊_
表示使用修改符的函数体的替换位置。
1.3 编译合约
同样可以参考之前的文章,有详细说明。
在项目根目录BlindAuction
的powershell中执行truffle compile
命令:
PS H:\TestContract\BlindAuction> truffle compile
Compiling .\contracts\BlindAuction.sol...
Compiling .\contracts\Migrations.sol...
Compilation warnings encountered:
.....
Writing artifacts to .\build\contracts
2 Ganache-cli 部署测试智能合约
2.1 启动ganache-cli
打开powershell
终端,可以看到ganache-cli
启动后自动建立了10
个账号(Accounts),与每个账号对应的私钥(Private Key)。每个账号中都有100
个测试用的以太币(Ether)。
Note. ganache-cli仅运行在内存中,因此每次重开时都会回到全新的状态。
补充
ganache-cli
制定创建账号数:
ganache-cli,有时10个账号可能不够测试用。
我们可以使用ganache-cli -a 20
来指定创建20个测试账号。
命令ganache-cli -a
或ganache-cli -accounts
启动选项
- -a 或 –accounts: 指定启动时要创建的测试账户数量。
- -e 或 –defaultBalanceEther: 分配给每个测试账户的ether数量,默认值为100。
- -b 或r –blockTime: 指定自动挖矿的blockTime,以秒为单位。默认值为0,表示不进行自动挖矿。
- -d 或 –deterministic: 基于预定的助记词(mnemonic)生成固定的测试账户地址。
- -n 或 –secure: 默认锁定所有测试账户,有利于进行第三方交易签名。
- -m 或 –mnemonic: 用于生成测试账户地址的助记词。
- -p 或 –port: 设置监听端口,默认值为8545。
- -h 或 –hostname: 设置监听主机,默认值同NodeJS的server.listen()。
- -s 或 –seed: 设置生成助记词的种子。.
- -g 或 –gasPrice: 设定Gas价格,默认值为20000000000。
- -l 或 –gasLimit: 设定Gas上限,默认值为90000。
- -f 或 –fork: 从一个运行中的以太坊节点客户端软件的指定区块分叉。输入值应当是该节点旳HTTP地址和端口,例如http://localhost:8545。 可选使用@标记来指定具体区块,例如:http://localhost:8545@1599200。
- -i 或 –networkId:指定网络id。默认值为当前时间,或使用所分叉链的网络id。
- –db: 设置保存链数据的目录。如果该路径中已经有链数据,ganache-cli将用它初始化链而不是重新创建。
- –debug:输出VM操作码,用于调试。
- –mem:输出ganache-cli内存使用统计信息,这将替代标准的输出信息。
- –noVMErrorsOnRPCResponse:不把失败的交易作为RCP错误发送。开启这个标志使错误报告方式兼容其他的节点客户端,例如geth和Parity。
特殊选项
–account: 指定账户私钥和账户余额来创建初始测试账户。可多次设置:
$ganache-cli --account="<privatekey>,balance" [--account="<privatekey>,balance"]
注意私钥长度为64字符,必须使用0x前缀的16进制字符串。账户余额可以是整数,也可以是0x前缀的17进制字符串,单位为wei。
使用–account选项时,不会自动创建HD钱包。-u 或 –unlock: 解锁指定账户,或解锁指定序号的账户。可以设置多次。当与–secure选项同时使用时,这个选项将改变指定账户的锁定状态:
$ ganache-cli --secure --unlock "0x1234..." --unlock "0xabcd..."
也可以指定一个数字,按序号解锁账号:
$ ganache-cli --secure -u 0 -u 1
2.2 部署合约
(1)migrations
目录下创建一个名字叫做2_deploy_contracts.js
的文件。文件中的内容为:
var BlindAuction = artifacts.require('./BlindAuction.sol');
module.exports = function(deployer) {
deployer.deploy(BlindAuction, '0xa988cf1fae1275fdacf1d5bdf591c2eb57e3e8e1', 500, 200);
}
(2)修改truffle.js
文件,连接本地ganache-cli
环境。参数在最开初始化ganache-cli
环境的窗口可以看到。
module.exports = {
// See <http://truffleframework.com/docs/advanced/configuration>
// to customize your Truffle configuration!
networks: {
development:{
host: "127.0.0.1",
port: 8545,
network_id: "*" // match any network id
}
}
};
(3)现在执行truffle migrate
命令,我们可以将BlindAuction.sol
原始码编译成Ethereum bytecode
。
PS H:\TestContract\BlindAuction> truffle migrate --reset
Using network 'development'.
Running migration: 1_initial_migration.js
Deploying Migrations...
... 0x8c623ea9503338d10d5d250d292290fe09ae1bf8f44d819af6056b1db8d2c036
Migrations: 0xf908b0ae60ab2d974d68a19c54c4a8e8400d0e6a
Saving successful migration to network...
... 0x3982ca94a4259f9e6530ec8e9068653dce008e6dc75b371bc6dd87042bdc46b9
Saving artifacts...
Running migration: 2_deploy_contracts.js
Deploying BlindAuction...
... 0xf78c790e3566212d13ddef7c0cbb62fe7a562ab2451cedf154506c02a7a4add4
BlindAuction: 0x043d70d33c5953d12df26e17e55a4a5a9182aebd
Saving successful migration to network...
... 0x2b9c56edfdef31a1c7a3effcaeec9ec5fc9825bddcf5d937012d3d1314c2323f
Saving artifacts...
2.3 与合约交互
truffle
提供命令行工具,执行truffle console
命令后,可用Javascript
来和刚刚部署的合约互动。
PS H:\TestContract\SimpleAuction> truffle console
truffle(development)>
使用web3.eth.accounts
会输出ganache-cli
网络上的所有账户。
truffle(development)> web3.eth.accounts
[ '0x9392600526453a1f140581cc6b2eceed4e73d9d6',
'0x4d6bc04cf9caa4409a1193e13f20d38fc6fe65a7',
'0x6f560775ed5180a362e133c212374b9449ce4f1a',
'0x0f060c7a5e74926be69fa5aaf204cc81c007df69',
'0x75ef9c9200cf06497df4f279f3f076b973b115ce',
'0x06a2f829f5b8563faf7a67cdd0616ed7e49ae621',
'0x2af8343940d42d327c11e211ed2b0b58b346d1aa',
'0xea6cccbcb61f96a18ab15ed5966335178dd2ca30',
'0xf57ecb22bfc8429f8e1acd1aae208634250af23d',
'0xa988cf1fae1275fdacf1d5bdf591c2eb57e3e8e1' ]
我们需要准备一些测试账户。
它会把第一个帐户的地址分配给变量account0
,第二个帐户分配给变量account1
。Web3
是一个JavaScript API
,它将RPC
调用包装起来以方便我们与区块链进行交互。
我在这里将第9个账户作为部署合约初始化的拍卖发起人。
2.3.1 参与拍卖的账户
PS H:\TestContract\BlindAuction> truffle console
truffle(development)> address = web3.eth.accounts[9]
'0xa988cf1fae1275fdacf1d5bdf591c2eb57e3e8e1'
truffle(development)> acc1 = web3.eth.accounts[1]
'0x4d6bc04cf9caa4409a1193e13f20d38fc6fe65a7'
truffle(development)> acc2 = web3.eth.accounts[2]
'0x6f560775ed5180a362e133c212374b9449ce4f1a'
truffle(development)> acc3 = web3.eth.accounts[3]
'0x0f060c7a5e74926be69fa5aaf204cc81c007df69'
我们可以看一下拍卖发起人以及第一个账户的余额:
truffle(development)> web3.eth.getBalance(address)
BigNumber { s: 1, e: 20, c: [ 1000000 ] }
truffle(development)> web3.eth.getBalance(acc1)
BigNumber { s: 1, e: 20, c: [ 1000000 ] }
2.3.2 启动拍卖
现在我们需要先启动一个拍卖,才能进行接下来的操作。
当拍卖一旦启动,计时就开始了。
truffle(development)> let contract
undefined
truffle(development)> BlindAuction.deployed().then(instance => contract = instance)
Note: 非常非常重要
大家如果看代码,可以发现,我在合约中写了一个生成hash
的函数,实际使用中会被别人调用来破解你的实际价格。然后目前存在一个问题是,truffle console
中web3.sha3()
和sodility
中的sha3() / keccak256()
hash结果不同。我只能通过调用solidity
函数加密得到hash值,再用这个hash值做后续测试。并且目前我在网上没有找到很好的解决办法。如果有好的办法的,请一定告诉我,感谢!!
truffle console
:
web3.sha3(string, options)
:只能放一个变量,如web3.sha3("hello world", {encoding: 'hex')
。
solidity
:
sha3(arg1, arg2, ...)
:可以放多个变量,如web3.sha3(2, 3, 4)
。但是我需要多个参数的hash,
console
这个无法实现。
还有一个最重要的问题是:在console中定义一个报价value = 2
,web3.sha3(2)
是会报错的。我们用web3.sha3('2')
,结果会和solidity中的sha3(2)
完全不同,这种写法在solidity中正确。实际上在solidity中传递进去的参数2
,是uint
格式,它真正的写法是0x0000000000...002
256位的16进制数。这时候我们就想到了使用
web3.sha3(web3.toHex(2))
或者web3.sha3('2', {encoding: 'hex'})
,然而发现结果仍然不对。那肯定是
web3.toHex(2)
与solidity
中的2
不一样,果然打印web3.toHex(2)
,结果是0x2
。在网上看到有人说使用
leftpad
填充成256位,然而尝试了一下,没有这个函数。如果有网友可以帮我解决在truffle console
中将类似0x2
的数填充成0x00000000000...002
这样的数的问题,那我将不胜感激。
所以由于上述问题的存在,我们首先需要先生成参与拍卖账户的hash
,并记录下来,以便参与bid
过程。
truffle(development)> contract.Encryption.call(30,1000,{from:acc1})
'0x6403671c6162860975ffd948c931906f19a21bb99274689fd4c91dcc48000f5a'
truffle(development)> contract.Encryption.call(65,1323,{from:acc2})
'0x5a8fd4c2bb0a84b354464a85e3b81b9fa74d1fe6dc8071ba24b3a91b9faa8c51'
truffle(development)> contract.Encryption.call(40,576,{from:acc3})
'0x7182e09ffb55fc0d219ea44373a9d2e586cd82c0bc2fc3a674b3f8ad4230bb5e'
truffle(development)> bid1 = '0x6403671c6162860975ffd948c931906f19a21bb99274689fd4c91dcc48000f5a'
'0x6403671c6162860975ffd948c931906f19a21bb99274689fd4c91dcc48000f5a'
truffle(development)> bid2 = '0x5a8fd4c2bb0a84b354464a85e3b81b9fa74d1fe6dc8071ba24b3a91b9faa8c51'
'0x5a8fd4c2bb0a84b354464a85e3b81b9fa74d1fe6dc8071ba24b3a91b9faa8c51'
truffle(development)> bid3 = '0x7182e09ffb55fc0d219ea44373a9d2e586cd82c0bc2fc3a674b3f8ad4230bb5e'
'0x7182e09ffb55fc0d219ea44373a9d2e586cd82c0bc2fc3a674b3f8ad4230bb5e'
2.3.3 开始报价
此时我们用acc1
调用bid()
,发送hash
值,并且附带50 ether
作为押金。
truffle(development)> contract.bid(bid1,{from:acc1,value:web3.toWei(50,"ether")})
{ tx: '0xcc5d68719659eff2a1393375b6d629e512397426e1a323b51c14e38339f10f2f',
receipt:
{ transactionHash: '0xcc5d68719659eff2a1393375b6d629e512397426e1a323b51c14e38339f10f2f',
transactionIndex: 0,
blockHash: '0xf17e17497165eb47cc692e4e4a8afe14a77acce153c439f9d2529a291f6d7f9d',
blockNumber: 5,
gasUsed: 64655,
cumulativeGasUsed: 64655,
contractAddress: null,
logs: [],
status: '0x1',
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' },
logs: [] }
并且查看此时acc1
余额。
truffle(development)> web3.eth.getBalance(acc1)
BigNumber { s: 1, e: 19, c: [ 499912, 93500000000000 ] }
这里给出参与拍卖三个账户的参数,以及bid
结束后各账户的余额:
Account | Bid | Nonce | value(deposit) | Balance |
---|---|---|---|---|
acc1 | 30 | 1000 | 50 | 499912, 93500000000000 |
acc2 | 65 | 1323 | 70 | 299912, 93500000000000 |
acc3 | 40 | 576 | 80 | 199912, 93500000000000 |
address | 拍卖发起人 | 1000000 |
2.3.4 打开价格
等待报价结束,展示报价阶段:
truffle(development)> contract.reveal(30,1000,{from:acc1})
{ tx: '0xf2e2ceb794991d66fc545a444c6d996ff7de4de25e5a7294bf649eeb8c9266da',
receipt:
{ transactionHash: '0xf2e2ceb794991d66fc545a444c6d996ff7de4de25e5a7294bf649eeb8c9266da',
transactionIndex: 0,
blockHash: '0x7eb68508ddbdedd3b10a76781555c20cb73595f72fc400d7b1af36de245db2ee',
blockNumber: 8,
gasUsed: 62209,
cumulativeGasUsed: 62209,
contractAddress: null,
logs: [],
status: '0x1',
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' },
logs: [] }
同样地:
truffle(development)> contract.reveal(65,1323,{from:acc2})
...
truffle(development)> contract.reveal(40,576,{from:acc3})
打开报价:
报价高于当前highestBid
时,会进行替换最高价,并且退回deposit-bid
的部分,同时将上一个报价最高的人的价格存起来,最后可以取回。
报价低于当前highestBid
时,会直接退回全部deposit
,竞价失败。
报价全部打开,没有按时打开的,或者给出了与开始报价不同的错误报价,将会失去自己的押金。
再看此时各账户的余额(此时还没有进行withdraw
):
Account | Value |
---|---|
acc1 | 699873, 13600000000000 |
acc2 | 349882, 31300000000000 |
acc3 | 999913, 62800000000000 |
分析一下:
acc1
第一个打开,成为highestBid
,因为30 > 0
。返还差价50-30 = 20 ether
,账户目前少了30 ether
。
acc2
第二个打开,成为highestBid
,因为65 > 30
。返还差价70 - 65 = 5 ether
,账户目前少了65 ether
。同时将上一个最高价者(也就是acc1
)的价格30
加入pendingReturns
,以供拍卖结束后,acc1
取回自己的钱。此时pendingReturns[acc1] = 30
。
acc3
第三个打开,40 < 65
,直接退回所有押金80 ether
,账户目前不少钱。
当然会发现,每个账户都会少量的失去以太币,这是因为交易需要消耗gas
。
2.3.5 拍卖结束
最终,拍卖发起者结束拍卖:
truffle(development)> contract.auctionEnd({from:address})
{ tx: '0xb6c6aaf61b1a5be2996c26c35e0e32c23328e11308b10e33746d966f2910197e',
receipt:
{ transactionHash: '0xb6c6aaf61b1a5be2996c26c35e0e32c23328e11308b10e33746d966f2910197e',
transactionIndex: 0,
........
event: 'AuctionEnded',
args: [Object] } ] }
truffle(development)> web3.eth.getBalance(address)
BigNumber { s: 1, e: 20, c: [ 1649948, 3300000000000 ] }
可以看到发起者address
的balance = 1649948, 3300000000000
,增加了约65 ether
。
其他拍卖者,可以取回自己的钱:
truffle(development)> contract.withdraw({from:acc1})
{ tx: '0x69eb8c1fc115ef55c7f5174fa67724b589b526269dc29bd268cf1a5d8d373e94',
receipt:
{ transactionHash: '0x69eb8c1fc115ef55c7f5174fa67724b589b526269dc29bd268cf1a5d8d373e94',
.........
logs: [] }
truffle(development)> contract.withdraw({from:acc2})
truffle(development)> contract.withdraw({from:acc3})
看看当前各账户余额:
Account | Balance |
---|---|
address | 1649948, 3300000000000 |
acc1 | 999843, 79000000000000 |
acc2 | 349860, 47000000000000 |
acc3 | 999891, 78500000000000 |
至此,整个拍卖过程完全结束。
2.5 账户变化过程总结
同时给出拍卖参数和账户余额变化,方便比较。
参与hash
的三个参数,地址&真实报价&随机数。
Account | Bid | Nonce | value(deposit) |
---|---|---|---|
acc1 | 30 | 1000 | 50 |
acc2 | 65 | 1323 | 70 |
acc3 | 40 | 576 | 80 |
每个参与拍卖的账户余额变化:
账户 | 1.初始化 | 2.Bid | 3.Reveal | 4.End & Withdraw |
---|---|---|---|---|
address | 1000000 | 1000000 | 1000000 | 1649948, 3300000000000 |
acc1 | 1000000 | 499912, 93500000000000 | 699873, 13600000000000 | 9999843, 79000000000000 |
acc2 | 1000000 | 299912, 93500000000000 | 349882, 31300000000000 | 349860, 4700000000000 |
acc3 | 1000000 | 199912, 93500000000000 | 999913, 62800000000000 | 999891, 78500000000000 |
如果测试过程出现什么难以理解的事情,请把build
文件夹完全删除,重新编译。
因为编译器多次编译,内部出的错误,让我折腾了一天,都怀疑自己了。╮(╯▽╰)╭
本文作者:Joyce
文章来源:https://www.jianshu.com/p/8d7bbf966dda
版权声明:转载请注明出处!
2018年7月16日