【转载】区块链技术之比特币交易地址(下)

上半部分,已经说明比特币交易的地址交易过程,接下里来我们来完成下部分的操作,如何实现签名。

实现签名

交易必须被签名,因为这是比特币里面保证发送方不会花费属于其他人的币的唯一方式。如果一个签名是无效的,那么这笔交易就会被认为是无效的,因此,这笔交易也就无法被加到区块链中。

我们现在离实现交易签名还差一件事情:用于签名的数据。一笔交易的哪些部分需要签名?又或者说,要对完整的交易进行签名?选择签名的数据相当重要。因为用于签名的这个数据,必须要包含能够唯一识别数据的信息。比如,如果仅仅对输出值进行签名并没有什么意义,因为签名不会考虑发送方和接收方。

考虑到交易解锁的是之前的输出,然后重新分配里面的价值,并锁定新的输出,那么必须要签名以下数据:

  1. 存储在已解锁输出的公钥哈希。它识别了一笔交易的“发送方”。
  2. 存储在新的锁定输出里面的公钥哈希。它识别了一笔交易的“接收方”。
  3. 新的输出值。

在比特币中,锁定/解锁逻辑被存储在脚本中,它们被分别存储在输入和输出的 ScriptSigScriptPubKey 字段。由于比特币允许这样不同类型的脚本,它对 ScriptPubKey 的整个内容进行了签名。

可以看到,我们不需要对存储在输入里面的公钥签名。因此,在比特币里, 所签名的并不是一个交易,而是一个去除部分内容的输入副本,输入里面存储了被引用输出的 ScriptPubKey

获取修剪后的交易副本的详细过程在这里. 虽然它可能已经过时了,但是我并没有找到另一个更可靠的来源。

看着有点复杂,来开始写代码吧。先从 Sign 方法开始:

func (tx *Transaction) Sign(privKey ecdsa.PrivateKey, prevTXs map[string]Transaction) {
if tx.IsCoinbase() {
return
}
txCopy := tx.TrimmedCopy()
for inID, vin := range txCopy.Vin {
prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
txCopy.Vin[inID].Signature = nil
txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
txCopy.ID = txCopy.Hash()
txCopy.Vin[inID].PubKey = nil
r, s, err := ecdsa.Sign(rand.Reader, &privKey, txCopy.ID)
signature := append(r.Bytes(), s.Bytes()…)
tx.Vin[inID].Signature = signature }}

这个方法接受一个私钥和一个之前交易的 map。正如上面提到的,为了对一笔交易进行签名,我们需要获取交易输入所引用的输出,因为我们需要存储这些输出的交易。

来一步一步地分析该方法:

if tx.IsCoinbase() {
return}

coinbase 交易因为没有实际输入,所以没有被签名。

txCopy := tx.TrimmedCopy()

将会被签署的是修剪后的交易副本,而不是一个完整交易:

func (tx *Transaction) TrimmedCopy() Transaction {
var inputs []TXInput var outputs []TXOutput for _, vin := range tx.Vin {
inputs = append(inputs, TXInput{vin.Txid, vin.Vout, nil, nil})
}
for _, vout := range tx.Vout {
outputs = append(outputs, TXOutput{vout.Value, vout.PubKeyHash})
}
txCopy := Transaction{tx.ID, inputs, outputs}
return txCopy}

这个副本包含了所有的输入和输出,但是 TXInput.SignatureTXIput.PubKey 被设置为 nil

接下来,我们会迭代副本中每一个输入:

