前言
水龙头是什么
水龙头这个名字总会让我想起生活中把水龙头开关拧到无限接近但又不达到关闭状态的大妈们,这样可以让水一滴一滴缓慢滴出,但又不会触发水表计费,彰显出她们丰富的生活经验和生存智慧。
水龙头是赠送小额比特币的服务。
为了让用户可以快速尝试Bitcoin SV网络,会有人搭建水龙头服务,给用户赠送给小额比特币,这样用户就可以用这些币尝试使用Bitcoin SV网络,如:转账、测试、写入数据等。
——wiki.bsv.info
合约需求
本文介绍如何通过智能合约直接在链上提供水龙头服务。该服务满足如下需求:
- 任何人都可以向合约中充值。
- 每隔一段时间,任何人都可以从合约中提取一定额度的BSV。
完整的实现代码放在了文末。
准备知识
阅读本文前需要先了解OP_PUSH_TX的相关知识,推荐阅读如下文章:
代码分析
合约有两个公有函数,代表着两个合约功能:
- 充值
charge
- 赠送(滴水)
drop
充值功能分析
函数参数
SigHashPreimage preImage
:当前tx的签名哈希原像。如果你不理解这个参数的含义,那么说明你没有看准备知识部分推荐的文章。int chargeAmount
:充值聪数。Ripemd160 changePKH
:找零用的公钥Hash。int changeAmount
: 找零聪数。
参数检查
require(Tx.checkPreimage(preImage));
require(chargeAmount > 0);
对参数进行取值范围的检查。
因为sCrypt目前还不支持unsigned int
类型,所以需要检查chargeAmount
参数是正数,避免出现利用充值函数从合约中取走币的漏洞。
构造合约输出
合约规定充值tx最多会有两个有先后顺序的输出:
- 充值后的合约
- 找零(可选)
bytes output0 = this.composeChargeOutput0(preImage, chargeAmount);
function composeChargeOutput0(SigHashPreimage preImage, int chargeAmount):bytes{
bytes lockingScript = Util.scriptCode(preImage);
int contractTotalAmount = Util.value(preImage) + chargeAmount;
return Util.buildOutput(lockingScript, contractTotalAmount);
}
充值前合约里的余额加上要充值的额度就是充值后的合约里的余额。这里你就可以理解为什么要检查chargeAmount
是正数了。
充值不会改变合约的脚本,所以用上一个合约的脚本和充值后的余额就可以拼出充值后的合约输出。
构造找零输出
bytes output1 = this.composeChargeOutput1(changePKH, changeAmount);
function composeChargeOutput1(Ripemd160 changePKH, int changeAmount):bytes{
bytes output1 = b'';
if(changeAmount > 546){
output1 = Util.buildOutput(Util.buildPublicKeyHashScript(changePKH), changeAmount);
}
return output1;
}
如果找零额度小于546聪,则认为不需要找零,也就没有找零输出。
根据找零PKH构造出P2PKH格式的输出脚本,再结合找零额度,就可以构造出完整的输出。
校验所有输出的哈希值
Sha256 hashOutputs = hash256(output0 + output1);
require(hashOutputs == Util.hashOutputs(preImage));
不解释。
赠送功能分析
函数参数
SigHashPreimage preImage
:当前tx的签名哈希原像。Ripemd160 receiver
:接收者的公钥哈希。
参数检查
require(Tx.checkPreimage(preImage));
//在nSequence < 0xFFFFFFFF时nLockTime才有效。 https://wiki.bitcoinsv.io/index.php/NLocktime_and_nSequence
require(Util.nSequence(preImage) < 0xFFFFFFFF);
为了满足两次赠送转账之间的时间间隔,需要使用比特币的nLocktime
功能。设置了nLocktime
的值后,能够限制合约在该时间之前被花费,也就阻止了在该时间之前发起下一次赠送转账。
要让nLocktime
生效,则需要让合约输入的nSequence
小于0xFFFFFFFF,所以需要对该值进行检查。
手续费和赠送额度
int dropAmount = 2000000;
int fee = 3000;
我们简单粗暴地把每次赠送的数额设置为0.02BSV,不能多也不能少。
每次转账费用设置为3000聪,基本上相当于0.5聪每字节。
构造合约输出
合约规定赠送tx最多会有两个有先后顺序的输出:
- 合约输出
- 赠送输出
bytes output0 = this.composeDropOutput0(preImage, dropAmount, fee);
function composeDropOutput0(SigHashPreimage preImage, int dropAmount, int fee):bytes{
bytes prevLockingScript = Util.scriptCode(preImage);
int scriptLen = len(prevLockingScript);
int fiveMinutesInSecond = 300;
int newMatureTime = this.getPrevMatureTime(prevLockingScript, scriptLen) + fiveMinutesInSecond;
require(Util.nLocktime(preImage) == newMatureTime);
bytes codePart = this.getCodePart(prevLockingScript, scriptLen);
bytes script = codePart + pack(newMatureTime);
int amount = Util.value(preImage) - dropAmount - fee;
return Util.buildOutput(script, amount);
}
合约输出的数据部分是一个四字节的时间戳,也就是matureTime
,该值与tx的nLocktime
相等,表示合约在该时刻之后才可以被花费(充值或赠送)。matureTime
的目的是为了记录合约所在的上一个tx的nLocktime
,从而可以用该值对当前tx的nLocktime
进行校验。
合约设计成每隔5分钟可以被花费一次,那么matureTime
的值每次都会增加300秒,保证nLocktime
的值也是按照该规律增加,从而最终保证每次花费合约之间的间隔为5分钟。
老合约的余额减去赠送的数额,再减去手续费,就是新合约里的余额。脚本部分和余额部分组合在一起形成新合约的输出。
构造赠送输出
bytes output1 = this.composeDropOutput1(receiver, dropAmount);
function composeDropOutput1(Ripemd160 receiver, int dropAmount):bytes{
bytes script = Util.buildPublicKeyHashScript(receiver);
return Util.buildOutput(script, dropAmount);
}
不解释。
检查所有输出的哈希值
Sha256 hashOutputs = hash256(output0 + output1);
require(hashOutputs == Util.hashOutputs(preImage));
不解释。
总结
测试网络上已经部署了该合约:
感谢晓峰大爷提供测试网络的BSV。
完整代码
import "util.scrypt";
contract Faucet {
public function charge(SigHashPreimage preImage, int chargeAmount, Ripemd160 changePKH, int changeAmount) {
require(Tx.checkPreimage(preImage));
require(chargeAmount > 0);
bytes output0 = this.composeChargeOutput0(preImage, chargeAmount);
bytes output1 = this.composeChargeOutput1(changePKH, changeAmount);
Sha256 hashOutputs = hash256(output0 + output1);
require(hashOutputs == Util.hashOutputs(preImage));
}
public function drop(SigHashPreimage preImage, Ripemd160 receiver){
require(Tx.checkPreimage(preImage));
//在nSequence < 0xFFFFFFFF时nLockTime才有效。 https://wiki.bitcoinsv.io/index.php/NLocktime_and_nSequence
require(Util.nSequence(preImage) < 0xFFFFFFFF);
int dropAmount = 2000000;
int fee = 3000;
bytes output0 = this.composeDropOutput0(preImage, dropAmount, fee);
bytes output1 = this.composeDropOutput1(receiver, dropAmount);
Sha256 hashOutputs = hash256(output0 + output1);
require(hashOutputs == Util.hashOutputs(preImage));
}
function composeChargeOutput0(SigHashPreimage preImage, int chargeAmount):bytes{
bytes lockingScript = Util.scriptCode(preImage);
int contractTotalAmount = Util.value(preImage) + chargeAmount;
return Util.buildOutput(lockingScript, contractTotalAmount);
}
function composeChargeOutput1(Ripemd160 changePKH, int changeAmount):bytes{
bytes output1 = b'';
if(changeAmount > 546){
output1 = Util.buildOutput(Util.buildPublicKeyHashScript(changePKH), changeAmount);
}
return output1;
}
function getPrevMatureTime(bytes lockingScript, int scriptLen):int {
return unpack(lockingScript[scriptLen - 4 :]);
}
function getCodePart(bytes lockingScript, int scriptLen):bytes{
return lockingScript[0:scriptLen - 4];
}
function composeDropOutput0(SigHashPreimage preImage, int dropAmount, int fee):bytes{
bytes prevLockingScript = Util.scriptCode(preImage);
int scriptLen = len(prevLockingScript);
int fiveMinutesInSecond = 300;
int newMatureTime = this.getPrevMatureTime(prevLockingScript, scriptLen) + fiveMinutesInSecond;
require(Util.nLocktime(preImage) == newMatureTime);
bytes codePart = this.getCodePart(prevLockingScript, scriptLen);
bytes script = codePart + pack(newMatureTime);
int amount = Util.value(preImage) - dropAmount - fee;
return Util.buildOutput(script, amount);
}
function composeDropOutput1(Ripemd160 receiver, int dropAmount):bytes{
bytes script = Util.buildPublicKeyHashScript(receiver);
return Util.buildOutput(script, dropAmount);
}
}
来源:oschina
链接:https://my.oschina.net/u/4305979/blog/4678281