深入学习 BSV 脚本之 OP_PUSH_TX(2)

前一篇文章中,我们介绍了一个强大的功能 OP_PUSH_TX。它允许在 BSV 智能合约里访问当前 transaction,这样就可以实现很多复杂功能,比如有状态的合约。本文将在此基础上进一步介绍一个有意思的扩展合约。

Tx.checkPreimageAdvanced() 方法

sCrypt 在标准库合约 Tx 中提供了 Tx.checkPreimage(txPreimage) 方法来实现 OP_PUSH_TX 的基础功能,它可以满足很多场景的需求。但是,随着合约变得越来越复杂,可能需要做更多的定制。比如,在使用 R-Puzzle 的合约中需要指定临时密钥 k

checkPreimageAdvanced 是 sCrypt 标准合约 Tx 的另一个方法,它提供了更多用户可以调整的参数。除了 sighashPreimage 参数, checkPreimageAdvanced() 方法中还添加了更多参数来控制 OP_PUSH_TXECDSA签名

Tx.checkPreimageAdvanced(txPreimage, privKey, pubKey, invK, r, rBigEndian, sigHashType)

  • privKeypubKey:ECDSA 密钥对
  • invKk 的模倒数,临时密钥
  • rR 的 x 坐标,通过 kG 计算得出
  • rBigEndian:大端模式的有符号 r。严格来说,这个参数可以通过参数 r 算出来,但这需要在脚本中进行很多计算,成本较高。所以我们提供了该参数,这样就可以在链下计算了。
  • sigHashTypeSIGHASH 标识用来指定 transaction 的哪部分需要进行 ECDSA 签名。

ANYONECANPAY 计数器合约

另一篇文章中,我们实现了一个计数器合约,它可以记录它的函数被调用了多少次。但它有个缺点,合约需要自己支付转账手续费,一旦合约中的资金耗尽,就不能再被调用了。

我们可以利用 checkPreimageAdvanced() 中的 sighashType 参数来改进计数器合约。通过将 sighashType 设置为 SIGHASH_ANYONECANPAY,我们允许在解锁计数器合约的第一个 input 后面再追加 input,这样调用者就可以通过追加 input 来支付转账手续费了。我们还允许调用者再追加一个 output,把剩下的资金作为找零转到里面。完整代码如下:

import "util.scrypt";

/**
 * Demonstrates TxAdvanced, with external funding (additional input) and a change output
 */
contract AdvancedCounter {
    public function increment(bytes txPreimage, int amount, Ripemd160 changePKH, int changeSats) {
        // The following arguments can be generated using sample code at
        // https://gist.github.com/scrypt-sv/f6882be580780a88984cee75dd1564c4.js
        PrivKey privKey = PrivKey(0x621de38d9af72be8585d19584e3954d3fd0dc9752bb9f9fb28c4f9ed7c1e40ea);
        PubKey pubKey = PubKey(b'02773aca113a3217b67a95d5b78b69bb6386ed443ea5decf0ba92c00d179291921');
        // invK is the modular inverse of k, the ephemeral key
        int invK = 0xa2103f96554aba49bbf581738d3b5a38c5a44b6238ffb54cfcca65b8c87ddc08;
        // r is x coordinate of R, which is kG
        int r = 0x00f0fc43da25095812fcddde7d7cd353990c62b078e1493dc603961af25dfc6b60;
        // rBigEndian is the signed magnitude representation of r, in big endian
        bytes rBigEndian = b'00f0fc43da25095812fcddde7d7cd353990c62b078e1493dc603961af25dfc6b60';
        
        SigHashType sigHashType = SigHash.ANYONECANPAY | SigHash.ALL | SigHash.FORKID;

        // this ensures the preimage is for the current tx
        require(Tx.checkPreimageAdvanced(txPreimage, privKey, pubKey, invK, r, rBigEndian, sigHashType));

        bytes scriptCode = Util.scriptCode(txPreimage);
        int scriptLen = length(scriptCode);

        // the last OP_RETURN byte contains the application state, i.e., counter
        int counter = unpack(scriptCode[scriptLen - 1 :]);

        // Expect the counter to be incremented in the new transaction state
        bytes scriptCode_ = scriptCode[: scriptLen - 1] + num2bin(counter + 1, 1);

        bytes counterOutput = num2bin(amount, 8) + Util.writeVarint(scriptCode_);

        // Expect the additional CHANGE output
        bytes changeScript = Util.buildPublicKeyHashScript(changePKH);
        bytes changeOutput = num2bin(changeSats, 8) + Util.writeVarint(changeScript);

        bytes hashOutputs = Util.hashOutputs(txPreimage);
        // output: amount + scriptlen + script
        Sha256 hashOutputs_ = hash256(counterOutput + changeOutput);

        // ensure output matches what we expect:
        //     - amount is same as specified
        //     - output script is the same as scriptCode except the counter was incremented
        //     - expected CHANGE output script is there
        require(hashOutputs == hashOutputs_);
    }
}