for inID, vin := range txCopy.Vin {
prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
txCopy.Vin[inID].Signature = nil
txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash

在每个输入中,Signature 被设置为 nil (仅仅是一个双重检验),PubKey 被设置为所引用输出的 PubKeyHash。现在,除了当前交易,其他所有交易都是“空的”,也就是说他们的 SignaturePubKey 字段被设置为 nil。因此,输入是被分开签名的,尽管这对于我们的应用并不十分紧要,但是比特币允许交易包含引用了不同地址的输入。

txCopy.ID = txCopy.Hash()
txCopy.Vin[inID].PubKey = nil

Hash 方法对交易进行序列化,并使用 SHA-256 算法进行哈希。哈希后的结果就是我们要签名的数据。在获取完哈希,我们应该重置 PubKey 字段,以便于它不会影响后面的迭代。

现在,关键点:

r, s, err := ecdsa.Sign(rand.Reader, &privKey, txCopy.ID)
signature := append(r.Bytes(), s.Bytes()…)
tx.Vin[inID].Signature = signature

我们通过 privKeytxCopy.ID 进行签名。一个 ECDSA 签名就是一对数字,我们对这对数字连接起来,并存储在输入的 Signature 字段。

现在,验证函数:

func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool {
txCopy := tx.TrimmedCopy()
curve := elliptic.P256()
for inID, vin := range tx.Vin {
prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
txCopy.Vin[inID].Signature = nil
txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
txCopy.ID = txCopy.Hash()
txCopy.Vin[inID].PubKey = nil
r := http://big.Int{}
s := http://big.Int{}
sigLen := len(vin.Signature)
r.SetBytes(vin.Signature[:(sigLen / 2)])
s.SetBytes(vin.Signature[(sigLen / 2):])
x := http://big.Int{}
y := http://big.Int{}
keyLen := len(vin.PubKey)
x.SetBytes(vin.PubKey[:(keyLen / 2)])
y.SetBytes(vin.PubKey[(keyLen / 2):])
rawPubKey := ecdsa.PublicKey{curve, &x, &y}
if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false {
return false
}
}
return true}

这个方法十分直观。首先,我们需要同一笔交易的副本:

txCopy := tx.TrimmedCopy()

然后,我们需要相同的区块用于生成密钥对:

curve := elliptic.P256()

接下来,我们检查每个输入中的签名:

for inID, vin := range tx.Vin {
prevTx := prevTXs[hex.EncodeToString(vin.Txid)]
txCopy.Vin[inID].Signature = nil
txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash
txCopy.ID = txCopy.Hash()
txCopy.Vin[inID].PubKey = nil

这个部分跟 Sign 方法一模一样,因为在验证阶段,我们需要的是与签名相同的数据。

r := http://big.Int{}
s := http://big.Int{}
sigLen := len(vin.Signature)
r.SetBytes(vin.Signature[:(sigLen / 2)])
s.SetBytes(vin.Signature[(sigLen / 2):])
x := http://big.Int{}
y := http://big.Int{}
keyLen := len(vin.PubKey)
x.SetBytes(vin.PubKey[:(keyLen / 2)])
y.SetBytes(vin.PubKey[(keyLen / 2):])

这里我们解包存储在 TXInput.SignatureTXInput.PubKey 中的值,因为一个签名就是一对数字,一个公钥就是一对坐标。我们之前为了存储将它们连接在一起,现在我们需要对它们进行解包在 crypto/ecdsa函数中使用。

rawPubKey := ecdsa.PublicKey{curve, &x, &y}
if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false {
return false
}}return true

在这里:我们使用从输入提取的公钥创建了一个 ecdsa.PublicKey,通过传入输入中提取的签名执行了 ecdsa.Verify。如果所有的输入都被验证,返回 true;如果有任何一个验证失败,返回 false.

现在,我们需要一个函数来获得之前的交易。由于这需要与区块链进行交互,我们将它放在了 Blockchain的方法里面:

func (bc *Blockchain) FindTransaction(ID []byte) (Transaction, error) {
bci := bc.Iterator()
for {
block := bci.Next()
for _, tx := range block.Transactions {
if bytes.Compare(tx.ID, ID) == 0 {
return *tx, nil
}
}
if len(block.PrevBlockHash) == 0 {
break
}
}
return Transaction{}, errors.New(“Transaction is not found”)}func (bc *Blockchain) SignTransaction(tx *Transaction, privKey ecdsa.PrivateKey) {
prevTXs := make(map[string]Transaction)
for _, vin := range tx.Vin {
prevTX, err := bc.FindTransaction(vin.Txid)
prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX }
tx.Sign(privKey, prevTXs)}func (bc *Blockchain) VerifyTransaction(tx *Transaction) bool {
prevTXs := make(map[string]Transaction)
for _, vin := range tx.Vin {
prevTX, err := bc.FindTransaction(vin.Txid)
prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX }
return tx.Verify(prevTXs)}

这几个比较简单:FindTransaction 通过 ID 找到一笔交易(这需要在区块链上迭代所有区块);SignTransaction 传入一笔交易,找到它引用的交易,然后对它进行签名;VerifyTransaction 做的是相同的事情,不过是对交易进行验证。

现在,我们需要实际签名和验证交易。签名在 NewUTXOTransaction 中进行:

func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {

tx := Transaction{nil, inputs, outputs}
tx.ID = tx.Hash()
bc.SignTransaction(&tx, wallet.PrivateKey)
return &tx}

在一笔交易被放入一个块之前进行验证:

func (bc *Blockchain) MineBlock(transactions []*Transaction) {
var lastHash []byte
for _, tx := range transactions {
if bc.VerifyTransaction(tx) != true {
log.Panic(“ERROR: Invalid transaction”)
}
}
…}

就是这些了!让我们再来检查一下所有内容:

$ blockchain_go createwallet
Your new address: 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR
$ blockchain_go createwallet
Your new address: 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab
$ blockchain_go createblockchain -address 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR
000000122348da06c19e5c513710340f4c307d884385da948a205655c6a9d008
Done!$ blockchain_go send -from 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR -to 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab -amount 60000000f3dbb0ab6d56c4e4b9f7479afe8d5a5dad4d2a8823345a1a16cf3347b
Success!$ blockchain_go getbalance -address 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR
Balance of ‘1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR’: 4$ blockchain_go getbalance -address 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab
Balance of ‘1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab’: 6

一切正常!

现在来注释掉 NewUTXOTransaction 里面的bc.SignTransaction(&tx, wallet.PrivateKey) 的调用,因为未签名的交易无法被打包:

func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {

tx := Transaction{nil, inputs, outputs}
tx.ID = tx.Hash()
// bc.SignTransaction(&tx, wallet.PrivateKey)
return &tx}

$ go install$ blockchain_go send -from 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR -to 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab -amount 1
2017/09/12 16:28:15 ERROR: Invalid transaction 

来源:https ://zhuanlan.zhihu.com/p/36574995