diff --git a/README.md b/README.md index 32bfdf1..38c89b1 100644 --- a/README.md +++ b/README.md @@ -2,5 +2,7 @@ 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/) +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/) +2. [Persistence and CLI](https://jeiwan.net/posts/building-blockchain-in-go-part-3/) +3. [Transactions 1](https://jeiwan.net/posts/building-blockchain-in-go-part-4/) diff --git a/block.go b/block.go index 4656080..04d96cc 100644 --- a/block.go +++ b/block.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "crypto/sha256" "encoding/gob" "log" "time" @@ -10,7 +11,7 @@ import ( // Block keeps block headers type Block struct { Timestamp int64 - Data []byte + Transactions []*Transaction PrevBlockHash []byte Hash []byte Nonce int @@ -29,9 +30,22 @@ func (b *Block) Serialize() []byte { return result.Bytes() } +// HashTransactions returns a hash of the transactions in the block +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[:] +} + // NewBlock creates and returns Block -func NewBlock(data string, prevBlockHash []byte) *Block { - block := &Block{time.Now().Unix(), []byte(data), prevBlockHash, []byte{}, 0} +func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block { + block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0} pow := NewProofOfWork(block) nonce, hash := pow.Run() @@ -42,8 +56,8 @@ func NewBlock(data string, prevBlockHash []byte) *Block { } // NewGenesisBlock creates and returns genesis Block -func NewGenesisBlock() *Block { - return NewBlock("Genesis Block", []byte{}) +func NewGenesisBlock(coinbase *Transaction) *Block { + return NewBlock([]*Transaction{coinbase}, []byte{}) } // DeserializeBlock deserializes a block diff --git a/blockchain.go b/blockchain.go index edc29df..b9cfb42 100644 --- a/blockchain.go +++ b/blockchain.go @@ -1,16 +1,19 @@ package main import ( + "encoding/hex" "fmt" "log" + "os" "github.com/boltdb/bolt" ) const dbFile = "blockchain.db" const blocksBucket = "blocks" +const genesisCoinbaseData = "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks" -// Blockchain keeps a sequence of Blocks +// Blockchain implements interactions with a DB type Blockchain struct { tip []byte db *bolt.DB @@ -22,8 +25,8 @@ type BlockchainIterator struct { db *bolt.DB } -// AddBlock saves provided data as a block in the blockchain -func (bc *Blockchain) AddBlock(data string) { +// MineBlock mines a new block with the provided transactions +func (bc *Blockchain) MineBlock(transactions []*Transaction) { var lastHash []byte err := bc.db.View(func(tx *bolt.Tx) error { @@ -37,7 +40,7 @@ func (bc *Blockchain) AddBlock(data string) { log.Panic(err) } - newBlock := NewBlock(data, lastHash) + newBlock := NewBlock(transactions, lastHash) err = bc.db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blocksBucket)) @@ -57,7 +60,94 @@ func (bc *Blockchain) AddBlock(data string) { }) } -// Iterator ... +// FindUnspentTransactions returns a list of transactions containing unspent outputs +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 +} + +// FindUTXO finds and returns all unspent transaction outputs +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 +} + +// FindSpendableOutputs finds and returns unspent outputs to reference in inputs +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 +} + +// Iterator returns a BlockchainIterat func (bc *Blockchain) Iterator() *BlockchainIterator { bci := &BlockchainIterator{bc.tip, bc.db} @@ -85,8 +175,21 @@ func (i *BlockchainIterator) Next() *Block { return block } +func dbExists() bool { + if _, err := os.Stat(dbFile); os.IsNotExist(err) { + return false + } + + return true +} + // NewBlockchain creates a new Blockchain with genesis Block -func NewBlockchain() *Blockchain { +func NewBlockchain(address string) *Blockchain { + if dbExists() == 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 { @@ -95,29 +198,52 @@ func NewBlockchain() *Blockchain { err = db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blocksBucket)) - - if b == nil { - fmt.Println("No existing blockchain found. Creating a new one...") - genesis := NewGenesisBlock() - - b, err := tx.CreateBucket([]byte(blocksBucket)) - if err != nil { - log.Panic(err) - } - - err = b.Put(genesis.Hash, genesis.Serialize()) - if err != nil { - log.Panic(err) - } - - err = b.Put([]byte("l"), genesis.Hash) - if err != nil { - log.Panic(err) - } - tip = genesis.Hash - } else { - tip = b.Get([]byte("l")) - } + tip = b.Get([]byte("l")) + + return nil + }) + + if err != nil { + log.Panic(err) + } + + bc := Blockchain{tip, db} + + return &bc +} + +// CreateBlockchain creates a new blockchain DB +func CreateBlockchain(address string) *Blockchain { + if dbExists() { + fmt.Println("Blockchain already exists.") + 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 { + cbtx := NewCoinbaseTX(address, genesisCoinbaseData) + genesis := NewGenesisBlock(cbtx) + + b, err := tx.CreateBucket([]byte(blocksBucket)) + if err != nil { + log.Panic(err) + } + + err = b.Put(genesis.Hash, genesis.Serialize()) + if err != nil { + log.Panic(err) + } + + err = b.Put([]byte("l"), genesis.Hash) + if err != nil { + log.Panic(err) + } + tip = genesis.Hash return nil }) diff --git a/cli.go b/cli.go index 127150e..1270c6e 100644 --- a/cli.go +++ b/cli.go @@ -9,14 +9,34 @@ import ( ) // CLI responsible for processing command line arguments -type CLI struct { - bc *Blockchain +type CLI struct{} + +func (cli *CLI) createBlockchain(address string) { + bc := CreateBlockchain(address) + bc.db.Close() + fmt.Println("Done!") +} + +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) } func (cli *CLI) printUsage() { fmt.Println("Usage:") - fmt.Println(" addblock -data BLOCK_DATA - add a block to the blockchain") - fmt.Println(" printchain - print all the blocks of the blockchain") + fmt.Println(" getbalance -address ADDRESS - Get balance of ADDRESS") + fmt.Println(" createblockchain -address ADDRESS - Create a blockchain and send genesis block reward to ADDRESS") + fmt.Println(" printchain - Print all the blocks of the blockchain") + fmt.Println(" send -from FROM -to TO -amount AMOUNT - Send AMOUNT of coins from FROM address to TO") } func (cli *CLI) validateArgs() { @@ -26,19 +46,17 @@ func (cli *CLI) validateArgs() { } } -func (cli *CLI) addBlock(data string) { - cli.bc.AddBlock(data) - fmt.Println("Success!") -} - func (cli *CLI) printChain() { - bci := cli.bc.Iterator() + // TODO: Fix this + bc := NewBlockchain("") + defer bc.db.Close() + + bci := bc.Iterator() for { block := bci.Next() fmt.Printf("Prev. hash: %x\n", block.PrevBlockHash) - fmt.Printf("Data: %s\n", block.Data) fmt.Printf("Hash: %x\n", block.Hash) pow := NewProofOfWork(block) fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate())) @@ -50,18 +68,38 @@ func (cli *CLI) printChain() { } } +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!") +} + // Run parses command line arguments and processes commands func (cli *CLI) Run() { cli.validateArgs() - addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError) + getBalanceCmd := flag.NewFlagSet("getbalance", flag.ExitOnError) + createBlockchainCmd := flag.NewFlagSet("createblockchain", flag.ExitOnError) + sendCmd := flag.NewFlagSet("send", flag.ExitOnError) printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError) - addBlockData := addBlockCmd.String("data", "", "Block data") + getBalanceAddress := getBalanceCmd.String("address", "", "The address to get balance for") + createBlockchainAddress := createBlockchainCmd.String("address", "", "The address to send genesis block reward to") + sendFrom := sendCmd.String("from", "", "Source wallet address") + sendTo := sendCmd.String("to", "", "Destination wallet address") + sendAmount := sendCmd.Int("amount", 0, "Amount to send") switch os.Args[1] { - case "addblock": - err := addBlockCmd.Parse(os.Args[2:]) + case "getbalance": + err := getBalanceCmd.Parse(os.Args[2:]) + if err != nil { + log.Panic(err) + } + case "createblockchain": + err := createBlockchainCmd.Parse(os.Args[2:]) if err != nil { log.Panic(err) } @@ -70,20 +108,42 @@ func (cli *CLI) Run() { if err != nil { log.Panic(err) } + case "send": + err := sendCmd.Parse(os.Args[2:]) + if err != nil { + log.Panic(err) + } default: cli.printUsage() os.Exit(1) } - if addBlockCmd.Parsed() { - if *addBlockData == "" { - addBlockCmd.Usage() + if getBalanceCmd.Parsed() { + if *getBalanceAddress == "" { + getBalanceCmd.Usage() os.Exit(1) } - cli.addBlock(*addBlockData) + cli.getBalance(*getBalanceAddress) + } + + if createBlockchainCmd.Parsed() { + if *createBlockchainAddress == "" { + createBlockchainCmd.Usage() + os.Exit(1) + } + cli.createBlockchain(*createBlockchainAddress) } if printChainCmd.Parsed() { cli.printChain() } + + if sendCmd.Parsed() { + if *sendFrom == "" || *sendTo == "" || *sendAmount <= 0 { + sendCmd.Usage() + os.Exit(1) + } + + cli.send(*sendFrom, *sendTo, *sendAmount) + } } diff --git a/main.go b/main.go index 5ba110b..a48ad8e 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,6 @@ package main func main() { - bc := NewBlockchain() - defer bc.db.Close() - - cli := CLI{bc} + cli := CLI{} cli.Run() } diff --git a/proofofwork.go b/proofofwork.go index 57127bb..fe55525 100644 --- a/proofofwork.go +++ b/proofofwork.go @@ -34,7 +34,7 @@ func (pow *ProofOfWork) prepareData(nonce int) []byte { data := bytes.Join( [][]byte{ pow.block.PrevBlockHash, - pow.block.Data, + pow.block.HashTransactions(), IntToHex(pow.block.Timestamp), IntToHex(int64(targetBits)), IntToHex(int64(nonce)), @@ -51,7 +51,7 @@ func (pow *ProofOfWork) Run() (int, []byte) { var hash [32]byte nonce := 0 - fmt.Printf("Mining the block containing \"%s\"\n", pow.block.Data) + fmt.Printf("Mining a new block") for nonce < maxNonce { data := pow.prepareData(nonce) diff --git a/transaction.go b/transaction.go new file mode 100644 index 0000000..e5c51db --- /dev/null +++ b/transaction.go @@ -0,0 +1,111 @@ +package main + +import ( + "bytes" + "crypto/sha256" + "encoding/gob" + "encoding/hex" + "fmt" + "log" +) + +const subsidy = 10 + +// Transaction represents a Bitcoin transaction +type Transaction struct { + ID []byte + Vin []TXInput + Vout []TXOutput +} + +// IsCoinbase checks whether the transaction is coinbase +func (tx Transaction) IsCoinbase() bool { + return len(tx.Vin) == 1 && len(tx.Vin[0].Txid) == 0 && tx.Vin[0].Vout == -1 +} + +// SetID sets ID of a transaction +func (tx *Transaction) SetID() { + var encoded bytes.Buffer + var hash [32]byte + + enc := gob.NewEncoder(&encoded) + err := enc.Encode(tx) + if err != nil { + log.Panic(err) + } + hash = sha256.Sum256(encoded.Bytes()) + tx.ID = hash[:] +} + +// TXInput represents a transaction input +type TXInput struct { + Txid []byte + Vout int + ScriptSig string +} + +// TXOutput represents a transaction output +type TXOutput struct { + Value int + ScriptPubKey string +} + +// CanUnlockOutputWith checks whether the address initiated the transaction +func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool { + return in.ScriptSig == unlockingData +} + +// CanBeUnlockedWith checks if the output can be unlocked with the provided data +func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool { + return out.ScriptPubKey == unlockingData +} + +// NewCoinbaseTX creates a new 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 +} + +// NewUTXOTransaction creates a new 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) + if err != nil { + log.Panic(err) + } + + 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 +}