一、项目地图

小白入门:https://github.com/dukedaily/solidity-expert ,欢迎star转发,文末加V入群。

职场进阶: https://dukeweb3.com

image-20221108142642784

二、数字签名

1. 图示

image-20221108142720740

2. 需要签名的内容

- 签名需要什么?

  • 想要签名的数据
  • 私钥

- 验证需要什么?

  • 想要签名的数据
  • 数字签名
  • 公钥

- 具体签哪些数据?

所谓对交易签名,就是对交易的哈希值签名,但是这个交易中要包含以下数据(注意,是包含以下内容,而不是仅限于以下信息):

  • 欲使用utxo中的pubKeyHash(这描述了付款人)
  • 新生成utxo中的pubKeyHash(这描述了收款人)
  • 转账金额

最后,把签好的数据放在每一个input的sig中,由于每一笔交易都可能引用多个utxo(存在于多条交易中),所以我们要遍历所有的引用交易,并对它们逐个签名。

每个input的签名不同,虽然都是pubkeyHash,但是填充的位置不同,如:hello,h的位置变化,哈希值不同

3. 在交易中签名

交易创建完成之后,在写入区块之前,我们要对其进行签名,这样对端才能够校验,从而保证系统的安全性。

为此我们需要在Transaction中添加签名(sign)和验证(verify)的方法。

- sign

先把空函数写出来,Sign和SignTransaction,把关系交代清楚之后再进行内部实现

签名需要下面两个数据:

  • 想要签名的数据
  • 私钥

无论是签名还是校验,一定要格外注意处理挖矿交易

func (tx *Transaction) Sign(privateKey ecdsa.PrivateKey, prevTXs map[string]Transaction) {
    fmt.Println("begin Sign ...")
    //挖矿交易不需要签名
    if tx.IsCoinbase() {
        return
    }

    fmt.Printf("====== Sign tx : %x ======\n", tx.TXID)

    //确保引用的交易都是有效的,tx中的每个input都应该对应一条交易
    for _, input := range tx.TXInputs {
        if prevTXs[string(input.TXID)].TXID == nil {
            //if prevTXs[hex.EncodeToString(input.TXID)].TXID == nil {
            log.Panic("Previous txs are not valid!")
        }
    }

    //获取要签名的信息
    //我们对把当前的交易签名,然后存放到Signature中。
    //将当前的交易复制一份,然后做签名处理
    txCopy := tx.TrimmedCopy()

    //再次强调一下我们要签名的三部分:
    //- 欲使用的utxo中的pubKeyHash(这描述了付款人)
    //- 新生成的utxo中的pubKeyHash(这描述了收款人)
    //- 转账金额

    //注意,遍历的是txCopy,而不是tx本身
    for index, input := range txCopy.TXInputs {
        prevTX := prevTXs[string(input.TXID)]

        //使用input的PublicKey字段暂时存储一下这个想要解锁的utxo的公钥哈希
        //这里有个坑!!!
        //我一直使用input.PublicKey来赋值,但是其实range会复制一个新的变量,而不会修改你遍历的的txCopy, 这里一定要小心!!!,所以下面的这就要修改如下:
        //input.PublicKey = prevTX.TXOutputs[input.VoutIndex].PublicKeyHash
        txCopy.TXInputs[index].PublicKey = prevTX.TXOutputs[input.VoutIndex].PublicKeyHash
        //至此三个想要签名的数据都有了, 如何签名?签名一般对数据的哈希值进行签名
        //要签名的哈希存放在txCopy.TXID中
        txCopy.setHash()
        //一定要赋值为nil,下面交易签名还要用的,虽然不用这个input了,但是它会污染其他交易
        //在校验过程时,也会生成txCopy,设置nil双方才能与生成一致的数据
        txCopy.TXInputs[index].PublicKey = nil
        fmt.Printf("data to sign (TXID) : %x\n", txCopy.TXID)

        r, s, err := ecdsa.Sign(rand.Reader, &privateKey, txCopy.TXID)
        if err != nil {
            fmt.Println("ecdsa.Sign failed!")
            log.Panic(err)
        }

    //将得到的r,s拼接成[]byte,放在signature中
        signature := append(r.Bytes(), s.Bytes()...)
        //tx的索引和txCopy的是一致的
        tx.TXInputs[index].Signature = signature
        fmt.Printf("signature : %x\n", signature)
    }
}

TrimmedCopy函数

func (tx *Transaction) TrimmedCopy() Transaction {
    var inputs []TXInput
    var outputs []TXOutput

    for _, input := range tx.TXInputs {
        inputs = append(inputs, TXInput{input.TXID, input.VoutIndex, nil, nil})
    }

    for _, output := range tx.TXOutputs {
        outputs = append(outputs, TXOutput{output.Value, output.PublicKeyHash})
    }

    txCopy := Transaction{tx.TXID, inputs, outputs}
    return txCopy
}

