From 3d8e28a725dd00d078861433b50a70c81c90b659 Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Fri, 22 Sep 2017 14:22:11 -0500 Subject: [PATCH] Step 4 - Transactions --- block.go | 24 ++++-- blockchain.go | 223 +++++++++++++++++++++++++++++++++++++++---------- cli.go | 144 ++++++++++++++++++++++++------- main.go | 5 +- proof.go | 4 +- transaction.go | 138 ++++++++++++++++++++++++++++++ utils.go | 3 +- 7 files changed, 450 insertions(+), 91 deletions(-) create mode 100644 transaction.go diff --git a/block.go b/block.go index 8eec69b..d34e7ab 100644 --- a/block.go +++ b/block.go @@ -2,8 +2,8 @@ package main import ( "bytes" + "crypto/sha256" "encoding/gob" - "log" "math" "time" ) @@ -14,17 +14,17 @@ var ( type Block struct { Timestamp int64 - Data []byte + Transactions []*Transaction PrevBlockHash []byte Hash []byte Nonce int } // NewBlock returns a new block, ready to be added to the chain -func NewBlock(data string, prevBlockHash []byte) *Block { +func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block { block := &Block{ Timestamp: time.Now().Unix(), - Data: []byte(data), + Transactions: transactions, PrevBlockHash: prevBlockHash, Hash: []byte{}, } @@ -41,7 +41,7 @@ func (b *Block) Serialize() []byte { var result bytes.Buffer encoder := gob.NewEncoder(&result) if err := encoder.Encode(b); err != nil { - log.Panic(err) + exitWithError(err) } return result.Bytes() } @@ -50,7 +50,19 @@ func DeserializeBlock(d []byte) *Block { var block Block decoder := gob.NewDecoder(bytes.NewReader(d)) if err := decoder.Decode(&block); err != nil { - log.Panic(err) + exitWithError(err) } return &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[:] +} diff --git a/blockchain.go b/blockchain.go index a476273..b630d9e 100644 --- a/blockchain.go +++ b/blockchain.go @@ -1,108 +1,241 @@ package main import ( + "encoding/hex" "fmt" - "log" + "os" "github.com/boltdb/bolt" ) -var dbFile = "blockchain.db" - -const blocksBucket = "blocks" +const ( + dbFile = "blockchain.db" + blocksBucket = "blocks" + genesisCoinbaseData = "Arbitrary Genesis Data" +) type Blockchain struct { tip []byte db *bolt.DB } -type BlockchainIterator struct { - currentHash []byte - db *bolt.DB +// Return whether the database file already exists +func dbExists() bool { + if _, err := os.Stat(dbFile); os.IsNotExist(err) { + return false + } + return true } +// NewBlockchain creates a new Blockchain object from the DB func NewBlockchain() *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 { + exitWithError(err) + } err = db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blocksBucket)) + tip = b.Get([]byte("1")) - 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("1"), genesis.Hash) - if err != nil { - log.Panic(err) - } - tip = genesis.Hash - } else { - tip = b.Get([]byte("1")) - } return nil }) if err != nil { - log.Panic(err) + exitWithError(err) } - bc := Blockchain{tip, db} + bc := Blockchain{ + tip: tip, + db: db, + } + return &bc +} +// CreateBlockchain creates the blockchain DB and generate a new genesis block +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 { + exitWithError(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 { + exitWithError(err) + } + + err = b.Put(genesis.Hash, genesis.Serialize()) + if err != nil { + exitWithError(err) + } + + err = b.Put([]byte("1"), genesis.Hash) + if err != nil { + exitWithError(err) + } + tip = genesis.Hash + return nil + }) + if err != nil { + exitWithError(err) + } + + bc := Blockchain{ + tip: tip, + db: db, + } return &bc } // NewGenesisBlock creates a new Genesis Block to start the blockchain -func NewGenesisBlock() *Block { - return NewBlock("Genesis Block", []byte{}) +func NewGenesisBlock(coinbase *Transaction) *Block { + return NewBlock([]*Transaction{coinbase}, []byte{}) } -// AddBlock adds a block to the blockchain -func (bc *Blockchain) AddBlock(data string) { +// 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 +} + +// 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 { b := tx.Bucket([]byte(blocksBucket)) lastHash = b.Get([]byte("1")) + return nil }) if err != nil { - log.Panic(err) + exitWithError(err) } - newBlock := NewBlock(data, lastHash) - + newBlock := NewBlock(transactions, lastHash) err = bc.db.Update(func(tx *bolt.Tx) error { - var err error b := tx.Bucket([]byte(blocksBucket)) - if err = b.Put(newBlock.Hash, newBlock.Serialize()); err != nil { - log.Panic(err) + err := b.Put(newBlock.Hash, newBlock.Serialize()) + if err != nil { + exitWithError(err) } - if err = b.Put([]byte("1"), newBlock.Hash); err != nil { - log.Panic(err) + + err = b.Put([]byte("1"), newBlock.Hash) + if err != nil { + exitWithError(err) } + bc.tip = newBlock.Hash return nil }) if err != nil { - log.Panic(err) + exitWithError(err) } } +// Iterating the Blockchain +type BlockchainIterator struct { + currentHash []byte + db *bolt.DB +} + +// Iterator returns a blockchain iterator func (bc *Blockchain) Iterator() *BlockchainIterator { bci := &BlockchainIterator{bc.tip, bc.db} return bci } +// next returns the next block (starting from the tip) func (bci *BlockchainIterator) Next() *Block { var block *Block @@ -113,7 +246,7 @@ func (bci *BlockchainIterator) Next() *Block { return nil }) if err != nil { - log.Panic(err) + exitWithError(err) } bci.currentHash = block.PrevBlockHash diff --git a/cli.go b/cli.go index 315b8b9..4ae6ab3 100644 --- a/cli.go +++ b/cli.go @@ -7,66 +7,99 @@ import ( "strconv" ) -type CLI struct { - bc *Blockchain -} +const ( + flagBalance = "balance" + flagCreate = "create" + flagSend = "send" + flagLedger = "ledger" +) -func (cli *CLI) printUsage() { - fmt.Println("Usage:") - fmt.Println("\taddblock -data BLOCK_DATA\tadd a block to the blockchain") - fmt.Println("\tprintchain\t\t\tprint all the blocks of the blockchain") -} - -func (cli *CLI) validateArgs() { - if len(os.Args) < 2 { - cli.printUsage() - os.Exit(1) - } -} +type CLI struct{} +// Run parses command line arguments and processes commands func (cli *CLI) Run() { cli.validateArgs() // Valid CLI flags - addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError) - printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError) - // CLI Flag Data - addBlockData := addBlockCmd.String("data", "", "Block data") + getBalanceCmd := flag.NewFlagSet(flagBalance, flag.ExitOnError) + createBlockchainCmd := flag.NewFlagSet(flagCreate, flag.ExitOnError) + sendCmd := flag.NewFlagSet(flagSend, flag.ExitOnError) + printChainCmd := flag.NewFlagSet(flagLedger, flag.ExitOnError) + + getBalanceAddress := getBalanceCmd.String("address", "", "The address to get the balance of") + createBlockchainAddress := createBlockchainCmd.String("address", "", "The address to send the 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": - addBlockCmd.Parse(os.Args[2:]) - case "printchain": - printChainCmd.Parse(os.Args[2:]) + case flagBalance: + if err := getBalanceCmd.Parse(os.Args[2:]); err != nil { + exitWithError(err) + } + case flagCreate: + if err := createBlockchainCmd.Parse(os.Args[2:]); err != nil { + exitWithError(err) + } + case flagLedger: + if err := printChainCmd.Parse(os.Args[2:]); err != nil { + exitWithError(err) + } + case flagSend: + if err := sendCmd.Parse(os.Args[2:]); err != nil { + exitWithError(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) + } } -func (cli *CLI) addBlock(data string) { - cli.bc.AddBlock(data) - fmt.Println("Success!") +// createBlockchain creates a new blockchain with a genesis block +func (cli *CLI) createBlockchain(address string) { + bc := CreateBlockchain(address) + bc.db.Close() + fmt.Println("Done!") } +// printChain outputs the entire ledger func (cli *CLI) printChain() { - bci := cli.bc.Iterator() + 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())) @@ -77,3 +110,50 @@ func (cli *CLI) printChain() { } } } + +// getBalance retrieves the transaction balance for an address +func (cli *CLI) getBalance(address string) { + bc := NewBlockchain() + 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) +} + +// send Triggers a transaction exchange +func (cli *CLI) send(from, to string, amount int) { + bc := NewBlockchain() + defer bc.db.Close() + + tx := NewUTXOTransaction(from, to, amount, bc) + bc.MineBlock([]*Transaction{tx}) + fmt.Println("Success!") +} + +// printUsage prints the usage of the cli app +func (cli *CLI) printUsage() { + fmt.Println("Usage:") + fmt.Println("\t" + flagBalance + " -address ADDRESS\tGet balance of ADDRESS") + fmt.Println("\t" + flagCreate + " -address ADDRESS\tCreate a blockchain and send genesis block reward to ADDRESS") + fmt.Println("\t" + flagLedger + "\t\t\tprint all the blocks of the blockchain") + fmt.Println("\t" + flagSend + " -from FROM -to TO -amount AMOUNT\tSend AMOUNT of coins from FROM address to TO address") +} + +// validateArgs just makes sure that we've got a couple arguments +func (cli *CLI) validateArgs() { + if len(os.Args) < 2 { + cli.printUsage() + os.Exit(1) + } +} + +func exitWithError(err error) { + fmt.Println(err) + os.Exit(1) +} 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/proof.go b/proof.go index 0542f23..e58ad1f 100644 --- a/proof.go +++ b/proof.go @@ -27,7 +27,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(proofTargetBits)), IntToHex(int64(nonce)), @@ -42,7 +42,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) hash = sha256.Sum256(data) diff --git a/transaction.go b/transaction.go new file mode 100644 index 0000000..989ee7e --- /dev/null +++ b/transaction.go @@ -0,0 +1,138 @@ +package main + +import ( + "bytes" + "crypto/sha256" + "encoding/gob" + "encoding/hex" + "fmt" + "os" +) + +const subsidy = 10 + +// Transaction represents a blockchain transaction +type Transaction struct { + ID []byte + Vin []TXInput + Vout []TXOutput +} + +// IsCoinbase checks whether the transaction is a coinbase +func (tx Transaction) IsCoinbase() bool { + return len(tx.Vin) == 1 && len(tx.Vin[0].Txid) == 0 && tx.Vin[0].Vout == -1 +} + +// SetID sets the 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 { + exitWithError(err) + } + hash = sha256.Sum256(encoded.Bytes()) + tx.ID = hash[:] +} + +// NewCoinbaseTX creates a new coinbase transaction +func NewCoinbaseTX(to, data string) *Transaction { + if data == "" { + data = fmt.Sprintf("Reward to '%s'", to) + } + + txin := TXInput{ + Txid: []byte{}, + Vout: -1, + ScriptSig: data, + } + txout := TXOutput{ + Value: subsidy, + ScriptPubKey: 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 { + fmt.Println("ERROR: Not enough funds") + os.Exit(1) + } + + // Build a list of inputs + for txid, outs := range validOutputs { + txID, err := hex.DecodeString(txid) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + for _, out := range outs { + input := TXInput{ + Txid: txID, + Vout: out, + ScriptSig: from, + } + inputs = append(inputs, input) + } + } + + // Build a list of outputs + output := TXOutput{ + Value: amount, + ScriptPubKey: to, + } + outputs = append(outputs, output) + if acc > amount { + // There is some change + chgOutput := TXOutput{ + Value: acc - amount, + ScriptPubKey: from, + } + outputs = append(outputs, chgOutput) + } + + tx := Transaction{ + ID: nil, + Vin: inputs, + Vout: outputs, + } + tx.SetID() + + return &tx +} + +// TXInput represents a transaction input +type TXInput struct { + Txid []byte + Vout int + ScriptSig string +} + +// CanUnlockOutputWith checks whether the unlockingData (address) initiated the transaction +func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool { + return in.ScriptSig == unlockingData +} + +// TXOutput represents a transaction output +type TXOutput struct { + Value int + ScriptPubKey string +} + +// CanBeUnlockedWith checks if the output can be unlocked with the provided data +func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool { + return out.ScriptPubKey == unlockingData +} diff --git a/utils.go b/utils.go index 20690bc..5cdd349 100644 --- a/utils.go +++ b/utils.go @@ -3,7 +3,6 @@ package main import ( "bytes" "encoding/binary" - "log" ) // IntToHex converts an int64 to a byte array @@ -11,7 +10,7 @@ func IntToHex(num int64) []byte { buf := new(bytes.Buffer) err := binary.Write(buf, binary.BigEndian, num) if err != nil { - log.Panic(err) + exitWithError(err) } return buf.Bytes() }