1.背景
工作中使用了BIP143的算法。在隔離見證中VERSION 0采用了BIP143的簽名驗證機制來提高效率,但BIP143S算法并沒有用在普通交易中。我們對普通交易的簽名和驗證進行了優化,使用BIP143的簽名和驗證機制,提高簽名和驗證效率。
1.1比特币的簽名算法
比特币采用了ECDSA(橢圓曲線數字簽名算法)的數字簽名算法,數字簽名算法在比特币中有三個用途:第一,簽名證明其為私鑰的擁有者,即該筆交易中支出資金的所有者。第二,授權證明具有不可否認性,即交易的不可否認。第三,簽名不可僞造,證明交易(或交易的具體部分)在簽名後不能被任何人修改。
數字簽名由兩部分組成:第一部分是使用私鑰(簽名密鑰)對消息(交易)的hash進行簽名,第二部分是允許任何人通過給定的公鑰和消息來驗證簽名,
簽名算法
比特币簽名算法如下:
Sig = Fsig( Fhash(m), dA )
其中:
dA 是簽名私鑰
m 是交易(或其部分)
Fhash是散列函數
Fsig是簽名算法
Sig是結果簽名
在整個簽名過程中,有兩個函數:Fhash和Fsig。
Fhash函數
Fhash函數用來生成交易的Hash,需要先将交易序列化,根據序列化後的二進制資料,使用SHA256函數來計算交易Hash。普通交易(單個輸入和單個輸出)過程如下:
交易的序列化:
1.nVersion交易版本
2.InputCount 輸入數量
3.Prevouts 對輸入UTXO進行序列化
4.OutputCount 輸出數量
5.outpoint 輸出的UTXO 進行序列化
6.nLocktime交易鎖定時間
7.Hash 将上述步驟産生的資料,進行兩次SHA256計算
Fsig函數
Fsig函數的簽名機制是基于橢圓曲線算法。在橢圓曲線中每次加密都會産生一個K值,根據K值,算法會生成一個臨時公私密鑰對(K,Q),由臨時公鑰Q的X坐标得到一個值R, 公式如下:
S=K-1 *(Hash(m) + dA *R) mod p
其中:
k是臨時私鑰
R是臨時公鑰的x坐标
dA是簽名私鑰
m是交易資料
p是橢圓曲線的主要順序
該函數會生成一個值S。
在橢圓曲線中每次加密都會産生一個K值,重用相同的K值會導緻私鑰暴露,K值是需要嚴格保密的,比特币采用FRC6979規範來保證确定性,通過SHA256保證K值的安全性。其簡單公式如下
K =SHA256(dA+HASH(m))
其中,
dA是私鑰,
m是消息。
最終簽名會産生 由(R和S)兩個值組成的簽名。
驗證簽名
驗證過程是簽名生成函數的倒數,其公式如下:
P=S-1 *Hash(m)*G +S-1*R*Qa
其中:
R和S是簽名值
Qa是使用者(簽名者)的公鑰
m是簽署的交易資料
G是橢圓曲線發生器點
從公式可以看出,根據消息(交易或其部分的Hash值)、簽名者的公鑰和簽名(R和S值),計算一個值P,該值是橢圓曲線上的一個點,如果該點的X坐标等于R,那麼簽名有效。
1.2 Bip143簡述
比特币有4個ECDSA(橢圓曲線數字簽名算法)的簽名驗證操作碼(sigops):CHECKSIG,CHECKSIGVERIFY,CHECKMULTISIG,CHECKMULTISIGVERIFY。一筆交易 的摘要資訊被兩次SHA256 。
比特币的原始數字簽名摘要算法存在至少兩個缺點:
驗證簽名資料的hash與交易的位元組大小成比例,驗證簽名的計算量是按照O(N2)的時間複雜度增長,驗證時間過長,BIP143通過引入一些可重用的“中間狀态”來優化摘要算法,使驗證簽名的時間複雜度變為O(n)。
原始簽名的第二個缺點:簽名中未包括交易輸入的比特币數量,對于網絡節點來說,這不是弱點,但對于離線交易簽名裝置(冷錢包),由于的輸入金額未知,造成無法計算所花費的确切金額和交易費用。BIP143在簽名中明确包含了每一筆交易輸入的金額。
BIP143定義了一個新的事務摘要算法,其規範如下
交易的序列化
1. nVersion交易版本(4位元組小端)
2. hashPrevouts 對所有輸入UTXO進行兩次SHA256計算的結果 (32位元組HASH)
3. hashSequence 對所有輸入nSequence進行兩次SHA256計算的結果(32位元組HASH)
4. outpoint 輸入的UTXO(32位元組HASH+ 4位元組小端)
5.輸入的scriptCode(序列化為CTxOuts中的腳本)
6.輸入所花費的數量(8位元組小端)
7.輸入的nSequence(4位元組 小端)
8. hashOutputs 對所有輸出進行兩次SHA256計算的結果(32位元組 HASH)
9. nLocktime交易鎖定時間(4位元組小端)
10. sighash簽名類型(4位元組小端)
以上條目中的 1,4,7,9,10與原始SIGHASH算法含有相同,原始的SIGHASH類型的語義保持不變。變動的有以下内容:
序列化的方式
所有SIGHASH都承諾簽名輸入所花費的金額
FindAndDelete簽名不适合scripteCode;
OP_CODESEPARATOR(S)後執行的最後OP_CODESEPARATOR不會從删除scriptCode(最後執行的OP_CODESEPARATOR任何腳本之前它總是删除);
SINGLE不送出輸入的索引。當ANYONECANPAY沒有設定,語義是不變的,hashPrevouts和outpoint一起隐式送出到輸入索引。當SINGLE使用ANYONECANPAY時,對簽名過的輸入和輸出成對出現,但對索引無限制。
2.BIP143簽名
在go語言中,我們使用了btcsuite庫來完成簽名,btcsuite庫是個完整的比特币代碼庫,可以編譯生成比特币全節點的程式,但這裡我們隻用btcsuite庫的公私鑰接口包、SHA接口包和signRFC6979簽名接口包。為省略篇幅,下面代碼未對錯誤進行處理。
2.1 生成交易HASH
生成交易資訊的hash值,交易中每個輸入,會生成一個對應hash值,如果交易中有多輸入,那麼會生成一個hash數組,數組中的每個hash對應交易中的一個輸入。
例如上圖的交易有兩筆交易輸入,每一筆都會生成一個hash,上圖中的交易會生成兩個hash。Fhash函數
CalcSignatureHash(script []byte, hashType SigHashType, tx *EMsgTx, idx int)
其中:
Script,pubscript 即輸入utxo的解鎖腳本
HashType,簽名方式或簽名類型
Tx,交易的具體資料
Idx,交易輸入的序号,即目前給交易的第幾筆輸入計算hash
下面為Fhash代碼。
1.Encode Version
binarySerializer.PutUint32(w, littleEndian, uint32(msg.Version))
2.Encode hashPrevouts
var hashPrevoutBuff bytes.Buffer
for _, ti := range msg.TxIn {
hashPrevoutBuff.Write(ti.PreviousOutPoint.Hash[:])
binarySerializer.PutUint32(&hashPrevoutBuff, littleEndian, ti.PreviousOutPoint.Index)
}
w.Write(chainhash.DoubleHashB(hashPrevoutBuff.Bytes()))
3.Encode HashSequence
var hashSequenceBuff bytes.Buffer
for _, ti := range msg.TxIn {
binarySerializer.PutUint32(&hashSequenceBuff, littleEndian, ti.Sequence)
}
w.Write(chainhash.DoubleHashB(hashSequenceBuff.Bytes()))
4.Encode Outpoint
w.Write(msg.TxIn[idx].PreviousOutPoint.Hash[:])
binarySerializer.PutUint32(w, littleEndian, op.Index)
5.Encode ScriptCode
WriteVarBytes(w, pver, msg.TxIn[idx].SignatureScript)
6. Encode Amount ,
binarySerializer.PutUint64(w, littleEndian, uint64(msg.Amount[idx]))
7. Encode nSequence
binarySerializer.PutUint32(w, littleEndian, msg.TxIn[idx].Sequence)
8.Encode hashOutputs
var hashOutputBuff bytes.Buffer
for _, to := range msg.TxOut {
binarySerializer.PutUint64(&hashOutputBuff, littleEndian, uint64(to.Value))
WriteVarBytes(&hashOutputBuff, pver, to.PkScript)
}
w.Write(chainhash.DoubleHashB(hashOutputBuff.Bytes()))
9.Encode Locktime
binarySerializer.PutUint32(w, littleEndian, msg.LockTime)
10.Encode Sighash Type
binarySerializer.PutUint32(w, littleEndian, uint32(SigHashType))
對于一個交易内有多筆UTXO輸入的情況,對每一筆輸入,依此調用上面步驟,生成一個hash數組。生成hash前,需要将其它輸入中包含的 “SigantureScript”字段内容清空,隻留目前輸入的“SigantureScript”字段内容,即下圖的“ScriptSig”字段。
每筆輸入UTXO對應花費的金額是不同的,在第六步需要注意 需要填入的是每筆的交易輸入的花費金額。
多筆輸入生成函數
func txHash(tx msgtx) ( *[][]byte)
代碼細節
for idx := range tx.TxIn {
hash, err := CalcSignatureHash(pkScript, SigHashAll|SigHashForkId, tx, idx)
sigHash = append(sigHash, hash)
}
循環調用Fhash函數(CalcSignatureHash)即可生成一個hash數組。
2.2對HASH簽名
在上面的步驟中生成了一個hash數組,對資料中每個hash對應于交易的每一筆輸入,采用signRFC6979簽名函數對hash進行簽名,這裡直接調用 btcsuite庫中的函數。
signRFC6979(PrivateKey, hash)
通過該函數,會生成SigantureScript,将該值付給交易中每筆輸入的SigantureScript字段。
2.3.多重簽名(Multisig)
多重簽名技術,簡單來說,就是花費一筆UTXO需要多個私鑰簽名才有效。腳本設定了一個條件,其中N個公鑰被記錄在腳本中,并且至少有M個必須提供簽名來解鎖資金。這也稱為M-N方案,其中N是密鑰的總數,M是驗證所需的簽名的數量。
以下go語言實作 一個基于P2SH(Pay-to-Script-Hash)腳本的2-2多重簽名。
2-2贖回腳本的生成函數代碼:
builder := txscript.NewScriptBuilder().AddInt64(int64(2))
builder.AddData(pk1)
builder.AddData(pk2)
builder.AddInt64(2)
builder.AddOp(txscript.OP_CHECKMULTISIG)
上面的函數生成了如下贖回腳本
2 <Partner1 Public Key> <Partner2 Public Key> 2 OP_C HECKMULTISIG
簽名函數
1. 根據交易TX,其包括輸入數組[]TxIn,生成交易HASH數組,此步驟與上面普通交易的步驟相同,直接調用上面普通交易的摘要生成函數。
func txHash(tx msgtx) ( *[][]byte)
該函數生成一個hash數組,即每個交易的輸入對應一個hash值。
2. 使用在贖回腳本中的第一個公鑰KEY,對應的私鑰進行簽名。簽名過程如普通交易。
signRFC6979(PrivateKey, hash)
簽名後生成了每筆輸入的簽名數組SignatureScriptArr1。根據這個數組中的簽名值,更新交易TX中每個輸入TxIn的"SigantureScript"字段。
3. 根據更新後的TX,再次調用 txHash函數,生成新的hash數組。
func txHash(tx msgtx) ( *[][]byte)
4. 使用在贖回腳本中的第二個公鑰KEY,對應的私鑰進行簽名。使用上一步驟中更新過的TX,生成每個輸入的hash并簽名
signRFC6979(PrivateKey, hash)
//合并第一個key生成的簽名、第二個key生成的簽名和贖回腳本
etxscript.EncodeSigScript(&(TX.TxIn[i].SignatureScript), &SigHash2, pkScript)
交易中有N筆交易,那麼上面步驟執行N次。
最後生成的資料内容如下圖所示
參考文獻
https://en.wikipedia.org/wiki/Digital_signature*
https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki
《OReilly.Mastering.Bitcoin.2nd.Edition》
http://www.8btc.com/rfc6979