在NewTransaction的最后,使用私钥对其进行签名。

签名函数由Transaction提供,但是签名动作由blockChain来实现,因为我们要遍历账本,

在SignTransaction内部再调用Sign函数。

image-20221108142857808

- SignTransaction

func (bc *BlockChain) SignTransaction(tx *Transaction, privateKey ecdsa.PrivateKey) {
    //寻找所引用的交易数组
    //调用tx.Sign进行签名

    prevTXs := make(map[string]Transaction)
    for _, input := range tx.TXInputs {
        prevTX, err := bc.FindTransaction(input.TXID)
        if err != nil {
            log.Panic(err)
        }

    //这里一定要注意,不是tx.TXID,小心掉坑
        prevTXs[string(prevTX.TXID)] = prevTX
    }

    tx.Sign(privateKey, prevTXs)
}

- FindTransaction

通过交易id返回交易结构

func (bc *BlockChain) FindTransaction(txid []byte) (Transaction, error) {
    it := NewBlockChainIterator(bc)
    for {
        block := it.GetBlockAndMoveLeft()
        for _, tx := range block.Transactions {
            if bytes.Compare(txid, tx.TXID) == 0 {
                return *tx, nil
            }
        }

        if len(block.PrevBlockHash) == 0 {
            break
        }
    }

    return Transaction{}, errors.New("Transaction not found!")
}

- 测试

image-20221108142908988

4.在挖矿时校验

当一笔交易发送到对端时,接收方在打包到自己的区块前,需要先对交易进行校验,从而保证

  1. 持有者花费的确实是自己的钱
  2. 交易确实是由私钥的持有者发起的

- 分析

校验函数(Verify)依然在Transaction结构中实现,需要三个数据:

  • 想要签名的数据
  • 数字签名
  • 公钥

代码如下:

校验函数原型如下,由于交易中已经存储了数字签名公钥,所以只需要将引用的交易传递进来即可(为了获取引用输出的公钥哈希)

func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool {
    return true
}

那这个交易由谁在何处调用呢?

答案是:我们要在挖矿之前进行验证,确保只有正确有效的交易才会被打包

func (bc *BlockChain) AddBlock(txs []*Transaction) {
    ...
    //创建新区块
    newBlock := NewBlock(txs, lastBlockHash)

    for _, tx := range txs {
        if !bc.VerifyTransaction(tx) {
            log.Panic("Error : invalid transaction found!")
        }
    }
    ...
}

交易函数很简单,同SignTransaction

func (bc *BlockChain) VerifyTransaction(tx *Transaction) bool {
    //必须加上这句校验,否则在FindTranction中就会报错,因为coinbase所引用的input的id为nil
    if tx.IsCoinbase() {
        fmt.Println("VerifyTransaction conibase found!")
        return true
    }

    prevTXs := make(map[string]Transaction)

    for _, input := range tx.TXInputs {
        fmt.Printf("input.TXID: %x\n", input.TXID)
        prevTX, err := bc.FindTransaction(input.TXID)
        if err != nil {
            log.Panic(err)
        }

        prevTXs[string(prevTX.TXID)] = prevTX
        //fmt.Printf("string(prevTX.TXID): %x", string(prevTX.TXID))
    }

    return tx.Verify(prevTXs)
}

编译一下,没有问题,接下来实现Verify函数

- Verify

func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool {
    fmt.Println("begin Verify ...")
    if tx.IsCoinbase() {
        return true
    }

    fmt.Printf("====== Verify tx : %x ======\n", tx.TXID)

    for _, input := range tx.TXInputs {
        if prevTXs[string(input.TXID)].TXID == nil {
            log.Panic("previous transaction is not valid!")
        }
    }

    //为了获取原始的签名数据
    txCopy := tx.TrimmedCopy()

    //获取签名的原始数据(一个哈希值),与签名时步骤一样
    //签名时对每一个引用的input都签名,校验也一定是对每一个input都校验
    //这里要注意for循环的对象,应该是原始的inputs结构,而不应该是copy过来的那个
  //与Sign时遍历的不同    
    for index, input := range tx.TXInputs {
        prevTX := prevTXs[string(input.TXID)]
        //这个可以为了确保为空再设置一次nil
        txCopy.TXInputs[index].Signature = nil
        txCopy.TXInputs[index].PublicKey = prevTX.TXOutputs[input.VoutIndex].PublicKeyHash
        txCopy.setHash()

    //这个也可以再次置空,不过不需要,因为每一个index对应的input只使用一次(这是错误的理解)
    //之所尚未出错,因为我们现在都只引用一个交易就完成了,没校验多个input
    //一定要设置,否则会影响其他交易的校验
        txCopy.TXInputs[index].PublicKey = nil

        //处理签名
        r := big.Int{}
        s := big.Int{}

        sigLen := len(input.Signature)

    //查看签名的原始数据
    fmt.Printf("data to Verify (TXID): %x\n", txCopy.TXID)
        //确保签名是一致的
        fmt.Printf("verify Signature: %x\n", input.Signature)

        r.SetBytes(input.Signature[:(sigLen / 2)])
        s.SetBytes(input.Signature[(sigLen / 2):])

        //处理公钥
        x := big.Int{}
        y := big.Int{}
        keyLen := len(input.PublicKey)
        x.SetBytes(input.PublicKey[:(keyLen / 2)])
        y.SetBytes(input.PublicKey[(keyLen / 2):])

        curve := elliptic.P256()
        rawPubKey := ecdsa.PublicKey{curve, &x, &y}

        if !ecdsa.Verify(&rawPubKey, txCopy.TXID, &r, &s) {
            return false
        }
    }

    return true
}

