diff --git a/README.md b/README.md index 87fc9f42..082f5eaa 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ A blockchain implementation in Go, as described in these articles: -1. [Basic Prototype](https://jeiwan.cc/posts/building-blockchain-in-go-part-1/) -2. [Proof-of-Work](https://jeiwan.cc/posts/building-blockchain-in-go-part-2/) -2. [Persistence and CLI](https://jeiwan.cc/posts/building-blockchain-in-go-part-3/) -3. [Transactions 1](https://jeiwan.cc/posts/building-blockchain-in-go-part-4/) -3. [Addresses](https://jeiwan.cc/posts/building-blockchain-in-go-part-5/) +1. [Basic Prototype](https://jeiwan.net/posts/building-blockchain-in-go-part-1/) +2. [Proof-of-Work](https://jeiwan.net/posts/building-blockchain-in-go-part-2/) +3. [Persistence and CLI](https://jeiwan.net/posts/building-blockchain-in-go-part-3/) +4. [Transactions 1](https://jeiwan.net/posts/building-blockchain-in-go-part-4/) +5. [Addresses](https://jeiwan.net/posts/building-blockchain-in-go-part-5/) +6. [Transactions 2](https://jeiwan.net/posts/building-blockchain-in-go-part-6/) diff --git a/block.go b/block.go index b98c79cc..65a0a6e5 100644 --- a/block.go +++ b/block.go @@ -2,7 +2,6 @@ package main import ( "bytes" - "crypto/sha256" "encoding/gob" "log" "time" @@ -36,15 +35,14 @@ func NewGenesisBlock(coinbase *Transaction) *Block { // HashTransactions returns a hash of the transactions in the block func (b *Block) HashTransactions() []byte { - var txHashes [][]byte - var txHash [32]byte + var transactions [][]byte for _, tx := range b.Transactions { - txHashes = append(txHashes, tx.Hash()) + transactions = append(transactions, tx.Serialize()) } - txHash = sha256.Sum256(bytes.Join(txHashes, []byte{})) + mTree := NewMerkleTree(transactions) - return txHash[:] + return mTree.RootNode.Data } // Serialize serializes the block diff --git a/blockchain.go b/blockchain.go index e2579feb..e00fb405 100644 --- a/blockchain.go +++ b/blockchain.go @@ -68,7 +68,7 @@ func CreateBlockchain(address string) *Blockchain { } // NewBlockchain creates a new Blockchain with genesis Block -func NewBlockchain(address string) *Blockchain { +func NewBlockchain() *Blockchain { if dbExists() == false { fmt.Println("No existing blockchain found. Create one first.") os.Exit(1) @@ -95,31 +95,6 @@ func NewBlockchain(address string) *Blockchain { return &bc } -// FindSpendableOutputs finds and returns unspent outputs to reference in inputs -func (bc *Blockchain) FindSpendableOutputs(pubKeyHash []byte, amount int) (int, map[string][]int) { - unspentOutputs := make(map[string][]int) - unspentTXs := bc.FindUnspentTransactions(pubKeyHash) - accumulated := 0 - -Work: - for _, tx := range unspentTXs { - txID := hex.EncodeToString(tx.ID) - - for outIdx, out := range tx.Vout { - if out.IsLockedWithKey(pubKeyHash) && accumulated < amount { - accumulated += out.Value - unspentOutputs[txID] = append(unspentOutputs[txID], outIdx) - - if accumulated >= amount { - break Work - } - } - } - } - - return accumulated, unspentOutputs -} - // FindTransaction finds a transaction by its ID func (bc *Blockchain) FindTransaction(ID []byte) (Transaction, error) { bci := bc.Iterator() @@ -141,9 +116,9 @@ func (bc *Blockchain) FindTransaction(ID []byte) (Transaction, error) { return Transaction{}, errors.New("Transaction is not found") } -// FindUnspentTransactions returns a list of transactions containing unspent outputs -func (bc *Blockchain) FindUnspentTransactions(pubKeyHash []byte) []Transaction { - var unspentTXs []Transaction +// FindUTXO finds all unspent transaction outputs and returns transactions with spent outputs removed +func (bc *Blockchain) FindUTXO() map[string]TXOutputs { + UTXO := make(map[string]TXOutputs) spentTXOs := make(map[string][]int) bci := bc.Iterator() @@ -164,17 +139,15 @@ func (bc *Blockchain) FindUnspentTransactions(pubKeyHash []byte) []Transaction { } } - if out.IsLockedWithKey(pubKeyHash) { - unspentTXs = append(unspentTXs, *tx) - } + outs := UTXO[txID] + outs.Outputs = append(outs.Outputs, out) + UTXO[txID] = outs } if tx.IsCoinbase() == false { for _, in := range tx.Vin { - if in.UsesKey(pubKeyHash) { - inTxID := hex.EncodeToString(in.Txid) - spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout) - } + inTxID := hex.EncodeToString(in.Txid) + spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout) } } } @@ -184,23 +157,7 @@ func (bc *Blockchain) FindUnspentTransactions(pubKeyHash []byte) []Transaction { } } - return unspentTXs -} - -// FindUTXO finds and returns all unspent transaction outputs -func (bc *Blockchain) FindUTXO(pubKeyHash []byte) []TXOutput { - var UTXOs []TXOutput - unspentTransactions := bc.FindUnspentTransactions(pubKeyHash) - - for _, tx := range unspentTransactions { - for _, out := range tx.Vout { - if out.IsLockedWithKey(pubKeyHash) { - UTXOs = append(UTXOs, out) - } - } - } - - return UTXOs + return UTXO } // Iterator returns a BlockchainIterat @@ -211,7 +168,7 @@ func (bc *Blockchain) Iterator() *BlockchainIterator { } // MineBlock mines a new block with the provided transactions -func (bc *Blockchain) MineBlock(transactions []*Transaction) { +func (bc *Blockchain) MineBlock(transactions []*Transaction) *Block { var lastHash []byte for _, tx := range transactions { @@ -251,6 +208,8 @@ func (bc *Blockchain) MineBlock(transactions []*Transaction) { if err != nil { log.Panic(err) } + + return newBlock } // SignTransaction signs inputs of a Transaction @@ -270,6 +229,10 @@ func (bc *Blockchain) SignTransaction(tx *Transaction, privKey ecdsa.PrivateKey) // VerifyTransaction verifies transaction input signatures func (bc *Blockchain) VerifyTransaction(tx *Transaction) bool { + if tx.IsCoinbase() { + return true + } + prevTXs := make(map[string]Transaction) for _, vin := range tx.Vin { diff --git a/cli.go b/cli.go index 863ffd11..72693ea2 100644 --- a/cli.go +++ b/cli.go @@ -17,6 +17,7 @@ func (cli *CLI) printUsage() { fmt.Println(" getbalance -address ADDRESS - Get balance of ADDRESS") fmt.Println(" listaddresses - Lists all addresses from the wallet file") fmt.Println(" printchain - Print all the blocks of the blockchain") + fmt.Println(" reindexutxo - Rebuilds the UTXO set") fmt.Println(" send -from FROM -to TO -amount AMOUNT - Send AMOUNT of coins from FROM address to TO") } @@ -35,8 +36,9 @@ func (cli *CLI) Run() { createBlockchainCmd := flag.NewFlagSet("createblockchain", flag.ExitOnError) createWalletCmd := flag.NewFlagSet("createwallet", flag.ExitOnError) listAddressesCmd := flag.NewFlagSet("listaddresses", flag.ExitOnError) - sendCmd := flag.NewFlagSet("send", flag.ExitOnError) printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError) + reindexUTXOCmd := flag.NewFlagSet("reindexutxo", flag.ExitOnError) + sendCmd := flag.NewFlagSet("send", flag.ExitOnError) getBalanceAddress := getBalanceCmd.String("address", "", "The address to get balance for") createBlockchainAddress := createBlockchainCmd.String("address", "", "The address to send genesis block reward to") @@ -75,6 +77,11 @@ func (cli *CLI) Run() { if err != nil { log.Panic(err) } + case "reindexutxo": + err := reindexUTXOCmd.Parse(os.Args[2:]) + if err != nil { + log.Panic(err) + } default: cli.printUsage() os.Exit(1) @@ -108,6 +115,10 @@ func (cli *CLI) Run() { cli.printChain() } + if reindexUTXOCmd.Parsed() { + cli.reindexUTXO() + } + if sendCmd.Parsed() { if *sendFrom == "" || *sendTo == "" || *sendAmount <= 0 { sendCmd.Usage() diff --git a/cli_createblockchain.go b/cli_createblockchain.go index 06ff1b1b..358e88b2 100644 --- a/cli_createblockchain.go +++ b/cli_createblockchain.go @@ -10,6 +10,10 @@ func (cli *CLI) createBlockchain(address string) { log.Panic("ERROR: Address is not valid") } bc := CreateBlockchain(address) - bc.db.Close() + defer bc.db.Close() + + UTXOSet := UTXOSet{bc} + UTXOSet.Reindex() + fmt.Println("Done!") } diff --git a/cli_getbalance.go b/cli_getbalance.go index 27cb55e3..814569df 100644 --- a/cli_getbalance.go +++ b/cli_getbalance.go @@ -9,13 +9,14 @@ func (cli *CLI) getBalance(address string) { if !ValidateAddress(address) { log.Panic("ERROR: Address is not valid") } - bc := NewBlockchain(address) + bc := NewBlockchain() + UTXOSet := UTXOSet{bc} defer bc.db.Close() balance := 0 pubKeyHash := Base58Decode([]byte(address)) pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-4] - UTXOs := bc.FindUTXO(pubKeyHash) + UTXOs := UTXOSet.FindUTXO(pubKeyHash) for _, out := range UTXOs { balance += out.Value diff --git a/cli_printchain.go b/cli_printchain.go index cd5c7a03..8541d2f6 100644 --- a/cli_printchain.go +++ b/cli_printchain.go @@ -6,7 +6,7 @@ import ( ) func (cli *CLI) printChain() { - bc := NewBlockchain("") + bc := NewBlockchain() defer bc.db.Close() bci := bc.Iterator() diff --git a/cli_reindexutxo.go b/cli_reindexutxo.go new file mode 100644 index 00000000..74d3b03d --- /dev/null +++ b/cli_reindexutxo.go @@ -0,0 +1,12 @@ +package main + +import "fmt" + +func (cli *CLI) reindexUTXO() { + bc := NewBlockchain() + UTXOSet := UTXOSet{bc} + UTXOSet.Reindex() + + count := UTXOSet.CountTransactions() + fmt.Printf("Done! There are %d transactions in the UTXO set.\n", count) +} diff --git a/cli_send.go b/cli_send.go index fb8117cb..456c2643 100644 --- a/cli_send.go +++ b/cli_send.go @@ -13,10 +13,15 @@ func (cli *CLI) send(from, to string, amount int) { log.Panic("ERROR: Recipient address is not valid") } - bc := NewBlockchain(from) + bc := NewBlockchain() + UTXOSet := UTXOSet{bc} defer bc.db.Close() - tx := NewUTXOTransaction(from, to, amount, bc) - bc.MineBlock([]*Transaction{tx}) + tx := NewUTXOTransaction(from, to, amount, &UTXOSet) + cbTx := NewCoinbaseTX(from, "") + txs := []*Transaction{cbTx, tx} + + newBlock := bc.MineBlock(txs) + UTXOSet.Update(newBlock) fmt.Println("Success!") } diff --git a/merkle_tree.go b/merkle_tree.go new file mode 100644 index 00000000..7a4156bd --- /dev/null +++ b/merkle_tree.go @@ -0,0 +1,65 @@ +package main + +import ( + "crypto/sha256" +) + +// MerkleTree represent a Merkle tree +type MerkleTree struct { + RootNode *MerkleNode +} + +// MerkleNode represent a Merkle tree node +type MerkleNode struct { + Left *MerkleNode + Right *MerkleNode + Data []byte +} + +// NewMerkleTree creates a new Merkle tree from a sequence of data +func NewMerkleTree(data [][]byte) *MerkleTree { + var nodes []MerkleNode + + if len(data)%2 != 0 { + data = append(data, data[len(data)-1]) + } + + for _, datum := range data { + node := NewMerkleNode(nil, nil, datum) + nodes = append(nodes, *node) + } + + for i := 0; i < len(data)/2; i++ { + var newLevel []MerkleNode + + for j := 0; j < len(nodes); j += 2 { + node := NewMerkleNode(&nodes[j], &nodes[j+1], nil) + newLevel = append(newLevel, *node) + } + + nodes = newLevel + } + + mTree := MerkleTree{&nodes[0]} + + return &mTree +} + +// NewMerkleNode creates a new Merkle tree node +func NewMerkleNode(left, right *MerkleNode, data []byte) *MerkleNode { + mNode := MerkleNode{} + + if left == nil && right == nil { + hash := sha256.Sum256(data) + mNode.Data = hash[:] + } else { + prevHashes := append(left.Data, right.Data...) + hash := sha256.Sum256(prevHashes) + mNode.Data = hash[:] + } + + mNode.Left = left + mNode.Right = right + + return &mNode +} diff --git a/merkle_tree_test.go b/merkle_tree_test.go new file mode 100644 index 00000000..acff5ffa --- /dev/null +++ b/merkle_tree_test.go @@ -0,0 +1,75 @@ +package main + +import ( + "encoding/hex" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewMerkleNode(t *testing.T) { + data := [][]byte{ + []byte("node1"), + []byte("node2"), + []byte("node3"), + } + + // Level 1 + + n1 := NewMerkleNode(nil, nil, data[0]) + n2 := NewMerkleNode(nil, nil, data[1]) + n3 := NewMerkleNode(nil, nil, data[2]) + n4 := NewMerkleNode(nil, nil, data[2]) + + // Level 2 + n5 := NewMerkleNode(n1, n2, nil) + n6 := NewMerkleNode(n3, n4, nil) + + // Level 3 + n7 := NewMerkleNode(n5, n6, nil) + + assert.Equal( + t, + "64b04b718d8b7c5b6fd17f7ec221945c034cfce3be4118da33244966150c4bd4", + hex.EncodeToString(n5.Data), + "Level 1 hash 1 is correct", + ) + assert.Equal( + t, + "08bd0d1426f87a78bfc2f0b13eccdf6f5b58dac6b37a7b9441c1a2fab415d76c", + hex.EncodeToString(n6.Data), + "Level 1 hash 2 is correct", + ) + assert.Equal( + t, + "4e3e44e55926330ab6c31892f980f8bfd1a6e910ff1ebc3f778211377f35227e", + hex.EncodeToString(n7.Data), + "Root hash is correct", + ) +} + +func TestNewMerkleTree(t *testing.T) { + data := [][]byte{ + []byte("node1"), + []byte("node2"), + []byte("node3"), + } + // Level 1 + n1 := NewMerkleNode(nil, nil, data[0]) + n2 := NewMerkleNode(nil, nil, data[1]) + n3 := NewMerkleNode(nil, nil, data[2]) + n4 := NewMerkleNode(nil, nil, data[2]) + + // Level 2 + n5 := NewMerkleNode(n1, n2, nil) + n6 := NewMerkleNode(n3, n4, nil) + + // Level 3 + n7 := NewMerkleNode(n5, n6, nil) + + rootHash := fmt.Sprintf("%x", n7.Data) + mTree := NewMerkleTree(data) + + assert.Equal(t, rootHash, fmt.Sprintf("%x", mTree.RootNode.Data), "Merkle tree root hash is correct") +} diff --git a/proofofwork.go b/proofofwork.go index fe555254..967a4bc1 100644 --- a/proofofwork.go +++ b/proofofwork.go @@ -12,7 +12,7 @@ var ( maxNonce = math.MaxInt64 ) -const targetBits = 24 +const targetBits = 16 // ProofOfWork represents a proof-of-work type ProofOfWork struct { diff --git a/transaction.go b/transaction.go index 90950cc2..67ba940c 100644 --- a/transaction.go +++ b/transaction.go @@ -173,7 +173,13 @@ func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool { // NewCoinbaseTX creates a new coinbase transaction func NewCoinbaseTX(to, data string) *Transaction { if data == "" { - data = fmt.Sprintf("Reward to '%s'", to) + 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)} @@ -185,7 +191,7 @@ func NewCoinbaseTX(to, data string) *Transaction { } // NewUTXOTransaction creates a new transaction -func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction { +func NewUTXOTransaction(from, to string, amount int, UTXOSet *UTXOSet) *Transaction { var inputs []TXInput var outputs []TXOutput @@ -195,7 +201,7 @@ func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transactio } wallet := wallets.GetWallet(from) pubKeyHash := HashPubKey(wallet.PublicKey) - acc, validOutputs := bc.FindSpendableOutputs(pubKeyHash, amount) + acc, validOutputs := UTXOSet.FindSpendableOutputs(pubKeyHash, amount) if acc < amount { log.Panic("ERROR: Not enough funds") @@ -222,7 +228,7 @@ func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transactio tx := Transaction{nil, inputs, outputs} tx.ID = tx.Hash() - bc.SignTransaction(&tx, wallet.PrivateKey) + UTXOSet.Blockchain.SignTransaction(&tx, wallet.PrivateKey) return &tx } diff --git a/transaction_output.go b/transaction_output.go index 4d70d74a..2ae68dec 100644 --- a/transaction_output.go +++ b/transaction_output.go @@ -1,6 +1,10 @@ package main -import "bytes" +import ( + "bytes" + "encoding/gob" + "log" +) // TXOutput represents a transaction output type TXOutput struct { @@ -27,3 +31,34 @@ func NewTXOutput(value int, address string) *TXOutput { return txo } + +// TXOutputs collects TXOutput +type TXOutputs struct { + Outputs []TXOutput +} + +// Serialize serializes TXOutputs +func (outs TXOutputs) Serialize() []byte { + var buff bytes.Buffer + + enc := gob.NewEncoder(&buff) + err := enc.Encode(outs) + if err != nil { + log.Panic(err) + } + + return buff.Bytes() +} + +// DeserializeOutputs deserializes TXOutputs +func DeserializeOutputs(data []byte) TXOutputs { + var outputs TXOutputs + + dec := gob.NewDecoder(bytes.NewReader(data)) + err := dec.Decode(&outputs) + if err != nil { + log.Panic(err) + } + + return outputs +} diff --git a/utxo_set.go b/utxo_set.go new file mode 100644 index 00000000..180ce04b --- /dev/null +++ b/utxo_set.go @@ -0,0 +1,193 @@ +package main + +import ( + "encoding/hex" + "log" + + "github.com/boltdb/bolt" +) + +const utxoBucket = "chainstate" + +// UTXOSet represents UTXO set +type UTXOSet struct { + Blockchain *Blockchain +} + +// FindSpendableOutputs finds and returns unspent outputs to reference in inputs +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 { + accumulated += out.Value + unspentOutputs[txID] = append(unspentOutputs[txID], outIdx) + } + } + } + + return nil + }) + if err != nil { + log.Panic(err) + } + + return accumulated, unspentOutputs +} + +// FindUTXO finds UTXO for a public key hash +func (u UTXOSet) FindUTXO(pubKeyHash []byte) []TXOutput { + var UTXOs []TXOutput + 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() { + outs := DeserializeOutputs(v) + + for _, out := range outs.Outputs { + if out.IsLockedWithKey(pubKeyHash) { + UTXOs = append(UTXOs, out) + } + } + } + + return nil + }) + if err != nil { + log.Panic(err) + } + + return UTXOs +} + +// CountTransactions returns the number of transactions in the UTXO set +func (u UTXOSet) CountTransactions() int { + db := u.Blockchain.db + counter := 0 + + err := db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(utxoBucket)) + c := b.Cursor() + + for k, _ := c.First(); k != nil; k, _ = c.Next() { + counter++ + } + + return nil + }) + if err != nil { + log.Panic(err) + } + + return counter +} + +// Reindex rebuilds the UTXO set +func (u UTXOSet) Reindex() { + db := u.Blockchain.db + bucketName := []byte(utxoBucket) + + err := db.Update(func(tx *bolt.Tx) error { + err := tx.DeleteBucket(bucketName) + if err != nil && err != bolt.ErrBucketNotFound { + log.Panic(err) + } + + _, err = tx.CreateBucket(bucketName) + if err != nil { + log.Panic(err) + } + + return nil + }) + if err != nil { + log.Panic(err) + } + + UTXO := u.Blockchain.FindUTXO() + + err = db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket(bucketName) + + for txID, outs := range UTXO { + key, err := hex.DecodeString(txID) + if err != nil { + log.Panic(err) + } + + err = b.Put(key, outs.Serialize()) + if err != nil { + log.Panic(err) + } + } + + return nil + }) +} + +// Update updates the UTXO set with transactions from the Block +// The Block is considered to be the tip of a blockchain +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) + } +} diff --git a/wallet.go b/wallet.go index 262a3665..31a22583 100644 --- a/wallet.go +++ b/wallet.go @@ -72,7 +72,7 @@ func checksum(payload []byte) []byte { firstSHA := sha256.Sum256(payload) secondSHA := sha256.Sum256(firstSHA[:]) - return secondSHA[len(secondSHA)-addressChecksumLen:] + return secondSHA[:addressChecksumLen] } func newKeyPair() (ecdsa.PrivateKey, []byte) {