水龙头合约

本文将实现一个运行在 BSV 链上的“水龙头”(faucet)合约,它可以满足以下需求:

  • 任何人都可以从合约中提取(withdraw)固定数量的 BSV,但连续两次提取必须至少间隔一定的冷却(cool down)时间
  • 任何人都可以随时向合约中充值(deposit)
  • 部署合约的人可以随时销毁(destroy)合约

withdraw

支持用户从合约中提取资金并不复杂,唯一的难点是冷却时间,我们可以使用 non-final 交易来实现。

  1. 在合约中记录最后一次提取资金的时间 lastWithdrawTimestamp,这样就可以限制下一次提取必须在时间 lastWithdrawTimestamp + withdrawIntervals 之后
  2. 要求调用交易 nLocktime 字段的值,不小于 lastWithdrawTimestamp + withdrawIntervals。如果满足,就把合约中记录的最后一次提款时间,更新为该交易 nLocktime 的值
  3. 限制调用交易只能有两个输出,合约本身的输出和用户提款的 P2PKH
contract Faucet {

// miner fee in satoshi per each withdraw
static const int withdrawMinerFee = 6000;

// withdraw interval limit in seconds
int withdrawIntervals;
// how many satoshis can be withdrawn each time
int withdrawAmount;
// public key hash of the creator
PubKeyHash creatorPkh;

@state
int lastWithdrawTimestamp;

public function withdraw(SigHashPreimage txPreimage, PubKeyHash pkh) {
require(Tx.checkPreimage(txPreimage));
// require nLocktime enabled https://wiki.bitcoinsv.io/index.php/NLocktime_and_nSequence
require(SigHash.nSequence(txPreimage) < 0xffffffff);
// require meets the call interval limits
require(SigHash.nLocktime(txPreimage) - this.lastWithdrawTimestamp >= this.withdrawIntervals);
require(SigHash.nLocktime(txPreimage) - this.lastWithdrawTimestamp < 2 * this.withdrawIntervals);

this.lastWithdrawTimestamp = SigHash.nLocktime(txPreimage);

bytes contractOutput = Utils.buildOutput(this.getStateScript(), SigHash.value(txPreimage) - this.withdrawAmount - withdrawMinerFee);
bytes withdrawOutput = Utils.buildOutput(Utils.buildPublicKeyHashScript(pkh), this.withdrawAmount);
// require 2 outputs
bytes expectedOutputs = contractOutput + withdrawOutput;
require(SigHash.hashOutputs(txPreimage) == hash256(expectedOutputs));
}
}

这个设计非常巧妙。在 nLocktime 指示的时间到来之前,调用交易是 non-final 的,虽然节点会接受和传播它,但这没什么用,任何人都不能继续花费 non-final 交易的输出,所以此时此刻,你其实并没有真正收到水龙头发出的 BSV。但它又不是完全没用,你只需等待,等到当前时间超过 nLocktime 的限制让这个交易自动变成 final 后,就会收到这些 BSV。

那为什么还要限制交易 nLocktime 的值不能太大呢?

require(SigHash.nLocktime(txPreimage) - this.lastWithdrawTimestamp < 2 * this.withdrawIntervals);

为了防止有人捣乱。有个叫张三的人他很坏,他在调用合约提款时把交易 nLocktime 的值设置的非常大,让交易很久都能不能 final,从而“卡住”合约,这样所有人就只能等 14 天,等交易超时被移出内存池后才能继续调用。那 non-final 交易不是可以被替换吗?是可以被替换,但张三可以在调用时把合约输入的 nSequence 设置成 0xFFFFFFFE,或者在调用时加上自己的输入来阻止你。😂

deposit 和 destroy

给合约充值和销毁合约这两个功能,都在文章计数器合约中详细讨论过。

  • 充值 v3
  • 充值 v4
  • 销毁 v5

这里我们用 SIGHASH_SINGLE | ANYONECANPAY 来实现 deposit,因为它既简单又灵活。

public function deposit(SigHashPreimage txPreimage, int depositAmount) {
SigHashType sigHashType = SigHash.SINGLE | SigHash.ANYONECANPAY | SigHash.FORKID;
require(Tx.checkPreimageSigHashType(txPreimage, sigHashType));
// avoid stealing money from the contract
require(depositAmount > 0);

bytes expectedOutput = Utils.buildOutput(this.getStateScript(), SigHash.value(txPreimage) + depositAmount);
require(SigHash.hashOutputs(txPreimage) == hash256(expectedOutput));
}

public function destroy(Sig sig, PubKey pk) {
// only the creator can destroy
require(hash160(pk) == this.creatorPkh);
require(checkSig(sig, pk));
}

执行合约

水龙头合约在 BSV 测试网上的运行效果如下:

代码

参考