使用golang怎么实现一个比特币交易功能?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。比特币交易交易(transaction)是比特币的核心所在,而区块链唯一的目的,也正
使用golang怎么实现一个比特币交易功能?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。
交易(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) }}
golang可以做服务器端开发,但golang很适合做日志处理、数据打包、虚拟机处理、数据库代理等工作。在网络编程方面,它还广泛应用于WEB应用、api应用等领域。
关于使用golang怎么实现一个比特币交易功能问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注编程网GO频道了解更多相关知识。
--结束END--
本文标题: 使用golang怎么实现一个比特币交易功能
本文链接: https://lsjlt.com/news/276600.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-04-05
2024-04-05
2024-04-05
2024-04-04
2024-04-05
2024-04-05
2024-04-05
2024-04-05
2024-04-04
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0