- 测试

发起一笔交易

image-20221108142923428

三、打印命令

- 结构体的String()

如果结构体实现了String()这个方法,那么fmt默认会调用String()。

package main

import "fmt"

type Test struct {
    s string
}

func (t *Test) String() string{
    return fmt.Sprintf("hello world: %s\n", t.s)
}

func main() {
    n := Test{s:"你好"}
    fmt.Println(&n) //hello world:
}

- 更新代码

func (cli *CLI) printChain() {

    //定义迭代器
    //it := NewBlockChainIterator(cli.bc)
    bc := GetBlockChainObj()
    it := NewBlockChainIterator(bc)
    for {

        block := it.GetBlockAndMoveLeft()

        fmt.Printf(" ============== Block %x============\n", block.Hash)
        pow := NewProofOfWork(block)
        fmt.Printf("IsValid : %v\n", pow.IsValid())

        for _, tx := range block.Transactions {
            fmt.Println(tx)
        }

        if len(block.PrevBlockHash) == 0 {
            fmt.Println("print over!")
            break
        }
    }
}

在Transaction结构中,添加如下函数:

func (tx Transaction) String() string {
    var lines []string

    lines = append(lines, fmt.Sprintf("--- Transaction %x:", tx.TXID))

    for i, input := range tx.TXInputs {

        lines = append(lines, fmt.Sprintf("     Input %d:", i))
        lines = append(lines, fmt.Sprintf("       TXID:      %x", input.TXID))
        lines = append(lines, fmt.Sprintf("       Out:       %d", input.VoutIndex))
        lines = append(lines, fmt.Sprintf("       Signature: %x", input.Signature))
        lines = append(lines, fmt.Sprintf("       PubKey:    %x", input.PublicKey))
    }

    for i, output := range tx.TXOutputs{
        lines = append(lines, fmt.Sprintf("     Output %d:", i))
        lines = append(lines, fmt.Sprintf("       Value:  %f", output.Value))
        lines = append(lines, fmt.Sprintf("       Script: %x", output.PublicKeyHash))
    }

    return strings.Join(lines, "\n")
}

- 测试

与比特币核心类似

image-20221108142942576

- Transaction 添加String()方法

func (tx Transaction) String() string {
    var lines []string

    lines = append(lines, fmt.Sprintf("--- Transaction %x:", tx.TxId))

    for i, input := range tx.TXInputs {

        lines = append(lines, fmt.Sprintf("     Input %d:", i))
        lines = append(lines, fmt.Sprintf("       TXID:      %x", input.TXID))
        lines = append(lines, fmt.Sprintf("       Out:       %d", input.Index))
        lines = append(lines, fmt.Sprintf("       Signature: %x", input.Signature))
        lines = append(lines, fmt.Sprintf("       PubKey:    %x", input.PubKey))
    }

    for i, output := range tx.TXOutputs {
        lines = append(lines, fmt.Sprintf("     Output %d:", i))
        lines = append(lines, fmt.Sprintf("       Value:  %f", output.Value))
        lines = append(lines, fmt.Sprintf("       Script: %x", output.PubKeyHash))
    }

    return strings.Join(lines, "\n")
}

- 增加printTx命令

func (bc *BlockChain) PrintTx() {

    it := bc.NewIterator()

    for {
        block := it.Next()
        fmt.Printf("+++++++++++++++++++++++++++++++++++++\n")

        for _, tx := range block.Transactions {
            fmt.Printf("tx : %v\n", tx)
        }

        if len(block.PrevBlockHash) == 0 {
            break
        }

    }
}