sigHashType 参数设置为 SIGHASH_ANYONECANPAY

SigHashType sigHashType = SigHash.ANYONECANPAY | SigHash.ALL | SigHash.FORKID;

检查 sighashPreimage 与当前 transaction 是否一致:

require(Tx.checkPreimageAdvanced(txPreimage, privKey, pubKey, invK, r, rBigEndian, sigHashType));

设置合约的新计数值:

bytes scriptCode_ = scriptCode[: scriptLen - 1] + num2bin(counter + 1, 1);

增加找零 output:

bytes changeScript = Util.buildPublicKeyHashScript(changePKH);
bytes changeOutput = num2bin(changeSats, 8) + Util.writeVarint(changeScript);

这里是部署合约并重复调用 increment() 函数的代码。这里有一个调用次数从0增加到4的合约实例:0 -> 1 -> 2 -> 3 -> 4。注意每个 transaction 都有两个 input 和两个 output,不像旧版本计数器合约一样只有一个 input 和一个 output。第一个 input 和第一个 output 的币数是相同的,也就是说合约的余额没变(如下图中标注出的)。第二个input 和第二个 output 的差额提供了转账手续费。

advancedCounter
感谢 BitShizzle 的 Bill 实现了这个高级计数器合约。

讨论

在部署智能合约时,我们让合约的新余额等于合约的旧余额。只要有人愿意支付转账手续费来触发它,合约就可以一直运行。

有一个有趣的替代方案,让新合约的余额大于旧合约的余额,这样就相当于收取调用者的服务费来完成某些计算。大家也可以思考用这种方法能做出什么样的应用。

更新(202112)

现在除了使用上述 Tx.checkPreimageAdvanced 外,如果你的合约只是需要使用定制化的 SigHashType,也可以直接使用新的 Tx.checkPreimageSigHashType 函数,比如上述合约可以更新为以下代码:

/**
 * Demonstrates TxAdvanced, with external funding (additional input) and a change output
 */
contract AdvancedCounter {
    @state
    int counter;

    public function increment(SigHashPreimage txPreimage) {
        SigHashType sigHashType = SigHash.ANYONECANPAY | SigHash.SINGLE | SigHash.FORKID;
        // this ensures the preimage is for the current tx
        require(Tx.checkPreimageSigHashType(txPreimage, sigHashType));

        // update counter state
        this.counter++;
        
        bytes outputScript = this.getStateScript();
        int amount = SigHash.value(txPreimage);

        bytes counterOutput = Utils.buildOutput(outputScript, amount);

        // ensure output matches what we expect:
        // - amount is same as specified
        // - output script is the same as scriptCode except the counter was incremented
        require(hash256(counterOutput) == SigHash.hashOutputs(txPreimage));
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sCrypt Web3应用开发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值