📜 ⬆️ ⬇️

Blockchain on Go. Part 4: Transactions, Part 1

Hi, Habr! I present to your attention the translation of the article " Building Blockchain in Go. Part 4: Transactions 1 ".

Content

  1. Blockchain on Go. Part 1: Prototype
  2. Blockchain on Go. Part 2: Proof-of-Work
  3. Blockchain on Go. Part 3: Permanent Memory and Command Line Interface
  4. Blockchain on Go. Part 4: Transactions, Part 1
  5. Blockchain on Go. Part 5: Addresses
  6. Blockchain on Go. Part 6: Transactions, Part 2
  7. Blockchain on Go. Part 7: Network

Introduction


Transactions are the heart of Bitcoin, and the only purpose of a blockchain is to keep transactions in a safe and secure way so that no one can modify them after creation. In this article, we begin work on the implementation of the transaction mechanism. But since this is a rather large topic, I have broken it into two parts: in this part, we implement a general mechanism, and in the second part we will examine the rest of the functionality in detail.

In this article we will almost completely edit all of our previous code, so it makes no sense to describe every change, you can see all the changes here .
')

No spoons


If you once developed a web application, then for the implementation of payments, you probably created two of these tables in the database: and . The account stored information about the user, including his personal information and balance, and the transaction stores information about the transfer of money from one account to another. In Bitcoin, payments are made in a completely different way:

  1. No accounts.
  2. No balances.
  3. No addresses.
  4. No coins.
  5. No senders and recipients.

Since the blockchain is a public and open database, we do not want to keep confidential information about wallet owners. Coins are not kept on accounts. Transactions do not transfer money from one address to another. There are no fields or attributes that contain an account balance. There are only transactions. But what's inside?

Bitcoin transaction


A transaction is a combination of inputs and outputs:

 type Transaction struct { ID []byte Vin []TXInput Vout []TXOutput } 

The inputs of the new transaction refer to the outputs of the previous transaction (there is an exception, which we will discuss below). Outputs - the place where coins are stored. The following diagram shows the transaction relationship:



Notice:

  1. There are exits that are not connected to entrances.
  2. In a single transaction, inputs can refer to the outputs of multiple transactions.
  3. The input must always refer to the output.

In this article, we will use the words "money", "coins", "spend", "send", "account", etc. But there are no such concepts in Bitcoin. Transactions are simply a value blocked by a script that can only be unlocked by someone who has blocked it.

Transaction Outputs


Let's start with exits:

 type TXOutput struct { Value int ScriptPubKey string } 

In fact, these are the outputs that store “coins” (note the Value field above). Means are blocked by a special puzzle that is stored in the ScriptPubKey . Inside, Bitcoin uses a scripting language, Script , which is used to determine the logic for blocking and unlocking outputs. The language is quite primitive (this is done intentionally to avoid possible hacking), but we will not discuss it in detail. You can read more about him here .
In Bitcoin, the value field holds the amount of satoshi, not the amount of BTC. 1 satoshi = 0.00000001 BTC. Thus, it is the smallest unit of currency in Bitcoin (as, for example, a cent).
As we have no addresses, for the time being we will avoid all the logic associated with scripts. To begin, the ScriptPubKey will store an arbitrary string (user wallet address).
By the way, the presence of such a scripting language means that Bitcoin can be used as a platform for smart contracts.
One important thing you need to know about exits is that they are indivisible , which means that you cannot refer to a part of your value. When the output refers to a new transaction, it is consumed completely. And if its value is greater than required, a difference is generated and a new value is sent back to the sender. It’s like a real world situation when you pay, say, $ 5 dollars for what costs $ 1, and you get a change of $ 4.

Transaction inputs


Consider the input:

 type TXInput struct { Txid []byte Vout int ScriptSig string } 

As mentioned earlier, the input refers to the previous output: Txid stores the identifier of such a transaction, and Vout stores the exit index of the transaction. ScriptSig is a script that provides data that will be further used in the ScriptPubKey script. If the data is correct, the output can be unblocked, and its value can be used to generate new outputs; if not, the input cannot refer to the output. This mechanism ensures that users cannot spend coins belonging to other people.

Again, since we still have no addresses, ScriptSig save only an arbitrary user wallet address. We will create a public key and signature verification in the next article.

Summarize. Outputs are the place where “coins” are stored. Each exit has an unlock script that defines the logic to unlock the exit. Each new transaction must have at least one entry and exit. The entry refers to the result of a previous transaction and provides data (the ScriptSig field) that is used in the exit unlock script to unlock it and use its value to create new exits.

