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

Wanda ·
更新时间:2024-09-20
· 1394 次阅读

比特币交易

交易(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 要做的事情,随后,对于每个找到的输出,会创建一个引用该输出的输入。接下来,我们创建两个输出:

一个由接收者地址锁定。这是给其他地址实际转移的币。

一个由发送者地址锁定。这是一个找零。只有当未花费输出超过新交易所需时产生。记住:输出是不可再分的。

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/

到此这篇关于利用go语言实现比特币交易(Transaction)的文章就介绍到这了,更多相关go语言比特币交易内容请搜索软件开发网以前的文章或继续浏览下面的相关文章希望大家以后多多支持软件开发网!



GO 比特币交易 比特 go语言 比特币

需要 登录 后方可回复, 如果你还没有账号请 注册新账号