diff --git a/.gitignore b/.gitignore index 98e6ef6..570133a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *.db +wallet.dat diff --git a/README.md b/README.md index 7c9b585..87fc9f4 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,4 @@ A blockchain implementation in Go, as described in these articles: 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/) diff --git a/base58.go b/base58.go new file mode 100644 index 0000000..589e00c --- /dev/null +++ b/base58.go @@ -0,0 +1,59 @@ +package main + +import ( + "bytes" + "math/big" +) + +var b58Alphabet = []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz") + +// Base58Encode encodes a byte array to Base58 +func Base58Encode(input []byte) []byte { + var result []byte + + x := big.NewInt(0).SetBytes(input) + + base := big.NewInt(int64(len(b58Alphabet))) + zero := big.NewInt(0) + mod := &big.Int{} + + for x.Cmp(zero) != 0 { + x.DivMod(x, base, mod) + result = append(result, b58Alphabet[mod.Int64()]) + } + + ReverseBytes(result) + for b := range input { + if b == 0x00 { + result = append([]byte{b58Alphabet[0]}, result...) + } else { + break + } + } + + return result +} + +// Base58Decode decodes Base58-encoded data +func Base58Decode(input []byte) []byte { + result := big.NewInt(0) + zeroBytes := 0 + + for b := range input { + if b == 0x00 { + zeroBytes++ + } + } + + payload := input[zeroBytes:] + for _, b := range payload { + charIndex := bytes.IndexByte(b58Alphabet, b) + result.Mul(result, big.NewInt(58)) + result.Add(result, big.NewInt(int64(charIndex))) + } + + decoded := result.Bytes() + decoded = append(bytes.Repeat([]byte{byte(0x00)}, zeroBytes), decoded...) + + return decoded +} diff --git a/block.go b/block.go index 04d96cc..b98c79c 100644 --- a/block.go +++ b/block.go @@ -8,7 +8,7 @@ import ( "time" ) -// Block keeps block headers +// Block represents a block in the blockchain type Block struct { Timestamp int64 Transactions []*Transaction @@ -17,32 +17,6 @@ type Block struct { Nonce int } -// Serialize serializes the block -func (b *Block) Serialize() []byte { - var result bytes.Buffer - encoder := gob.NewEncoder(&result) - - err := encoder.Encode(b) - if err != nil { - log.Panic(err) - } - - 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(transactions []*Transaction, prevBlockHash []byte) *Block { block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0} @@ -60,6 +34,32 @@ func NewGenesisBlock(coinbase *Transaction) *Block { return NewBlock([]*Transaction{coinbase}, []byte{}) } +// 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.Hash()) + } + txHash = sha256.Sum256(bytes.Join(txHashes, []byte{})) + + return txHash[:] +} + +// Serialize serializes the block +func (b *Block) Serialize() []byte { + var result bytes.Buffer + encoder := gob.NewEncoder(&result) + + err := encoder.Encode(b) + if err != nil { + log.Panic(err) + } + + return result.Bytes() +} + // DeserializeBlock deserializes a block func DeserializeBlock(d []byte) *Block { var block Block diff --git a/blockchain.go b/blockchain.go index b9cfb42..e2579fe 100644 --- a/blockchain.go +++ b/blockchain.go @@ -1,7 +1,10 @@ package main import ( + "bytes" + "crypto/ecdsa" "encoding/hex" + "errors" "fmt" "log" "os" @@ -19,23 +22,210 @@ type Blockchain struct { db *bolt.DB } -// BlockchainIterator is used to iterate over blockchain blocks -type BlockchainIterator struct { - currentHash []byte - db *bolt.DB +// CreateBlockchain creates a new blockchain DB +func CreateBlockchain(address string) *Blockchain { + if dbExists() { + fmt.Println("Blockchain already exists.") + os.Exit(1) + } + + var tip []byte + + cbtx := NewCoinbaseTX(address, genesisCoinbaseData) + genesis := NewGenesisBlock(cbtx) + + db, err := bolt.Open(dbFile, 0600, nil) + if err != nil { + log.Panic(err) + } + + err = db.Update(func(tx *bolt.Tx) error { + 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 + }) + if err != nil { + log.Panic(err) + } + + bc := Blockchain{tip, db} + + return &bc +} + +// NewBlockchain creates a new Blockchain with genesis Block +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 { + log.Panic(err) + } + + err = db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(blocksBucket)) + tip = b.Get([]byte("l")) + + return nil + }) + if err != nil { + log.Panic(err) + } + + bc := Blockchain{tip, db} + + 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() + + for { + block := bci.Next() + + for _, tx := range block.Transactions { + if bytes.Compare(tx.ID, ID) == 0 { + return *tx, nil + } + } + + if len(block.PrevBlockHash) == 0 { + break + } + } + + 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 + 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 _, spentOutIdx := range spentTXOs[txID] { + if spentOutIdx == outIdx { + continue Outputs + } + } + } + + if out.IsLockedWithKey(pubKeyHash) { + unspentTXs = append(unspentTXs, *tx) + } + } + + 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) + } + } + } + } + + if len(block.PrevBlockHash) == 0 { + break + } + } + + 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 +} + +// Iterator returns a BlockchainIterat +func (bc *Blockchain) Iterator() *BlockchainIterator { + bci := &BlockchainIterator{bc.tip, bc.db} + + return bci } // MineBlock mines a new block with the provided transactions func (bc *Blockchain) MineBlock(transactions []*Transaction) { var lastHash []byte + for _, tx := range transactions { + if bc.VerifyTransaction(tx) != true { + log.Panic("ERROR: Invalid transaction") + } + } + err := bc.db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blocksBucket)) lastHash = b.Get([]byte("l")) return nil }) - if err != nil { log.Panic(err) } @@ -58,121 +248,39 @@ func (bc *Blockchain) MineBlock(transactions []*Transaction) { return nil }) -} - -// 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} - - return bci -} - -// Next returns next block starting from the tip -func (i *BlockchainIterator) Next() *Block { - var block *Block - - err := i.db.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte(blocksBucket)) - encodedBlock := b.Get(i.currentHash) - block = DeserializeBlock(encodedBlock) - - return nil - }) - if err != nil { log.Panic(err) } +} - i.currentHash = block.PrevBlockHash +// SignTransaction signs inputs of a Transaction +func (bc *Blockchain) SignTransaction(tx *Transaction, privKey ecdsa.PrivateKey) { + prevTXs := make(map[string]Transaction) - return block + for _, vin := range tx.Vin { + prevTX, err := bc.FindTransaction(vin.Txid) + if err != nil { + log.Panic(err) + } + prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX + } + + tx.Sign(privKey, prevTXs) +} + +// VerifyTransaction verifies transaction input signatures +func (bc *Blockchain) VerifyTransaction(tx *Transaction) bool { + prevTXs := make(map[string]Transaction) + + for _, vin := range tx.Vin { + prevTX, err := bc.FindTransaction(vin.Txid) + if err != nil { + log.Panic(err) + } + prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX + } + + return tx.Verify(prevTXs) } func dbExists() bool { @@ -182,77 +290,3 @@ func dbExists() bool { return true } - -// NewBlockchain creates a new Blockchain with genesis Block -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 { - log.Panic(err) - } - - err = db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte(blocksBucket)) - 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 - }) - - if err != nil { - log.Panic(err) - } - - bc := Blockchain{tip, db} - - return &bc -} diff --git a/blockchain_iterator.go b/blockchain_iterator.go new file mode 100644 index 0000000..305311d --- /dev/null +++ b/blockchain_iterator.go @@ -0,0 +1,34 @@ +package main + +import ( + "log" + + "github.com/boltdb/bolt" +) + +// BlockchainIterator is used to iterate over blockchain blocks +type BlockchainIterator struct { + currentHash []byte + db *bolt.DB +} + +// Next returns next block starting from the tip +func (i *BlockchainIterator) Next() *Block { + var block *Block + + err := i.db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(blocksBucket)) + encodedBlock := b.Get(i.currentHash) + block = DeserializeBlock(encodedBlock) + + return nil + }) + + if err != nil { + log.Panic(err) + } + + i.currentHash = block.PrevBlockHash + + return block +} diff --git a/cli.go b/cli.go index 1270c6e..863ffd1 100644 --- a/cli.go +++ b/cli.go @@ -5,36 +5,17 @@ import ( "fmt" "log" "os" - "strconv" ) // CLI responsible for processing command line arguments 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(" getbalance -address ADDRESS - Get balance of ADDRESS") fmt.Println(" createblockchain -address ADDRESS - Create a blockchain and send genesis block reward to ADDRESS") + fmt.Println(" createwallet - Generates a new key-pair and saves it into the wallet file") + 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(" send -from FROM -to TO -amount AMOUNT - Send AMOUNT of coins from FROM address to TO") } @@ -46,43 +27,14 @@ func (cli *CLI) validateArgs() { } } -func (cli *CLI) printChain() { - // 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("Hash: %x\n", block.Hash) - pow := NewProofOfWork(block) - fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate())) - fmt.Println() - - if len(block.PrevBlockHash) == 0 { - break - } - } -} - -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() getBalanceCmd := flag.NewFlagSet("getbalance", flag.ExitOnError) 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) @@ -103,6 +55,16 @@ func (cli *CLI) Run() { if err != nil { log.Panic(err) } + case "createwallet": + err := createWalletCmd.Parse(os.Args[2:]) + if err != nil { + log.Panic(err) + } + case "listaddresses": + err := listAddressesCmd.Parse(os.Args[2:]) + if err != nil { + log.Panic(err) + } case "printchain": err := printChainCmd.Parse(os.Args[2:]) if err != nil { @@ -134,6 +96,14 @@ func (cli *CLI) Run() { cli.createBlockchain(*createBlockchainAddress) } + if createWalletCmd.Parsed() { + cli.createWallet() + } + + if listAddressesCmd.Parsed() { + cli.listAddresses() + } + if printChainCmd.Parsed() { cli.printChain() } diff --git a/cli_createblockchain.go b/cli_createblockchain.go new file mode 100644 index 0000000..06ff1b1 --- /dev/null +++ b/cli_createblockchain.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "log" +) + +func (cli *CLI) createBlockchain(address string) { + if !ValidateAddress(address) { + log.Panic("ERROR: Address is not valid") + } + bc := CreateBlockchain(address) + bc.db.Close() + fmt.Println("Done!") +} diff --git a/cli_createwallet.go b/cli_createwallet.go new file mode 100644 index 0000000..b42a142 --- /dev/null +++ b/cli_createwallet.go @@ -0,0 +1,11 @@ +package main + +import "fmt" + +func (cli *CLI) createWallet() { + wallets, _ := NewWallets() + address := wallets.CreateWallet() + wallets.SaveToFile() + + fmt.Printf("Your new address: %s\n", address) +} diff --git a/cli_getbalance.go b/cli_getbalance.go new file mode 100644 index 0000000..27cb55e --- /dev/null +++ b/cli_getbalance.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "log" +) + +func (cli *CLI) getBalance(address string) { + if !ValidateAddress(address) { + log.Panic("ERROR: Address is not valid") + } + bc := NewBlockchain(address) + defer bc.db.Close() + + balance := 0 + pubKeyHash := Base58Decode([]byte(address)) + pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-4] + UTXOs := bc.FindUTXO(pubKeyHash) + + for _, out := range UTXOs { + balance += out.Value + } + + fmt.Printf("Balance of '%s': %d\n", address, balance) +} diff --git a/cli_listaddress.go b/cli_listaddress.go new file mode 100644 index 0000000..96fd282 --- /dev/null +++ b/cli_listaddress.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + "log" +) + +func (cli *CLI) listAddresses() { + wallets, err := NewWallets() + if err != nil { + log.Panic(err) + } + addresses := wallets.GetAddresses() + + for _, address := range addresses { + fmt.Println(address) + } +} diff --git a/cli_printchain.go b/cli_printchain.go new file mode 100644 index 0000000..cd5c7a0 --- /dev/null +++ b/cli_printchain.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "strconv" +) + +func (cli *CLI) printChain() { + bc := NewBlockchain("") + defer bc.db.Close() + + bci := bc.Iterator() + + for { + block := bci.Next() + + fmt.Printf("============ Block %x ============\n", block.Hash) + fmt.Printf("Prev. block: %x\n", block.PrevBlockHash) + pow := NewProofOfWork(block) + fmt.Printf("PoW: %s\n\n", strconv.FormatBool(pow.Validate())) + for _, tx := range block.Transactions { + fmt.Println(tx) + } + fmt.Printf("\n\n") + + if len(block.PrevBlockHash) == 0 { + break + } + } +} diff --git a/cli_send.go b/cli_send.go new file mode 100644 index 0000000..fb8117c --- /dev/null +++ b/cli_send.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + "log" +) + +func (cli *CLI) send(from, to string, amount int) { + if !ValidateAddress(from) { + log.Panic("ERROR: Sender address is not valid") + } + if !ValidateAddress(to) { + log.Panic("ERROR: Recipient address is not valid") + } + + bc := NewBlockchain(from) + defer bc.db.Close() + + tx := NewUTXOTransaction(from, to, amount, bc) + bc.MineBlock([]*Transaction{tx}) + fmt.Println("Success!") +} diff --git a/transaction.go b/transaction.go index fe82a7b..90950cc 100644 --- a/transaction.go +++ b/transaction.go @@ -2,11 +2,17 @@ package main import ( "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" "crypto/sha256" + "math/big" + "encoding/gob" "encoding/hex" "fmt" "log" + "strings" ) const subsidy = 10 @@ -23,41 +29,145 @@ 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() { +// Serialize returns a serialized Transaction +func (tx Transaction) Serialize() []byte { 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[:] + + return encoded.Bytes() } -// TXInput represents a transaction input -type TXInput struct { - Txid []byte - Vout int - ScriptSig string +// Hash returns the hash of the Transaction +func (tx *Transaction) Hash() []byte { + var hash [32]byte + + txCopy := *tx + txCopy.ID = []byte{} + + hash = sha256.Sum256(txCopy.Serialize()) + + return hash[:] } -// TXOutput represents a transaction output -type TXOutput struct { - Value int - ScriptPubKey string +// Sign signs each input of a Transaction +func (tx *Transaction) Sign(privKey ecdsa.PrivateKey, prevTXs map[string]Transaction) { + if tx.IsCoinbase() { + return + } + + for _, vin := range tx.Vin { + if prevTXs[hex.EncodeToString(vin.Txid)].ID == nil { + log.Panic("ERROR: Previous transaction is not correct") + } + } + + txCopy := tx.TrimmedCopy() + + for inID, vin := range txCopy.Vin { + prevTx := prevTXs[hex.EncodeToString(vin.Txid)] + txCopy.Vin[inID].Signature = nil + txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash + txCopy.ID = txCopy.Hash() + txCopy.Vin[inID].PubKey = nil + + r, s, err := ecdsa.Sign(rand.Reader, &privKey, txCopy.ID) + if err != nil { + log.Panic(err) + } + signature := append(r.Bytes(), s.Bytes()...) + + tx.Vin[inID].Signature = signature + } } -// CanUnlockOutputWith checks whether the address initiated the transaction -func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool { - return in.ScriptSig == unlockingData +// String returns a human-readable representation of a transaction +func (tx Transaction) String() string { + var lines []string + + lines = append(lines, fmt.Sprintf("--- Transaction %x:", tx.ID)) + + for i, input := range tx.Vin { + + lines = append(lines, fmt.Sprintf(" Input %d:", i)) + lines = append(lines, fmt.Sprintf(" TXID: %x", input.Txid)) + lines = append(lines, fmt.Sprintf(" Out: %d", input.Vout)) + lines = append(lines, fmt.Sprintf(" Signature: %x", input.Signature)) + lines = append(lines, fmt.Sprintf(" PubKey: %x", input.PubKey)) + } + + for i, output := range tx.Vout { + lines = append(lines, fmt.Sprintf(" Output %d:", i)) + lines = append(lines, fmt.Sprintf(" Value: %d", output.Value)) + lines = append(lines, fmt.Sprintf(" Script: %x", output.PubKeyHash)) + } + + return strings.Join(lines, "\n") } -// CanBeUnlockedWith checks if the output can be unlocked with the provided data -func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool { - return out.ScriptPubKey == unlockingData +// TrimmedCopy creates a trimmed copy of Transaction to be used in signing +func (tx *Transaction) TrimmedCopy() Transaction { + var inputs []TXInput + var outputs []TXOutput + + for _, vin := range tx.Vin { + inputs = append(inputs, TXInput{vin.Txid, vin.Vout, nil, nil}) + } + + for _, vout := range tx.Vout { + outputs = append(outputs, TXOutput{vout.Value, vout.PubKeyHash}) + } + + txCopy := Transaction{tx.ID, inputs, outputs} + + return txCopy +} + +// Verify verifies signatures of Transaction inputs +func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool { + if tx.IsCoinbase() { + return true + } + + for _, vin := range tx.Vin { + if prevTXs[hex.EncodeToString(vin.Txid)].ID == nil { + log.Panic("ERROR: Previous transaction is not correct") + } + } + + txCopy := tx.TrimmedCopy() + curve := elliptic.P256() + + for inID, vin := range tx.Vin { + prevTx := prevTXs[hex.EncodeToString(vin.Txid)] + txCopy.Vin[inID].Signature = nil + txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash + txCopy.ID = txCopy.Hash() + txCopy.Vin[inID].PubKey = nil + + r := big.Int{} + s := big.Int{} + sigLen := len(vin.Signature) + r.SetBytes(vin.Signature[:(sigLen / 2)]) + s.SetBytes(vin.Signature[(sigLen / 2):]) + + x := big.Int{} + y := big.Int{} + keyLen := len(vin.PubKey) + x.SetBytes(vin.PubKey[:(keyLen / 2)]) + y.SetBytes(vin.PubKey[(keyLen / 2):]) + + rawPubKey := ecdsa.PublicKey{curve, &x, &y} + if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false { + return false + } + } + + return true } // NewCoinbaseTX creates a new coinbase transaction @@ -66,10 +176,10 @@ func NewCoinbaseTX(to, data string) *Transaction { 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() + txin := TXInput{[]byte{}, -1, nil, []byte(data)} + txout := NewTXOutput(subsidy, to) + tx := Transaction{nil, []TXInput{txin}, []TXOutput{*txout}} + tx.ID = tx.Hash() return &tx } @@ -79,7 +189,13 @@ func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transactio var inputs []TXInput var outputs []TXOutput - acc, validOutputs := bc.FindSpendableOutputs(from, amount) + wallets, err := NewWallets() + if err != nil { + log.Panic(err) + } + wallet := wallets.GetWallet(from) + pubKeyHash := HashPubKey(wallet.PublicKey) + acc, validOutputs := bc.FindSpendableOutputs(pubKeyHash, amount) if acc < amount { log.Panic("ERROR: Not enough funds") @@ -93,19 +209,20 @@ func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transactio } for _, out := range outs { - input := TXInput{txID, out, from} + input := TXInput{txID, out, nil, wallet.PublicKey} inputs = append(inputs, input) } } // Build a list of outputs - outputs = append(outputs, TXOutput{amount, to}) + outputs = append(outputs, *NewTXOutput(amount, to)) if acc > amount { - outputs = append(outputs, TXOutput{acc - amount, from}) // a change + outputs = append(outputs, *NewTXOutput(acc-amount, from)) // a change } tx := Transaction{nil, inputs, outputs} - tx.SetID() + tx.ID = tx.Hash() + bc.SignTransaction(&tx, wallet.PrivateKey) return &tx } diff --git a/transaction_input.go b/transaction_input.go new file mode 100644 index 0000000..23beeba --- /dev/null +++ b/transaction_input.go @@ -0,0 +1,18 @@ +package main + +import "bytes" + +// TXInput represents a transaction input +type TXInput struct { + Txid []byte + Vout int + Signature []byte + PubKey []byte +} + +// UsesKey checks whether the address initiated the transaction +func (in *TXInput) UsesKey(pubKeyHash []byte) bool { + lockingHash := HashPubKey(in.PubKey) + + return bytes.Compare(lockingHash, pubKeyHash) == 0 +} diff --git a/transaction_output.go b/transaction_output.go new file mode 100644 index 0000000..4d70d74 --- /dev/null +++ b/transaction_output.go @@ -0,0 +1,29 @@ +package main + +import "bytes" + +// TXOutput represents a transaction output +type TXOutput struct { + Value int + PubKeyHash []byte +} + +// Lock signs the output +func (out *TXOutput) Lock(address []byte) { + pubKeyHash := Base58Decode(address) + pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-4] + out.PubKeyHash = pubKeyHash +} + +// IsLockedWithKey checks if the output can be used by the owner of the pubkey +func (out *TXOutput) IsLockedWithKey(pubKeyHash []byte) bool { + return bytes.Compare(out.PubKeyHash, pubKeyHash) == 0 +} + +// NewTXOutput create a new TXOutput +func NewTXOutput(value int, address string) *TXOutput { + txo := &TXOutput{value, nil} + txo.Lock([]byte(address)) + + return txo +} diff --git a/utils.go b/utils.go index 7f5e727..aecf919 100644 --- a/utils.go +++ b/utils.go @@ -16,3 +16,10 @@ func IntToHex(num int64) []byte { return buff.Bytes() } + +// ReverseBytes reverses a byte array +func ReverseBytes(data []byte) { + for i, j := 0, len(data)-1; i < j; i, j = i+1, j-1 { + data[i], data[j] = data[j], data[i] + } +} diff --git a/wallet.go b/wallet.go new file mode 100644 index 0000000..31a2258 --- /dev/null +++ b/wallet.go @@ -0,0 +1,87 @@ +package main + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "log" + + "golang.org/x/crypto/ripemd160" +) + +const version = byte(0x00) +const walletFile = "wallet.dat" +const addressChecksumLen = 4 + +// Wallet stores private and public keys +type Wallet struct { + PrivateKey ecdsa.PrivateKey + PublicKey []byte +} + +// NewWallet creates and returns a Wallet +func NewWallet() *Wallet { + private, public := newKeyPair() + wallet := Wallet{private, public} + + return &wallet +} + +// GetAddress returns wallet address +func (w Wallet) GetAddress() []byte { + pubKeyHash := HashPubKey(w.PublicKey) + + versionedPayload := append([]byte{version}, pubKeyHash...) + checksum := checksum(versionedPayload) + + fullPayload := append(versionedPayload, checksum...) + address := Base58Encode(fullPayload) + + return address +} + +// HashPubKey hashes public key +func HashPubKey(pubKey []byte) []byte { + publicSHA256 := sha256.Sum256(pubKey) + + RIPEMD160Hasher := ripemd160.New() + _, err := RIPEMD160Hasher.Write(publicSHA256[:]) + if err != nil { + log.Panic(err) + } + publicRIPEMD160 := RIPEMD160Hasher.Sum(nil) + + return publicRIPEMD160 +} + +// ValidateAddress check if address if valid +func ValidateAddress(address string) bool { + pubKeyHash := Base58Decode([]byte(address)) + actualChecksum := pubKeyHash[len(pubKeyHash)-addressChecksumLen:] + version := pubKeyHash[0] + pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-addressChecksumLen] + targetChecksum := checksum(append([]byte{version}, pubKeyHash...)) + + return bytes.Compare(actualChecksum, targetChecksum) == 0 +} + +// Checksum generates a checksum for a public key +func checksum(payload []byte) []byte { + firstSHA := sha256.Sum256(payload) + secondSHA := sha256.Sum256(firstSHA[:]) + + return secondSHA[:addressChecksumLen] +} + +func newKeyPair() (ecdsa.PrivateKey, []byte) { + curve := elliptic.P256() + private, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + log.Panic(err) + } + pubKey := append(private.PublicKey.X.Bytes(), private.PublicKey.Y.Bytes()...) + + return *private, pubKey +} diff --git a/wallets.go b/wallets.go new file mode 100644 index 0000000..e5b2134 --- /dev/null +++ b/wallets.go @@ -0,0 +1,94 @@ +package main + +import ( + "bytes" + "crypto/elliptic" + "encoding/gob" + "fmt" + "io/ioutil" + "log" + "os" +) + +// Wallets stores a collection of wallets +type Wallets struct { + Wallets map[string]*Wallet +} + +// NewWallets creates Wallets and fills it from a file if it exists +func NewWallets() (*Wallets, error) { + wallets := Wallets{} + wallets.Wallets = make(map[string]*Wallet) + + err := wallets.LoadFromFile() + + return &wallets, err +} + +// CreateWallet adds a Wallet to Wallets +func (ws *Wallets) CreateWallet() string { + wallet := NewWallet() + address := fmt.Sprintf("%s", wallet.GetAddress()) + + ws.Wallets[address] = wallet + + return address +} + +// GetAddresses returns an array of addresses stored in the wallet file +func (ws *Wallets) GetAddresses() []string { + var addresses []string + + for address := range ws.Wallets { + addresses = append(addresses, address) + } + + return addresses +} + +// GetWallet returns a Wallet by its address +func (ws Wallets) GetWallet(address string) Wallet { + return *ws.Wallets[address] +} + +// LoadFromFile loads wallets from the file +func (ws *Wallets) LoadFromFile() error { + if _, err := os.Stat(walletFile); os.IsNotExist(err) { + return err + } + + fileContent, err := ioutil.ReadFile(walletFile) + if err != nil { + log.Panic(err) + } + + var wallets Wallets + gob.Register(elliptic.P256()) + decoder := gob.NewDecoder(bytes.NewReader(fileContent)) + err = decoder.Decode(&wallets) + if err != nil { + log.Panic(err) + } + + ws.Wallets = wallets.Wallets + + return nil +} + +// SaveToFile saves wallets to a file +func (ws Wallets) SaveToFile() { + var content bytes.Buffer + + gob.Register(elliptic.P256()) + + encoder := gob.NewEncoder(&content) + err := encoder.Encode(ws) + if err != nil { + log.Panic(err) + } + + err = ioutil.WriteFile(walletFile, content.Bytes(), 0644) + if err != nil { + log.Panic(err) + } +}