在 sCrypt 合约的开发调试过程中,最常见也最头疼的两个问题就是碰到 checkSig 和 checkPreimage 异常。虽然我们可以在 Debug 过程中定位到源码中错误的具体位置,但对于为什么失败以及如何修复总是感觉一头雾水。今天我们就来聊聊如何快速定位和修复这两类问题的一些技巧,希望能对大家有所帮助。
sCrypt boilerplate 项目中包含了一些 sCrypt 合约的具体示例代码且在不断更新中,所以有时可能会碰到配置失效导致无法正常完成 Debug 的情况。下面我们以该项目中的 tokenUtxo 合约为例来看看如何定位及解决这两类问题。
注意:本文中使用的 sCrypt 插件版本为 0.4.3。
checkPreimage 异常
首先看下 tokenUtxo.scrypt
的 Debug 启动配置(位于 .vscode/launch.json 中,为方便查看故省略了部分数值):
{
"type": "scrypt",
"request": "launch",
"name": "Debug tokenUtxo",
"program": "${workspaceFolder}/contracts/tokenUtxo.scrypt",
"constructorParams": "",
"entryMethod": "split",
"entryMethodParams": "Sig(b'304402200...'), PubKey(b'0251c866a29a93b6eb51197be1e9ccdcc5e822caa69c7593905347e3ec310bebad'), 60, 22222, PubKey(b'0291e61f25a92c94103f0f4ef1f70bf3582f44cff95d497ceb3efdb945f4ce3cbe'), 40, 22222, SigHashPreimage(b'0100000028bc...')",
"txContext": {
"hex": "01000000015884e5...",
"inputSatoshis": 100000,
"opReturn": "029a77564154c6ed13ffcc387342692480e7e15f2e3ad832cf2ac6de1c3ccf28230a5a",
"inputIndex": 0
}
}
上述配置指定了 Debug 的启动函数为 split
,并在 entryMethodParams
中指定了若干启动参数;同时在 txContext
中指定了 tx 相关的上下文参数。 当我们在 vscode 中启动这个配置准备进行 Debug 时,却发现 Debug Console 里输出了以下异常:
Execution failed with error SCRIPT_ERR_NULLFAIL.
Stacktrace:
/Users/hero/work/boilerplate/contracts/tokenUtxo.scrypt:14:in 'Token.split'
这里显示的异常位置是在 14 行,其代码是 require(Tx.checkPreimage(txPreimage));
,由此可以推断是 txPreimage 出了问题,但具体是什么原因呢?
在之前的文章中,我们介绍过 Sighash Preiamge,它被称为交易的原像,可由交易 tx 计算出来。这里的 Tx.checkPreimage
失败,说明在启动配置参数 entryMethodParams
中传入的数值与使用 txContext
中各项参数所计算出的结果不一致。
如上图所示,Sighash Preimage 由多个部分组合而成,如果两个原像不一致,一定是其中某些字段不相同。究竟是哪个字段的问题呢?让我们再来看看接下来的日志:
----- CheckPreimage Fail Hints Begin -----
You should check the differences in detail listed below:
Fields with difference | From preimage in entry method params | From preimage calculated with tx
md5(scriptCode) | 148dd2b3fcc09d6baf15c9fcf5d961d3 | 6393778445f442466414464ee3be7cc7
Preimage calculated with tx:
0100000028bce...
----- CheckPreimage Fail Hints End -----
这段日志为我们提供了关于 checkPreimage
异常的更多细节,主要是对比了前文提到的两个原像的具体差异。这里显示二者的 scriptCode
的 MD5 值有区别,即说明二者本身的 scriptCode
(对应 input 的锁定脚本)是不一致的。
至此基本找到了问题的所在,鉴于近期的一些改动,推测是 entryMethodParams
和 txContext
的某些配置参数可能失效了。于是重新计算并且更新了 preimage
、txContext.hex
、txContext.opReturn
等参数后,Debug 终于得到了正确的结果。
checkSig 异常
还有是一类常见的错误是 checkSig
异常,通常是由于签名问题导致的。这里我们可以通过随意修改下 senderSig
的参数值来模拟一个签名错误问题,之后再启动 Debug 就可以看到如下提示信息:
Execution failed with error SCRIPT_ERR_NULLFAIL.
Stacktrace:
/Users/hero/work/boilerplate/contracts/tokenUtxo.scrypt:25:in 'Token.split'
----- CheckSig Fail Hints Begin -----
You should make sure the following check points all passed:
1. private key used to sign should be corresponding to the public key 028f46cb8ec957dcda049ac549fc46d451e0095a5b6f95950bc58830a7dc21167c
2. the preimage of the tx to be signed should be 0100000028bcef7e73248aa273...
上述提示信息涵盖了解决签名错误时的主要检查点,即:
1. 确定生成签名所使用私钥是否正确;
2. 确认待签名 tx 的 preimage(根据 txContext 自动计算得到)与传入参数是否一致。
为了对比两个 preimage 是否一致,可以使用 SigHashPreimage
的 toJSON()
方法查看其内部细节,得到类似下面的结果:
{
nVersion: 1,
hashPrevouts: '1029c58f269f3a1f0165149921e7a726d13bf29883f80ec3bb08c75fabaa06ad',
hashSequence: '3bb13029ce7b1f559ef5e747fcac439f1455a2ec7c5f09b72290795e70665044',
outpoint: {
hash: '5884e5db9de218238671572340b207ee85b628074e7e467096c267266baf77a4',
index: 0
},
scriptCode: 'fd860f5101400...',
amount: 100000,
nSequence: 4294967295,
hashOutputs: '1029c58f269f3a1f0165149921e7a726d13bf29883f80ec3bb08c75fabaa06ad',
nLocktime: 0,
sighashType: 'SigHash.ALL | SigHash.FORKID'
}
这里的小技巧是:在生成输入参数 preimage 的地方插入一段代码,与上述异常提示中输出的 preimage 进行对比,进而找出二者可能存在的差异。如以下代码所示:
const { getPreimage, SigHashPreimage, signTx } = require('scryptlib');
...
const preimage = getPreimage(tx_, token.lockingScript.toASM(), inputSatoshis, inputIndex)
const sig = signTx(tx_, privKey, token.lockingScript.toASM(), inputSatoshis)
// compare two preimages for debugging purpose
const preimage2 = new SigHashPreimage('fd860f51014001760...'); // use hex from checkSig fail hints
console.log(preimage2.toString() === preimage.toString())
console.log(preimage.toJSON())
console.log(preimage2.toJSON())
这里需要再次提醒大家的是,启动配置 txContext
属性下的字段都会影响 preimage
的计算,所以在排查问题时需要逐一对比确认是否一致。