diff options
author | Michael Foiani <sotech117@michaels-mbp.devices.brown.edu> | 2022-02-28 15:33:25 -0500 |
---|---|---|
committer | Michael Foiani <sotech117@michaels-mbp.devices.brown.edu> | 2022-02-28 15:33:25 -0500 |
commit | f095ce07e577d6b90e8c6f9090be56eb46af8487 (patch) | |
tree | 14533c52a8fc97cbb711f3930dd60bddf6d3471c | |
parent | 1dd0508d5d3c737f1ee9c723f580baf73b1cfd70 (diff) |
Solution code inputted.
-rw-r--r-- | .idea/.gitignore | 8 | ||||
-rw-r--r-- | .idea/1-chain-shady99.iml | 9 | ||||
-rw-r--r-- | .idea/modules.xml | 8 | ||||
-rw-r--r-- | .idea/vcs.xml | 6 | ||||
-rw-r--r-- | pkg/blockchain/blockchain.go | 66 | ||||
-rw-r--r-- | pkg/blockchain/blockinfodatabase/blockinfodatabase.go | 49 | ||||
-rw-r--r-- | pkg/blockchain/chainwriter/chainwriter.go | 60 | ||||
-rw-r--r-- | pkg/blockchain/coindatabase/coindatabase.go | 154 |
8 files changed, 344 insertions, 16 deletions
diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/1-chain-shady99.iml b/.idea/1-chain-shady99.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/1-chain-shady99.iml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="WEB_MODULE" version="4"> + <component name="Go" enabled="true" /> + <component name="NewModuleRootManager"> + <content url="file://$MODULE_DIR$" /> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module>
\ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..070ffcb --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/1-chain-shady99.iml" filepath="$PROJECT_DIR$/.idea/1-chain-shady99.iml" /> + </modules> + </component> +</project>
\ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="$PROJECT_DIR$" vcs="Git" /> + </component> +</project>
\ No newline at end of file diff --git a/pkg/blockchain/blockchain.go b/pkg/blockchain/blockchain.go index c456be1..7b92b1a 100644 --- a/pkg/blockchain/blockchain.go +++ b/pkg/blockchain/blockchain.go @@ -6,6 +6,7 @@ import ( "Chain/pkg/blockchain/chainwriter" "Chain/pkg/blockchain/coindatabase" "Chain/pkg/utils" + "math" ) // BlockChain is the main type of this project. @@ -86,7 +87,70 @@ func GenesisBlock(config *Config) *block.Block { // (4) Handles a fork, if necessary. // (5) Updates the BlockChain's fields. func (bc *BlockChain) HandleBlock(b *block.Block) { - //TODO + appends := bc.appendsToActiveChain(b) + blockHash := b.Hash() + + // 1. Validate Block + if appends && !bc.CoinDB.ValidateBlock(b.Transactions) { + return + } + + // 2. Make Undo Block + ub := bc.makeUndoBlock(b.Transactions) + + // 3. Store Block in CoinDatabase + bc.CoinDB.StoreBlock(b.Transactions, appends) + + // 4. Get BlockRecord for previous Block + previousBr := bc.BlockInfoDB.GetBlockRecord(b.Header.PreviousHash) + + // 5. Store UndoBlock and Block to Disk + height := previousBr.Height + 1 + br := bc.ChainWriter.StoreBlock(b, ub, height) + + // 6. Store BlockRecord to BlockInfoDatabase + bc.BlockInfoDB.StoreBlockRecord(blockHash, br) + + if appends { + // 7. Handle appending Block + bc.Length++ + bc.LastBlock = b + bc.LastHash = blockHash + if len(bc.UnsafeHashes) >= 6 { + bc.UnsafeHashes = bc.UnsafeHashes[1:] + } + bc.UnsafeHashes = append(bc.UnsafeHashes, blockHash) + } else if height > bc.Length { + // 8. Handle fork + bc.handleFork(b, blockHash, height) + } +} + +// handleFork updates the BlockChain when a fork occurs. First, it +// finds the Blocks the BlockChain must revert. Once found, it uses +// those Blocks to update the CoinDatabase. Lastly, it updates the +// BlockChain's fields to reflect the fork. +func (bc *BlockChain) handleFork(b *block.Block, blockHash string, height uint32) { + bc.Length = height + forkedBlocks := bc.getForkedBlocks(blockHash) + var forkedUnsafeHashes []string + min := int(math.Min(float64(6), float64(len(forkedBlocks)))) + for i := 0; i < min; i++ { + forkedHash := forkedBlocks[i].Hash() + forkedUnsafeHashes = append(forkedUnsafeHashes, forkedHash) + } + // likely have to add unsafe hashes + bc.UnsafeHashes = forkedUnsafeHashes + blocks, undoBlocks := bc.getBlocksAndUndoBlocks(len(forkedBlocks) - 1) + bc.CoinDB.UndoCoins(blocks, undoBlocks) + for _, bl := range forkedBlocks { + if !bc.CoinDB.ValidateBlock(bl.Transactions) { + utils.Debug.Printf("Validation failed for forked block {%v}", b.Hash()) + } + bc.CoinDB.StoreBlock(bl.Transactions, true) + } + bc.LastBlock = b + bc.LastHash = b.Hash() } // makeUndoBlock returns an UndoBlock given a slice of Transaction. diff --git a/pkg/blockchain/blockinfodatabase/blockinfodatabase.go b/pkg/blockchain/blockinfodatabase/blockinfodatabase.go index c49a625..e219da4 100644 --- a/pkg/blockchain/blockinfodatabase/blockinfodatabase.go +++ b/pkg/blockchain/blockinfodatabase/blockinfodatabase.go @@ -1,8 +1,10 @@ package blockinfodatabase import ( + "Chain/pkg/pro" "Chain/pkg/utils" "github.com/syndtr/goleveldb/leveldb" + "google.golang.org/protobuf/proto" ) // BlockInfoDatabase is a wrapper for a levelDB @@ -20,13 +22,54 @@ func New(config *Config) *BlockInfoDatabase { } // StoreBlockRecord stores a BlockRecord in the BlockInfoDatabase. +// hash is the hash of the block, and the key for the blockRecord. +// blockRecord is the value we're storing in the database +// +// At a high level, here's what this function is doing: +// (1) converting a blockRecord to a protobuf version (for more effective storage) +// (2) converting the protobuf to bytes +// (3) storing the byte version of the blockRecord in our database func (blockInfoDB *BlockInfoDatabase) StoreBlockRecord(hash string, blockRecord *BlockRecord) { - //TODO + // convert the blockRecord to a proto version + protoRecord := EncodeBlockRecord(blockRecord) + // marshaling (serializing) the proto record to bytes + bytes, err := proto.Marshal(protoRecord) + // checking that the marshalling process didn't throw an error + if err != nil { + utils.Debug.Printf("Failed to marshal protoRecord:", err) + } + // attempting to store the bytes in our database AND checking to make + // sure that the storing process doesn't fail. The Put(key, value, writeOptions) + // function is levelDB's. + if err = blockInfoDB.db.Put([]byte(hash), bytes, nil); err != nil { + utils.Debug.Printf("Unable to store block protoRecord for hash {%v}", hash) + } } // GetBlockRecord returns a BlockRecord from the BlockInfoDatabase given // the relevant block's hash. +// hash is the hash of the block, and the key for the blockRecord. +// +// At a high level, here's what this function is doing: +// (1) retrieving the byte version of the protobuf record. +// (2) converting the bytes to protobuf +// (3) converting the protobuf to blockRecord and returning that. func (blockInfoDB *BlockInfoDatabase) GetBlockRecord(hash string) *BlockRecord { - //TODO - return nil + // attempting to retrieve the byte-version of the protobuf record + // from our database AND checking that the value is retrieved successfully. + // The Get(key, writeOptions) function is levelDB's. + data, err := blockInfoDB.db.Get([]byte(hash), nil) + if err != nil { + utils.Debug.Printf("Unable to get block record for hash {%v}", hash) + } + // creating a protobuf blockRecord object to fill + protoRecord := &pro.BlockRecord{} + // unmarshalling (deserializing) the bytes stored in the database into the + // protobuf object created on line 66. Checking that the conversion process + // from bytes to protobuf object succeeds. + if err = proto.Unmarshal(data, protoRecord); err != nil { + utils.Debug.Printf("Failed to unmarshal record from hash {%v}:", hash, err) + } + // convert the protobuf record to a normal blockRecord and returning that. + return DecodeBlockRecord(protoRecord) } diff --git a/pkg/blockchain/chainwriter/chainwriter.go b/pkg/blockchain/chainwriter/chainwriter.go index 67a7d49..e1dd512 100644 --- a/pkg/blockchain/chainwriter/chainwriter.go +++ b/pkg/blockchain/chainwriter/chainwriter.go @@ -8,6 +8,7 @@ import ( "google.golang.org/protobuf/proto" "log" "os" + "strconv" ) // ChainWriter handles all I/O for the BlockChain. It stores and retrieves @@ -95,15 +96,66 @@ func (cw *ChainWriter) StoreBlock(bl *block.Block, undoBlock *UndoBlock, height // WriteBlock writes a serialized Block to Disk and returns // a FileInfo for storage information. func (cw *ChainWriter) WriteBlock(serializedBlock []byte) *FileInfo { - //TODO - return nil + // need to know the length of the block + length := uint32(len(serializedBlock)) + // if we don't have enough space for this block in the current file, + // we have to update our file by changing the current file number + // and resetting the start offset to zero (so we write at the beginning + // of the file again. + // (recall format from above: "data/block_0.txt") + if cw.CurrentBlockOffset+length >= cw.MaxBlockFileSize { + cw.CurrentBlockOffset = 0 + cw.CurrentBlockFileNumber++ + } + // create path to correct file, following format + // "DataDirectory/BlockFileName_CurrentBlockFileNumber.FileExtension" + // Ex: "data/block_0.txt" + fileName := cw.DataDirectory + "/" + cw.BlockFileName + "_" + strconv.Itoa(int(cw.CurrentBlockFileNumber)) + cw.FileExtension + // write serialized block to disk + writeToDisk(fileName, serializedBlock) + // create a file info object with the starting and ending offsets of the serialized block + fi := &FileInfo{ + FileName: fileName, + StartOffset: cw.CurrentBlockOffset, + EndOffset: cw.CurrentBlockOffset + length, + } + // update offset for next write + cw.CurrentBlockOffset += length + // return the file info + return fi } // WriteUndoBlock writes a serialized UndoBlock to Disk and returns // a FileInfo for storage information. func (cw *ChainWriter) WriteUndoBlock(serializedUndoBlock []byte) *FileInfo { - //TODO - return nil + // need to know the length of the block + length := uint32(len(serializedUndoBlock)) + // if we don't have enough space for this undo block in the current undo file, + // we have to update our undo file by changing the current undo file number + // and resetting the start undo offset to zero (so we write at the beginning + // of the undo file again. + // (recall format from above: "data/undo_0.txt") + if cw.CurrentUndoOffset+length >= cw.MaxUndoFileSize { + cw.CurrentUndoOffset = 0 + cw.CurrentUndoFileNumber++ + } + // create path to correct file, following format + // "DataDirectory/BlockFileName_CurrentBlockFileNumber.FileExtension" + // Ex: "data/undo_0.txt" + fileName := cw.DataDirectory + "/" + cw.UndoFileName + "_" + strconv.Itoa(int(cw.CurrentUndoFileNumber)) + cw.FileExtension + // write serialized undo block to disk + writeToDisk(fileName, serializedUndoBlock) + // create a file info object with the starting and ending undo offsets of the serialized + // undo block + fi := &FileInfo{ + FileName: fileName, + StartOffset: cw.CurrentUndoOffset, + EndOffset: cw.CurrentUndoOffset + length, + } + // update offset for next write + cw.CurrentUndoOffset += length + // return the file info + return fi } // ReadBlock returns a Block given a FileInfo. diff --git a/pkg/blockchain/coindatabase/coindatabase.go b/pkg/blockchain/coindatabase/coindatabase.go index 83b3026..f61e178 100644 --- a/pkg/blockchain/coindatabase/coindatabase.go +++ b/pkg/blockchain/coindatabase/coindatabase.go @@ -80,15 +80,69 @@ func (coinDB *CoinDatabase) validateTransaction(transaction *block.Transaction) // (1) erases the Coins created by a Block and // (2) marks the Coins used to create those Transactions as unspent. func (coinDB *CoinDatabase) UndoCoins(blocks []*block.Block, undoBlocks []*chainwriter.UndoBlock) { - //TODO + // loop through all the block/undoBlock pairings || len(blocks) = len(undoBlocks) + for i := 0; i < len(blocks); i++ { + // (1) deal with Blocks + for _, tx := range blocks[i].Transactions { + txHash := tx.Hash() + for j := 0; j < len(tx.Outputs); j++ { + // create the coin locator + cl := CoinLocator{ + ReferenceTransactionHash: txHash, + OutputIndex: uint32(j), + } + // Remove the coin from the main cache (if it exists there) and update size + if _, ok := coinDB.mainCache[cl]; ok { + delete(coinDB.mainCache, cl) + coinDB.mainCacheSize-- + } + } + // delete the coin record from the db (erases all the coins in the process) + if err := coinDB.db.Delete([]byte(txHash), nil); err != nil { + utils.Debug.Printf("Error deleting coin record for transaction {%v}", txHash) + } + } + // (2) deal with UndoBlocks + for j := 0; j < len(undoBlocks[i].TransactionInputHashes); j++ { + txHash := undoBlocks[i].TransactionInputHashes[j] + // retrieve coin record from db + cr := coinDB.getCoinRecordFromDB(txHash) + // + if cr != nil { + // Add coins to record. This is the reestablishing part. + cr = coinDB.addCoinToRecord(cr, undoBlocks[i], j) + cl := CoinLocator{ + ReferenceTransactionHash: txHash, + OutputIndex: undoBlocks[i].OutputIndexes[j], + } + // Mark the coin in the main cache as unspent + if coin, ok := coinDB.mainCache[cl]; ok { + coin.IsSpent = false + coinDB.mainCache[cl] = coin + } + } else { + // if there was no coin record to get from the db, we + // need to make a new one with all the coins from + // the undoBlock + cr = &CoinRecord{ + Version: 0, + OutputIndexes: undoBlocks[i].OutputIndexes, + Amounts: undoBlocks[i].Amounts, + LockingScripts: undoBlocks[i].LockingScripts, + } + } + // put the updated record back in the db. + coinDB.putRecordInDB(txHash, cr) + } + } } -// addCoinsToRecord adds coins to a CoinRecord given an UndoBlock and -// returns the updated CoinRecord. -func (coinDB *CoinDatabase) addCoinsToRecord(cr *CoinRecord, ub *chainwriter.UndoBlock) *CoinRecord { - cr.OutputIndexes = append(cr.OutputIndexes, ub.OutputIndexes...) - cr.Amounts = append(cr.Amounts, ub.Amounts...) - cr.LockingScripts = append(cr.LockingScripts, ub.LockingScripts...) +// addCoinToRecord adds a Coin to a CoinRecord given an UndoBlock and index, +// returning the updated CoinRecord. +func (coinDB *CoinDatabase) addCoinToRecord(cr *CoinRecord, ub *chainwriter.UndoBlock, index int) *CoinRecord { + cr.OutputIndexes = append(cr.OutputIndexes, ub.OutputIndexes[index]) + cr.Amounts = append(cr.Amounts, ub.Amounts[index]) + cr.LockingScripts = append(cr.LockingScripts, ub.LockingScripts[index]) return cr } @@ -143,7 +197,91 @@ func (coinDB *CoinDatabase) FlushMainCache() { // (2) stores new TransactionOutputs as Coins in the mainCache (if active) // (3) stores CoinRecords for the Transactions in the db. func (coinDB *CoinDatabase) StoreBlock(transactions []*block.Transaction, active bool) { - //TODO + // do (1) and (2) only if active + if active { + coinDB.updateSpentCoins(transactions) + coinDB.storeTransactionsInMainCache(transactions) + } + // always do (3) + coinDB.storeTransactionsInDB(transactions) +} + +// storeTransactionsInDB generates CoinRecords from a slice of Transactions and +// stores them in the CoinDatabase's db. +// +// At a high level, this function: +// (1) creates coin records for the block's transactions +// (2) stores those coin records in the db +// +// Note: NOT included in the stencil. +func (coinDB *CoinDatabase) storeTransactionsInDB(transactions []*block.Transaction) { + for _, tx := range transactions { + cr := coinDB.createCoinRecord(tx) + txHash := tx.Hash() + coinDB.putRecordInDB(txHash, cr) + } +} + +// storeTransactionsInMainCache generates Coins from a slice of Transactions +// and stores them in the CoinDatabase's mainCache. It flushes the mainCache +// if it reaches mainCacheCapacity. +// +// At a high level, this function: +// (1) loops through the newly created transaction outputs from the Block's +// transactions. +// (2) flushes our cache if we reach capacity +// (3) creates a coin (value) and coin locator (key) for each output, +// adding them to the main cache. +// +// Note: NOT included in the stencil. +func (coinDB *CoinDatabase) storeTransactionsInMainCache(transactions []*block.Transaction) { + for _, tx := range transactions { + // get hash now, which we will use in creating coin locators + // for each output later + txHash := tx.Hash() + for i, txo := range tx.Outputs { + // check whether we're approaching our capacity and flush if we are + if coinDB.mainCacheSize+uint32(len(tx.Outputs)) >= coinDB.mainCacheCapacity { + coinDB.FlushMainCache() + } + // actually create the coin + coin := &Coin{ + TransactionOutput: txo, + IsSpent: false, + } + // create the coin locator, which is they key to the coin + cl := CoinLocator{ + ReferenceTransactionHash: txHash, + OutputIndex: uint32(i), + } + // add the coin to main cach and increment the size of the main cache. + coinDB.mainCache[cl] = coin + coinDB.mainCacheSize++ + } + } +} + +func (coinDB *CoinDatabase) updateSpentCoins(transactions []*block.Transaction) { + // loop through all the transactions from the block, + // marking the coins used to create the inputs as spent. + for _, tx := range transactions { + for _, txi := range tx.Inputs { + // get the coin locator for the input + cl := makeCoinLocator(txi) + // mark coins in the main cache as spent + if coin, ok := coinDB.mainCache[cl]; ok { + coin.IsSpent = true + coinDB.mainCache[cl] = coin + } else { + // if the coin is not in the cache, + // we have to remove the coin from the + // database. + txHash := tx.Hash() + // remove the spent coin from the db + coinDB.removeCoinFromDB(txHash, cl) + } + } + } } // removeCoinFromDB removes a Coin from a CoinRecord, deleting the CoinRecord |