教你用go语言实现比特币交易功能(Transaction)
来源:脚本之家
时间:2023-01-01 12:11:42 140浏览 收藏
本篇文章给大家分享《教你用go语言实现比特币交易功能(Transaction)》,覆盖了Golang的常见基础知识,其实一个语言的全部知识点一篇文章是不可能说完的,但希望通过这些问题,让读者对自己的掌握程度有一定的认识(B 数),从而弥补自己的不足,更好的掌握它。
比特币交易
交易(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 { 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在创建新的输出时,我们必须找到所有的为花费的输出,并且确保他们有足够的价值(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参考
https://jeiwan.cc/
今天关于《教你用go语言实现比特币交易功能(Transaction)》的内容介绍就到此结束,如果有什么疑问或者建议,可以在golang学习网公众号下多多回复交流;文中若有不正之处,也希望回复留言以告知!
-
168 收藏
-
165 收藏
-
473 收藏
-
377 收藏
-
384 收藏
-
246 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习