对交易进行签名是保证某人只能花费属于自己货币的唯一方法。签名无效代表交易无效,该交易就无法被加入到blockchain中。

对于实现交易签名,万事俱备只欠东风:基于什么数据进行签名。是基于部分交易进行签名还是对整个交易进行签名?选择数据非常重要。签名的数据中必须包含可以唯一标识该数据的信息。例如,仅仅对TXO的交易额进行签名,这就不合理,因为签名中没有考虑发送者和接受者信息。

交易会解锁之前的TXO,生成新的TXO,因此必须将对以下数据进行签名:

  1. 被解锁的TXO的公钥hash值。该值标识“发送者”
  2. 新生成的,被加锁的TXO的公钥hash值。该值标识“接收者”
  3. 新TXO的交易额

比特币的加锁、解锁逻辑存放在脚本中,分别存储在TXI的ScriptSig和TXO的ScriptPubKey字段。由于比特币支持多种类型的脚本,因此ScriptPubKey的整体也需要被签名

我们并不需要对TXI中的公钥进行签名。因此并非对“原始”交易进行签名,而是对TrimmedCopy交易进行签名。什么是TrimmedCopy交易?还是让我们来看代码理解下,从Sign方法开始:

TrimmedCopy交易的处理过程参见此文,可能有些过程,但是还是可以看出些端倪的。

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
    }
}

该方法接受两个参数:私钥以及所引用的之前交易的集合。对交易进行签名时,需要获取该交易所有TXI引用的TXO列表,因此需要存储这些TXO对应的交易。

我们一步一步来看:

if tx.IsCoinbase() {
    return
}

Coinbase交易没有真实的TXI,因此此交易不进行签名。

txCopy := tx.TrimmedCopy()

基于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
}

TrimmedCopy交易包括复制原始交易中所有的TXI和TXO,区别在于TrimmedCopy中TXI的SignaturePubKey被设置为nil。

接下来对TrimmedCopy交易中所有的TXI进行遍历:

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

对于每个TXI,为了确保万无一失,再次将Signature字段设置为nil,同时将PubKey设置为其所引用的TXO的PubKeyHash。此时,除了当前被处理的交易外,其他所有交易都是“空”交易(SignaturePubKey字段为nil)。由于比特币允许交易包含来自于不同地址的TXI,因此需要单独地对每个TXI进行签名,虽然对于我们的应用来说没必要这么做(在我们的应用中,一个交易包中的TXI均来自同一地址)。

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

Hash方法将交易序列化并通过SHA-256进行哈希。生成的哈希结果就是待签名的数据。此后,我们将PubKey字段重新设置为nil避免影响后续的迭代。下面是核心部分:

    r, s, err := ecdsa.Sign(rand.Reader, &privKey, txCopy.ID)
    signature := append(r.Bytes(), s.Bytes()...)

    tx.Vin[inID].Signature = signature

我们使用ECDSA签名算法通过私钥 privKeytxCopy.ID进行签名,生成一对数字序列。这些数字序列组合后放在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 := big.Int{}
        s := big.Int{}
        sigLen := len(vin.Signature)
        r.SetBytes(vin.Signature[:(sigLen / 2)])
        s.SetBytes(vin.Signature[(sigLen / 2):])

        x := big.Int{}
        y := 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
}

首先,生成TrimmedCopy交易:

txCopy := tx.TrimmedCopy()

接下来,创建椭圆曲线用于生成键值对:

curve := elliptic.P256()

对于每个TXI的签名进行验证:

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 := big.Int{}
    s := big.Int{}
    sigLen := len(vin.Signature)
    r.SetBytes(vin.Signature[:(sigLen / 2)])
    s.SetBytes(vin.Signature[(sigLen / 2):])

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

这里,。之前,将签名生成的两个字节序列组合生成TXI的Signature;将椭圆曲线的X,Y坐标点集合(其实也是两个字节序列)组合生成TXI的PubKey。现在,我们需要将TXI的SignaturePubKey中的数据进行“拆包”,用于crypto/ecdsa库进行验证使用。

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

return true

现在,需要一个根据交易ID获取交易的函数,由于需要访问blockchain,因此作为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
}

在交易加入到block前进行交易验证:

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 6
0000000f3dbb0ab6d56c4e4b9f7479afe8d5a5dad4d2a8823345a1a16cf3347b

Success!

$ blockchain_go getbalance -address 1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR
Balance of '1AmVdDvvQ977oVCpUqz7zAPUEiXKrX5avR': 4

$ blockchain_go getbalance -address 1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab
Balance of '1NE86r4Esjf53EL7fR86CsfTZpNN42Sfab': 6

一切OK!

NewUTXOTransaction中的以下语句注释,这样未被签名的交易不会被叫入到blockchain中。

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

results matching ""

    No results matching ""