本文将实现一个运行在 BSV 链上的“水龙头”(faucet)合约,它可以满足以下需求:
- 任何人都可以从合约中提取(withdraw)固定数量的 BSV,但连续两次提取必须至少间隔一定的冷却(cool down)时间
- 任何人都可以随时向合约中充值(deposit)
- 部署合约的人可以随时销毁(destroy)合约
withdraw
支持用户从合约中提取资金并不复杂,唯一的难点是冷却时间,我们可以使用 non-final 交易来实现。
- 在合约中记录最后一次提取资金的时间 lastWithdrawTimestamp,这样就可以限制下一次提取必须在时间 lastWithdrawTimestamp + withdrawIntervals 之后
- 要求调用交易 nLocktime 字段的值,不小于 lastWithdrawTimestamp + withdrawIntervals。如果满足,就把合约中记录的最后一次提款时间,更新为该交易 nLocktime 的值
- 限制调用交易只能有两个输出,合约本身的输出和用户提款的 P2PKH
contract Faucet { |
这个设计非常巧妙。在 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
给合约充值和销毁合约这两个功能,都在文章计数器合约中详细讨论过。
这里我们用 SIGHASH_SINGLE | ANYONECANPAY 来实现 deposit,因为它既简单又灵活。
public function deposit(SigHashPreimage txPreimage, int depositAmount) { |
执行合约
水龙头合约在 BSV 测试网上的运行效果如下:
- 部署合约,锁定 100,000 聪 BSV,冷却时间 5 秒,每次可以提取 20,000 聪并固定支付 6,000 聪矿工费
- 调用 withdraw 提款
- 调用 withdraw 提款
- 调用 withdraw 提款
- 调用 deposit 充值
- 调用 withdraw 提款
- 调用 withdraw 提款
- 调用 withdraw 提款
- 调用 destroy 销毁合约