foreword
Transactions are the core of Bitcoin, and the only purpose of the blockchain is to store transactions securely and reliably. In the blockchain, once a transaction is created, no one can modify or delete it. Today, we will begin to implement transactions. However, since trading is such a big topic, I will break it down into two parts: In today's section, we will implement the basic framework of trading. In the second part, we will continue to discuss some of its details.
Since Bitcoin adopts the UTXO model, not the account model, the concept of "balance" does not directly exist, and the balance needs to be obtained by traversing the entire transaction history.
Check out the transaction information in the image below at blockchain.info.
A transaction consists of a combination of inputs and outputs:
type Transaction struct {
ID []byte
Vin []TXInput
Vout []TXOutput
}
For each new transaction, its input will reference the output of the previous transaction (there is an exception here, the coinbase transaction), and the reference is the meaning of spending. The so-called reference to a previous output, that is, to include a previous output in the input of another transaction, is to spend the previous transaction output. The output of the transaction is where the coins are actually stored. The following diagram illustrates the interconnection between transactions:
Notice:
Some outputs are not linked to an input
The input of a transaction can refer to the output of multiple previous transactions
An input must refer to an output
Throughout this article, we will use words like "money", "coin", "spend", "send", "account", etc. But in Bitcoin, there is no such concept. Transactions are simply a script that locks values that can only be unlocked by the person who locked them.
Every Bitcoin transaction creates an output, and the output is recorded on the blockchain. Sending bitcoins to someone actually means creating a new UTXO and registering it with that person's address, which can be used by him.
transaction outputLet's start with the output:
type TXOutput struct {
Value int
ScriptPubKey string
}
The output mainly consists of two parts:
A certain amount of Bitcoin (
Value
)A lock script (
ScriptPubKey
) that must be unlocked to spend this money.
In fact, it is the output that stores "coins" (note, the Value
field above). The storage here refers to locking the output with a mathematical puzzle, and the puzzle is stored in ScriptPubKey
it. Internally, Bitcoin uses a scripting language called Script , which defines the logic for locking and unlocking outputs. While the language is fairly primitive (this is intentional to avoid potential hacking and abuse) and uncomplicated, we won't discuss its details here. You can find a detailed explanation here .
In Bitcoin, the
value
field stores the amount of satoshi , not the amount of BTC. One satoshi is equal to one millionth of BTC (0.00000001 BTC), which is the smallest monetary unit in Bitcoin (like a 1 cent coin).
Since addresses are not implemented yet, we will avoid full scripts involving logic for now. ScriptPubKey
An arbitrary string (user-defined wallet address) will be stored.
By the way, having such a scripting language also means that Bitcoin can actually be used as a smart contract platform.
A very important point about outputs is that they are indivisible . That is, you can't cite just one part of it. Either don't use it, if you want to use it, you must use it up at one time. When a new transaction references an output, that output must be spent in full. If its value is greater than required, then a change is generated and the change is returned to the sender. This is very similar to the real world scenario, when you want to pay, if something is worth $1 and you give a $5 bill, you get a $4 change.
send coinsNow, we want to send some coins to other people. To do this, we need to create a new transaction, put it in a block, and then mine the block. Before we only implemented the coinbase transaction (which is a special kind of transaction), now we need a general 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 outputs, we first have to find all unspent outputs and make sure they have enough value, and that's what FindSpendableOutputs
methods do. Then, for each found output, an input is created that refers to that output. Next, we create two outputs:
One is locked by the recipient address. This is the coins actually transferred to other addresses.
One is locked by the sender address. This is a change. Generated only when unspent output exceeds what is required for a new transaction. Remember: outputs are indivisible .
FindSpendableOutputs
The method is based on the previously defined FindUnspentTransactions
method:
func (bc *Blockchain) FindSpendableOutputs(address string, amount int) (int, map[string][]int) {
unspentOutputs := make(map[string][]int)
unspentTXs := bc.FindUnspentTransactions(address)
accumulated := 0Work:
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}
This method iterates over all unspent transactions and accumulates their values. When the accumulated value is greater than or equal to the value we want to transfer, it stops and returns the accumulated value along with the output index grouped by transaction ID. All we have to do is withdraw enough money to pay.
Now, we can modify the Blockchain.MineBlock
method:
func (bc *Blockchain) MineBlock(transactions []*Transaction) {
...
newBlock := NewBlock(transactions, lastHash)
...}
Finally, let's implement the send
method:
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 new transactions and packing them into the blockchain by mining new blocks. However, Bitcoin is not a chain of things that does these things at once (although our current implementation does). Instead, it puts all new transactions into a mempool, and when the miner is ready to mine a new block, it takes all the transactions from the mempool and creates a candidate block. Only after the block containing these transactions has been mined and added to the blockchain do the transactions in it begin to be confirmed.
Let's check if 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
very good! Now, let's create more transactions, making sure that sending coins from multiple outputs also works:
$ 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 locked in two outputs: one from Pedro and one from Ivan. Let's send them to others:
$ 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
It looks ok! Now, to test some failure cases:
$ 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
It wasn't easy, but now it's finally a deal! However, we are still missing some key features like Bitcoin:
address. We don't yet have a real address based on a private key.
reward. Now mining is definitely not profitable!
UTXO set. Getting the balance requires scanning the entire blockchain, which can take a long time when there are a lot of blocks. And, if we want to validate subsequent transactions, it also takes a long time. The UTXO set is to solve these problems and speed up transaction-related operations.
memory pool (mempool). Transactions are stored in the mempool before they are packaged into blocks. In our current implementation, a block contains only one transaction, which is rather inefficient.