在前一篇文章中,我们介绍了一个强大的功能 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_TX 的 ECDSA签名。
Tx.checkPreimageAdvanced(txPreimage, privKey, pubKey, invK, r, rBigEndian, sigHashType)
- privKey 和 pubKey:ECDSA 密钥对
- invK:k 的模倒数,临时密钥
- r:R 的 x 坐标,通过 kG 计算得出
- rBigEndian:大端模式的有符号 r。严格来说,这个参数可以通过参数 r 算出来,但这需要在脚本中进行很多计算,成本较高。所以我们提供了该参数,这样就可以在链下计算了。
- sigHashType:SIGHASH 标识用来指定 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 的差额提供了转账手续费。
感谢 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));
}
}