水龍頭合約

2020-10-17 12:01:11

前言

水龍頭是什麼

水龍頭這個名字總會讓我想起生活中把水龍頭開關擰到無限接近但又不達到關閉狀態的大媽們,這樣可以讓水一滴一滴緩慢滴出,但又不會觸發水錶計費,彰顯出她們豐富的生活經驗和生存智慧。

水龍頭是贈送小額位元幣的服務。
為了讓使用者可以快速嘗試Bitcoin SV網路,會有人搭建水龍頭服務,給使用者贈送給小額位元幣,這樣使用者就可以用這些幣嘗試使用Bitcoin SV網路,如:轉賬、測試、寫入資料等。
——wiki.bsv.info

合約需求

本文介紹如何通過智慧合約直接在鏈上提供水龍頭服務。該服務滿足如下需求:

  1. 任何人都可以向合約中充值。
  2. 每隔一段時間,任何人都可以從合約中提取一定額度的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最多會有兩個有先後順序的輸出:

  1. 充值後的合約
  2. 找零(可選)
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最多會有兩個有先後順序的輸出:

  1. 合約輸出
  2. 贈送輸出
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);
    }
}