But what came first: the entrances or exits?

Egg


In Bitcoin, the egg appeared before the chicken. The inputs-referencing-outputs logic is a classic chicken or egg situation: the inputs produce the outputs, and the outputs allow you to create inputs. And in Bitcoin, exits always appear in front of the entrances.

When the miner starts mining a block, he adds a coinbase transaction to it. A coinbase transaction is a special type of transaction that does not require previously existing exits. He creates exits (i.e. "Coins") from nowhere. Egg without chicken. This is the reward that miners receive for the extraction of new blocks.

As you know, there is a genesis block at the beginning of the chain. It is this block that generates the very first output in the block chain. And no previous exits are required, since there are no previous transactions and there are no exits.

Let's create a coinbase transaction:

 func NewCoinbaseTX(to, data string) *Transaction { if data == "" { data = fmt.Sprintf("Reward to '%s'", to) } txin := TXInput{[]byte{}, -1, data} txout := TXOutput{subsidy, to} tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}} tx.SetID() return &tx } 

There is only one entry in the coinbase transaction. In our implementation, Txid empty, and Vout is -1. In addition, the coinbase transaction does not store the script in ScriptSig . Instead, arbitrary data is stored there.
In Bitcoin, the very first coinbase transaction contains the following message: “The Times 03 / Jan / 2009 Chancellor for brink of second bailout for banks”. You yourself can look at it .
subsidy is the amount of the reward. In Bitcoin, this number is not stored anywhere and is calculated only on the basis of the total number of blocks: the number of blocks is divided by 210,000 . Mining a block of genesis brings 50 BTC, and every 210,000 blocks the reward is halved. In our implementation, we will store the reward as a constant (at least for the time being).

Saving transactions in the chain


From this point on, each block should keep at least one transaction, and it should be impossible to mine the blocks without a transaction. Now, we will delete the date field from Block and instead we will now store transactions.

 type Block struct { Timestamp int64 Transactions []*Transaction PrevBlockHash []byte Hash []byte Nonce int } 

NewBlock and NewGenesisBlock should also be changed accordingly.

 func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block { block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0} ... } func NewGenesisBlock(coinbase *Transaction) *Block { return NewBlock([]*Transaction{coinbase}, []byte{}) } 

Now create function CreateBlockchain

 func CreateBlockchain(address string) *Blockchain { ... err = db.Update(func(tx *bolt.Tx) error { cbtx := NewCoinbaseTX(address, genesisCoinbaseData) genesis := NewGenesisBlock(cbtx) b, err := tx.CreateBucket([]byte(blocksBucket)) err = b.Put(genesis.Hash, genesis.Serialize()) ... }) ... } 

The function now accepts an address that will be rewarded for mining a genesis block.

Proof-of-work


