深入学习比特币脚本之 OP_PUSH_TX(2)

前一篇文章中,我们介绍了一个强大的功能 OP_PUSH_TX。它允许在比特币智能合约里访问当前 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 实现了这个高级计数器合约。

讨论

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

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

猜你喜欢

转载自blog.csdn.net/freedomhero/article/details/107333738