当前位置 博文首页 > 教你用go语言实现比特币交易功能(Transaction)

    教你用go语言实现比特币交易功能(Transaction)

    作者:辜智强 -buaa 时间:2021-05-29 17:52

    比特币交易

    交易(transaction)是比特币的核心所在,而区块链唯一的目的,也正是为了能够安全可靠地存储交易。在区块链中,交易一旦被创建,就没有任何人能够再去修改或是删除它。
    对于每一笔新的交易,它的输入会引用(reference)之前一笔交易的输出(这里有个例外,coinbase 交易),引用就是花费的意思。所谓引用之前的一个输出,也就是将之前的一个输出包含在另一笔交易的输入当中,就是花费之前的交易输出。交易的输出,就是币实际存储的地方。下面的图示阐释了交易之间的互相关联:

    这里写图片描述 

    注意:

    有一些输出并没有被关联到某个输入上

    一笔交易的输入可以引用之前多笔交易的输出

    一个输入必须引用一个输出

    贯穿本文,我们将会使用像“钱(money)”,“币(coin)”,“花费(spend)”,“发送(send)”,“账户(account)” 等等这样的词。但是在比特币中,其实并不存在这样的概念。交易仅仅是通过一个脚本(script)来锁定(lock)一些值(value),而这些值只可以被锁定它们的人解锁(unlock)。

    每一笔比特币交易都会创造输出,输出都会被区块链记录下来。给某个人发送比特币,实际上意味着创造新的 UTXO 并注册到那个人的地址,可以为他所用。
    交易的主函数:

    func (cli *CLI) send(from, to string, amount int, nodeID string, mineNow bool) {
        if !ValidateAddress(from) {   
            log.Panic("ERROR: Sender address is not valid")
        }
        if !ValidateAddress(to) {
            log.Panic("ERROR: Recipient address is not valid")
        }
        bc := NewBlockchain(nodeID)    //获取区块链实例
        UTXOSet := UTXOSet{bc}    //创建UTXO集
        defer bc.Db.Close()
        wallets, err := NewWallets(nodeID)
        if err != nil {
            log.Panic(err)
        }
        wallet := wallets.GetWallet(from)
        tx := NewUTXOTransaction(&wallet, to, amount, &UTXOSet)
        if mineNow {    
            cbTx := NewCoinbaseTX(from, "")
            txs := []*Transaction{cbTx, tx}
            newBlock := bc.MineBlock(txs)
            UTXOSet.Update(newBlock)
        } else {
            sendTx(knownNodes[0], tx)
        }
    
        fmt.Println("Success!")
    }

    我们从头分析整个交易过程,首先利用ValidateAddress()方法判断输入的地址是否为有效的比特币地址,然后从我们的blotDB数据库中获取blockchain实例(我们利用一个数据库实现区块链数据的存储,这里读者可以忽略),其中读取数据库的代码如下

    func NewBlockchain(nodeID string) *Blockchain {
        dbFile := fmt.Sprintf(dbFile, nodeID)
        if dbExists(dbFile) == false {
            fmt.Println("No existing blockchain found. Create one first.")
            os.Exit(1)
        }
        var tip []byte
        db, err := bolt.Open(dbFile, 0600, nil)    //打开数据库
        if err != nil {
            log.Panic(err)
        }
        err = db.Update(func(tx *bolt.Tx) error {
            b := tx.Bucket([]byte(blocksBucket))
            tip = b.Get([]byte("l"))  //读取最新的区块链
            return nil
        })
        if err != nil {
            log.Panic(err)
        }
        bc := Blockchain{tip, db}
        return &bc
    }

    其中我们的区块链的基本原型为

    type Blockchain struct {
        tip []byte
        Db  *bolt.DB
    }
    type Block struct {
        Timestamp     int64
        Transactions  []*Transaction
        PrevBlockHash []byte
        Hash          []byte
        Nonce         int
        Height        int
    }

    获取完成区块链实例后,我们创建出一个utxo集合,其数据结构为

    type UTXOSet struct {
        Blockchain *Blockchain
    }

    然后我们从钱包文件中获取我们的钱包集合(wallets),接着调用我们的转账函数。

    func NewUTXOTransaction(wallet *Wallet, to string, amount int, UTXOSet *UTXOSet) *Transaction {
        var inputs []TXInput
        var outputs []TXOutput
        pubKeyHash := HashPubKey(wallet.PublicKey)
        acc, validOutputs := UTXOSet.FindSpendableOutputs(pubKeyHash, amount)    //找到能够使用的输出
        if acc < amount {    //如果能够使用的输出小于目标值,则返回错误
            log.Panic("ERROR: Not enough funds")
        }
        // Build a list of inputs
        for txid, outs := range validOutputs {         
            txID, err := hex.DecodeString(txid)
            if err != nil {
                log.Panic(err)
            }
            for _, out := range outs {
                input := TXInput{txID, out, nil, wallet.PublicKey}
                inputs = append(inputs, input)
            }
        }
        // Build a list of outputs
        from := fmt.Sprintf("%s", wallet.GetAddress())
        outputs = append(outputs, *NewTXOutput(amount, to))    //创建新的交易输出
        if acc > amount {
            outputs = append(outputs, *NewTXOutput(acc-amount, from)) // a change    //找零输出
        }
        tx := Transaction{nil, inputs, outputs}
        tx.ID = tx.Hash()    //创建一笔交易
        UTXOSet.Blockchain.SignTransaction(&tx, wallet.PrivateKey)       //对交易签名
        return &tx
    }

    对于一笔交易来说,其数据结构为

    type Transaction struct {
        ID   []byte
        Vin  []TXInput
        Vout []TXOutput
    }
    type TXInput struct {
        Txid      []byte
        Vout      int
        Signature []byte
        PubKey    []byte
    }
    type TXOutput struct {
        Value      int
        PubKeyHash []byte
    }
    type UTXOSet struct {
        Blockchain *Blockchain
    }

    一笔交易来说,输出主要包含两部分: 一定量的比特币(Value), 一个锁定脚本(ScriptPubKey),要花这笔钱,必须要解锁该脚本。一个输入引用了之前交易的一个输出:Txid 存储的是之前交易的 ID,Vout 存储的是该输出在那笔交易中所有输出的索引(因为一笔交易可能有多个输出,需要有信息指明是具体的哪一个)Signature是签名,而Pubkey是公钥,两者保证了用户无法花费属于其他人的币。

    func HashPubKey(pubKey []byte) []byte {  // RIPEMD160(SHA256(PubKey))
        publicSHA256 := sha256.Sum256(pubKey)
        RIPEMD160Hasher := ripemd160.New()
        _, err := RIPEMD160Hasher.Write(publicSHA256[:])
        if err != nil {
            log.Panic(err)
        }
        publicRIPEMD160 := RIPEMD160Hasher.Sum(nil)
        return publicRIPEMD160
    }
    func (u UTXOSet) FindSpendableOutputs(pubkeyHash []byte, amount int) (int, map[string][]int) {
        unspentOutputs := make(map[string][]int)     //为输出开辟一块内存空间
        accumulated := 0       
        db := u.Blockchain.db             //获取存取区块链的数据库
        err := db.View(func(tx *bolt.Tx) error {               //读取数据库
            b := tx.Bucket([]byte(utxoBucket))
            c := b.Cursor()
            for k, v := c.First(); k != nil; k, v = c.Next() {               //遍历数据库
                txID := hex.EncodeToString(k)
                outs := DeserializeOutputs(v)
                for outIdx, out := range outs.Outputs {
                    if out.IsLockedWithKey(pubkeyHash) && accumulated < amount {          //如果能够解锁输出,代表utxo集中的输出是的所有者是该公钥所对应的人
                        accumulated += out.Value     //累加值
                        unspentOutputs[txID] = append(unspentOutputs[txID], outIdx)     //加到数组中
                    }
                }
            }
    
            return nil
        })
        if err != nil {
            log.Panic(err)
        }
        return accumulated, unspentOutputs
    }
    func (out *TXOutput) IsLockedWithKey(pubKeyHash []byte) bool {      //判断输出是否能够被某个公钥解锁
        return bytes.Compare(out.PubKeyHash, pubKeyHash) == 0
    } 
    func NewTXOutput(value int, address string) *TXOutput {
        txo := &TXOutput{value, nil}    //注册一个输出
        txo.Lock([]byte(address))    //设置输出的pubhashkey
        return txo
    }
    func (out *TXOutput) Lock(address []byte) {
        pubKeyHash := Base58Decode(address)
        pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-4]
        out.PubKeyHash = pubKeyHash
    }

    在创建新的输出时,我们必须找到所有的为花费的输出,并且确保他们有足够的价值(value),这就是FindSpendableOutputs 要做的事情,随后,对于每个找到的输出,会创建一个引用该输出的输入。接下来,我们创建两个输出:

    1. 一个由接收者地址锁定。这是给其他地址实际转移的币。
    2. 一个由发送者地址锁定。这是一个找零。只有当未花费输出超过新交易所需时产生。记住:输出是不可再分的。
    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)
            if err != nil {
                log.Panic(err)
            }
            prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
        }
        tx.Sign(privKey, prevTXs)
    }
    func (tx *Transaction) Sign(privKey ecdsa.PrivateKey, prevTXs map[string]Transaction) {//方法接受一个私钥和之前一个交易的map
        if tx.IsCoinbase() {
            return
        }//判断是是否为发币交易,因为发币交易没有输入,故不用进行签名
    
        for _, vin := range tx.Vin {
            if prevTXs[hex.EncodeToString(vin.Txid)].ID == nil {
                log.Panic("ERROR: Previous transaction is not correct")
            }
        }
    
        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
    //迭代副本中的每一个输入,在每个输入中,Pubkey 被设置为所引用输出的PubKeyHash
    /
            dataToSign := fmt.Sprintf("%x\n", txCopy)
            r, s, err := ecdsa.Sign(rand.Reader, &privKey, []byte(dataToSign))//我们通过private对txCopy进行签名将这串数字连接起来储存在signature中
            if err != nil {
                log.Panic(err)
            }
            signature := append(r.Bytes(), s.Bytes()...)
            tx.Vin[inID].Signature = signature
            txCopy.Vin[inID].PubKey = nil
        }
    }
    
    
    func (tx *Transaction) TrimmedCopy() Transaction {  
        var inputs []TXInput
        var outputs []TXOutput
    
        for _, vin := range tx.Vin {//将输入的TXInput.Signature 和TXIput.PubKey设置为空
            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
    }

    交易必须被签名,因为这是保证发送方不会花费其他人的币的唯一方式,如果一个签名是无效的,那么这笔交易也会被认为是无效的,因为这笔交易无法被加到区块链中。考虑到交易解锁的是之前的输出,然后重新分配里面的价值,并锁定新的输出,那么必须要签名一下的数据

    • 存储在已经解锁输出的公钥哈希,他识别了一笔交易的发送方
    • 存储在新的锁定输出里面的公钥哈希,他识别了一笔交易的接收方
    • 新的输出值

    因此,在比特币里,所签名的并不是一个交易,而是一个去除部分签名的输入的副本,输入里面存储了被引用输出的ScriptPubKey

    如果现在进行过挖矿

       cbTx := NewCoinbaseTX(from, "")
            txs := []*Transaction{cbTx, tx}
            newBlock := bc.MineBlock(txs)
            UTXOSet.Update(newBlock)
    
    
    
    func NewCoinbaseTX(to, data string) *Transaction {
        if data == "" {  //如果数据为空生成一个随机数据
            randData := make([]byte, 20)
            _, err := rand.Read(randData)
            if err != nil {
                log.Panic(err)
            }
            data = fmt.Sprintf("%x", randData)
        }//生成一笔挖矿交易
        txin := TXInput{[]byte{}, -1, nil, []byte(data)}
        txout := NewTXOutput(subsidy, to)
        tx := Transaction{nil, []TXInput{txin}, []TXOutput{*txout}}
        tx.ID = tx.Hash()
        return &tx
    }
    
    func (bc *Blockchain) MineBlock(transactions []*Transaction) *Block {   //开始挖矿
        var lastHash []byte
        var lastHeight int
        for _, tx := range transactions {
            // TODO: ignore transaction if it's not valid
            if bc.VerifyTransaction(tx) != true {
                log.Panic("ERROR: Invalid transaction")   //对打包在区块中的交易进行认证
            }
        }
    
        err := bc.db.View(func(tx *bolt.Tx) error {
            b := tx.Bucket([]byte(blocksBucket))
            lastHash = b.Get([]byte("l"))   //获取最新的一个块的hash值
            blockData := b.Get(lastHash)
            block := DeserializeBlock(blockData)  //将最新的一个块解序列
            lastHeight = block.Height
            return nil
        })
        if err != nil {
            log.Panic(err)
        }
        newBlock := NewBlock(transactions, lastHash, lastHeight+1)
        err = bc.db.Update(func(tx *bolt.Tx) error {    //更新区块链数据库
            b := tx.Bucket([]byte(blocksBucket))
            err := b.Put(newBlock.Hash, newBlock.Serialize())
            if err != nil {
                log.Panic(err)
            }
            err = b.Put([]byte("l"), newBlock.Hash)
            if err != nil {
                log.Panic(err)
            }
            bc.tip = newBlock.Hash
            return nil
        })
        if err != nil {
            log.Panic(err)
        }
        return newBlock
    }
    func (bc *Blockchain) VerifyTransaction(tx *Transaction) bool {
        if tx.IsCoinbase() {
            return true
        }
        prevTXs := make(map[string]Transaction)
        for _, vin := range tx.Vin {
            prevTX, err := bc.FindTransaction(vin.Txid)
            if err != nil {
                log.Panic(err)
            }
            prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX
        }
        return tx.Verify(prevTXs)
    }
    func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool {
        if tx.IsCoinbase() {   //判断是否为大笔交易
            return true
        }
        for _, vin := range tx.Vin {
            if prevTXs[hex.EncodeToString(vin.Txid)].ID == nil {
                log.Panic("ERROR: Previous transaction is not correct")   //判断输入地址的有效性
            }
        }
        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
            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):])
    //这里我们解包存储在 TXInput.Signature 和 TXInput.PubKey 中的值,因为一个签名就是一对数字,一个公钥就是一对坐标。我们之前为了存储将它们连接在一起,现在我们需要对它们进行解包在 crypto/ecdsa 函数中使用
            dataToVerify := fmt.Sprintf("%x\n", txCopy)
            rawPubKey := ecdsa.PublicKey{curve, &x, &y}
            if ecdsa.Verify(&rawPubKey, []byte(dataToVerify), &r, &s) == false {  //验证
                return false
            }
            txCopy.Vin[inID].PubKey = nil
        }
    
        return true
    }
    func NewBlock(transactions []*Transaction, prevBlockHash []byte, height int) *Block {//产生一个新的块
        block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0, height}//定义数据结构
        pow := NewProofOfWork(block)    //定义工作量证明的数据结构
        nonce, hash := pow.Run()    //挖矿
        block.Hash = hash[:]
        block.Nonce = nonce
        return block
    }
    func (pow *ProofOfWork) Run() (int, []byte) {
        var hashInt big.Int
        var hash [32]byte
        nonce := 0
        fmt.Printf("Mining a new block")
        for nonce < maxNonce {
            data := pow.prepareData(nonce)
            hash = sha256.Sum256(data)
            fmt.Printf("\r%x", hash)
            hashInt.SetBytes(hash[:])
            if hashInt.Cmp(pow.target) == -1 {
                break
            } else {
                nonce++
            }
        }
        fmt.Print("\n\n")
    
        return nonce, hash[:]
    }
    func (pow *ProofOfWork) prepareData(nonce int) []byte {
        data := bytes.Join(
            [][]byte{
                pow.block.PrevBlockHash,
                pow.block.HashTransactions(),
                IntToHex(pow.block.Timestamp),
                IntToHex(int64(targetBits)),
                IntToHex(int64(nonce)),
            },
            []byte{},
        )
    
        return data
    }
    func (u UTXOSet) Update(block *Block) {
        db := u.Blockchain.db
        err := db.Update(func(tx *bolt.Tx) error {
            b := tx.Bucket([]byte(utxoBucket))
            for _, tx := range block.Transactions {
                if tx.IsCoinbase() == false {
                    for _, vin := range tx.Vin {
                        updatedOuts := TXOutputs{}
                        outsBytes := b.Get(vin.Txid)
                        outs := DeserializeOutputs(outsBytes)
    
                        for outIdx, out := range outs.Outputs {
                            if outIdx != vin.Vout {
                                updatedOuts.Outputs = append(updatedOuts.Outputs, out)
                            }
                        }
    
                        if len(updatedOuts.Outputs) == 0 {
                            err := b.Delete(vin.Txid)
                            if err != nil {
                                log.Panic(err)
                            }
                        } else {
                            err := b.Put(vin.Txid, updatedOuts.Serialize())
                            if err != nil {
                                log.Panic(err)
                            }
                        }
    
                    }
                }
                newOutputs := TXOutputs{}
                for _, out := range tx.Vout {
                    newOutputs.Outputs = append(newOutputs.Outputs, out)
                }
                err := b.Put(tx.ID, newOutputs.Serialize())
                if err != nil {
                    log.Panic(err)
                }
            }
    
            return nil
        })
        if err != nil {
            log.Panic(err)
        }
    }

    参考

    https://jeiwan.cc/

    js
    下一篇:没有了