The Proof-of-Work algorithm should consider transactions stored in the block to ensure consistency and reliability of the chain as a transaction repository. So now we have to change the ProofOfWork.prepareData method:

 func (pow *ProofOfWork) prepareData(nonce int) []byte { data := bytes.Join( [][]byte{ pow.block.PrevBlockHash, pow.block.HashTransactions(), // This line was changed IntToHex(pow.block.Timestamp), IntToHex(int64(targetBits)), IntToHex(int64(nonce)), }, []byte{}, ) return data } 

Instead of pow.block.Data we now add pow.block.HashTransactions () :

 func (b *Block) HashTransactions() []byte { var txHashes [][]byte var txHash [32]byte for _, tx := range b.Transactions { txHashes = append(txHashes, tx.ID) } txHash = sha256.Sum256(bytes.Join(txHashes, []byte{})) return txHash[:] } 

Again, we use hashing as a mechanism for providing a unique view of the data. We want all transactions in the block to be uniquely identified using a single hash. To achieve this, we get the hashes of each transaction, combine them and get the hash of the combined combinations.
Bitcoin uses a more complex technique: it presents all transactions contained in a block as a hash tree , and uses the root hash of the tree in the Proof-of-Work system. This approach allows you to quickly check if a block contains a specific transaction that has only a root hash and does not load all transactions.
Check the correctness of the work:

 $ blockchain_go createblockchain -address Ivan 00000093450837f8b52b78c25f8163bb6137caf43ff4d9a01d1b731fa8ddcc8a Done! 

Fine! We received our first award. But how do we check the balance?

Unspent exits


We need to find all unspent exits (UTXO). This means that these outputs did not refer to any inputs. In the diagram above, this is:

  1. tx0, output 1;
  2. tx1, output 0;
  3. tx3, output 0;
  4. tx4, output 0.

Of course, when we check the balance, we don’t need them all, only those that can be unlocked with the key we own are needed (currently we don’t have implemented keys and user addresses will be used instead). To begin with, let's define locking-unlocking methods for entrances and exits:

 func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool { return in.ScriptSig == unlockingData } func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool { return out.ScriptPubKey == unlockingData } 

Here we simply compare the fields from ScriptSig to unlockingData . In the next article we will improve them after we implement addresses based on private keys.

The next step is to search for transactions with unspent exits — this is more difficult:

 func (bc *Blockchain) FindUnspentTransactions(address string) []Transaction { var unspentTXs []Transaction spentTXOs := make(map[string][]int) bci := bc.Iterator() for { block := bci.Next() for _, tx := range block.Transactions { txID := hex.EncodeToString(tx.ID) Outputs: for outIdx, out := range tx.Vout { // Was the output spent? if spentTXOs[txID] != nil { for _, spentOut := range spentTXOs[txID] { if spentOut == outIdx { continue Outputs } } } if out.CanBeUnlockedWith(address) { unspentTXs = append(unspentTXs, *tx) } } if tx.IsCoinbase() == false { for _, in := range tx.Vin { if in.CanUnlockOutputWith(address) { inTxID := hex.EncodeToString(in.Txid) spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout) } } } } if len(block.PrevBlockHash) == 0 { break } } return unspentTXs } 

Because transactions are stored in blocks, we have to check every block in the chain. Let's start with exits:

 if out.CanBeUnlockedWith(address) { unspentTXs = append(unspentTXs, tx) } 


If an exit has been blocked at the same address, we are looking for unspent exits that we want. But before accepting it, we need to check if the input has already been specified at the output:

 if spentTXOs[txID] != nil { for _, spentOut := range spentTXOs[txID] { if spentOut == outIdx { continue Outputs } } } 

We skip those that are already referenced inputs (their values ​​were transferred to other outputs, so we can not count them). After checking the outputs, we collect all the inputs that can unblock the outputs blocked with the provided address (this does not apply to coinbase transactions, since they do not unblock the outputs):

 if tx.IsCoinbase() == false { for _, in := range tx.Vin { if in.CanUnlockOutputWith(address) { inTxID := hex.EncodeToString(in.Txid) spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout) } } } 

The function returns a list of transactions containing unspent exits. To calculate the balance, we need another function that takes transactions and returns only exits:

 func (bc *Blockchain) FindUTXO(address string) []TXOutput { var UTXOs []TXOutput unspentTransactions := bc.FindUnspentTransactions(address) for _, tx := range unspentTransactions { for _, out := range tx.Vout { if out.CanBeUnlockedWith(address) { UTXOs = append(UTXOs, out) } } } return UTXOs } 

Done! Now we are implementing the getbalance command getbalance

 func (cli *CLI) getBalance(address string) { bc := NewBlockchain(address) defer bc.db.Close() balance := 0 UTXOs := bc.FindUTXO(address) for _, out := range UTXOs { balance += out.Value } fmt.Printf("Balance of '%s': %d\n", address, balance) } 

The account balance is the sum of the values ​​of all unspent exits blocked by the account address.

Let's check our balance after mining a genesis block:

 $ blockchain_go getbalance -address Ivan Balance of 'Ivan': 10 

These are our first coins!

Sending Coins


Now we want to send some coins to someone else. To do this, we need to create a new transaction, put it into a block and process it. So far, we have implemented only the coinbase transaction (which is a special type of transaction), now we need a general transaction:

 func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction { var inputs []TXInput var outputs []TXOutput acc, validOutputs := bc.FindSpendableOutputs(from, 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) for _, out := range outs { input := TXInput{txID, out, from} inputs = append(inputs, input) } } // Build a list of outputs outputs = append(outputs, TXOutput{amount, to}) if acc > amount { outputs = append(outputs, TXOutput{acc - amount, from}) // a change } tx := Transaction{nil, inputs, outputs} tx.SetID() return &tx } 

Before creating new exits, we first need to find all the unspent exits and make sure that there are enough coins in them. This is the FindSpendableOutputs method. After that, for each output found, an input is created that references it. Then we create two outputs:

  1. The one that is blocked with the recipient address. This is the actual transfer of coins to another address.
  2. The one that is blocked with the sender address. This is the difference. It is created only when unspent exits matter more than what is required for a new transaction. Remember: exits are indivisible .

The FindSpendableOutputs method FindSpendableOutputs based on the FindUnspentTransactions method, which we defined earlier:

 func (bc *Blockchain) FindSpendableOutputs(address string, amount int) (int, map[string][]int) { unspentOutputs := make(map[string][]int) unspentTXs := bc.FindUnspentTransactions(address) accumulated := 0 Work: for _, tx := range unspentTXs { txID := hex.EncodeToString(tx.ID) for outIdx, out := range tx.Vout { if out.CanBeUnlockedWith(address) && accumulated < amount { accumulated += out.Value unspentOutputs[txID] = append(unspentOutputs[txID], outIdx) if accumulated >= amount { break Work } } } } return accumulated, unspentOutputs } 

The method performs a walk through all unspent transactions and accumulates their values. When the accumulated value is greater than or equal to the amount we want to transfer, the crawl stops and returns the accumulated values ​​and output indices grouped by transaction identifiers. We do not need to take more than we are going to spend.

Now we can change the Blockchain.MineBlock method:

 func (bc *Blockchain) MineBlock(transactions []*Transaction) { ... newBlock := NewBlock(transactions, lastHash) ... } 

Finally, create the send command:

 func (cli *CLI) send(from, to string, amount int) { bc := NewBlockchain(from) defer bc.db.Close() tx := NewUTXOTransaction(from, to, amount, bc) bc.MineBlock([]*Transaction{tx}) fmt.Println("Success!") } 

Sending coins means creating a transaction and adding it to the block chain by mining a block. But Bitcoin doesn't do it right away (as we do). Instead, it places all new transactions in the memory pool (or mempool), and when the miner is ready to mine the block, he takes all the transactions from the mempool and creates a candidate block. Transactions become confirmed only when the block containing them is extracted and added to the block chain.

Let's check how sending coins works:

 $ blockchain_go send -from Ivan -to Pedro -amount 6 00000001b56d60f86f72ab2a59fadb197d767b97d4873732be505e0a65cc1e37 Success! $ blockchain_go getbalance -address Ivan Balance of 'Ivan': 4 $ blockchain_go getbalance -address Pedro Balance of 'Pedro': 6 

Fine! Now let's create more transactions and make sure that sending from multiple outputs works correctly:

 $ blockchain_go send -from Pedro -to Helen -amount 2 00000099938725eb2c7730844b3cd40209d46bce2c2af9d87c2b7611fe9d5bdf Success! $ blockchain_go send -from Ivan -to Helen -amount 2 000000a2edf94334b1d94f98d22d7e4c973261660397dc7340464f7959a7a9aa Success! 

Now Helen's coins are blocked on two outs: one exit from Pedro and one from Ivan. Send to someone else:

 $ blockchain_go send -from Helen -to Rachel -amount 3 000000c58136cffa669e767b8f881d16e2ede3974d71df43058baaf8c069f1a0 Success! $ blockchain_go getbalance -address Ivan Balance of 'Ivan': 2 $ blockchain_go getbalance -address Pedro Balance of 'Pedro': 4 $ blockchain_go getbalance -address Helen Balance of 'Helen': 1 $ blockchain_go getbalance -address Rachel Balance of 'Rachel': 3 

Looks nice! Now let's test the exceptions:

 $ blockchain_go send -from Pedro -to Ivan -amount 5 panic: ERROR: Not enough funds $ blockchain_go getbalance -address Pedro Balance of 'Pedro': 4 $ blockchain_go getbalance -address Ivan Balance of 'Ivan': 2 

Conclusion


Phew! It was not easy, but now we have transactions! Although, some key features of a Bitcoin-like cryptocurrency are missing:

  1. Addresses While we do not have addresses on the basis of a private key.
  2. Awards Mine blocks is absolutely unprofitable!
  3. Utxo. Getting the balance requires scanning the entire block chain, which can take a very long time when there are so many blocks. In addition, it will take a very long time if we want to confirm subsequent transactions. UTXO is designed to solve these problems and to work quickly with transactions.
  4. Mempool. Transactions are stored here before they are packaged. In our current implementation, the block contains only one transaction, and it is very inefficient.

Links


  1. Full source codes
  2. Transaction
  3. Merkle tree
  4. Coinbase

Source: https://habr.com/ru/post/351752/


All Articles