sCrypt 合約開發除錯技巧: 定位及解決 checkSig / checkPreimage 異常

freedomhero發表於2020-10-04

在 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 中各項引數所計算出的結果不一致。

sighashPreimage

如上圖所示,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 的鎖定指令碼)是不一致的。

至此基本找到了問題的所在,鑑於近期的一些改動,推測是 entryMethodParamstxContext 的某些配置引數可能失效了。於是重新計算並且更新了 preimagetxContext.hextxContext.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 是否一致,可以使用 SigHashPreimagetoJSON() 方法檢視其內部細節,得到類似下面的結果:

{
  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 的計算,所以在排查問題時需要逐一對比確認是否一致。

相關文章