Released: February 6th, 11:59pm EST
Due: February 26th, 11:59pm EST
Chain is an example storage system (a blockchain) for a cryptocurrency. Chain is modeled after Bitcoin's storage system, though heavily simplified. The goal of this project is to give you a taste of the many working components of a blockchain.
Chain consists of several main components. Here's a quick overview:
Here's a gear-up video to get you started!
For this assignment, you will implement the functions listed below. We recommend that you tackle them in the following order:
StoreBlockRecord
GetBlockRecord
WriteBlock
WriteUndoBlock
StoreBlock
UndoCoins
HandeBlock
To see how difficult we think each function is, check out our grading breakdown.
Good question. Normally, when using protobufs, you would probably NOT create redundant local structs. After all, if you have a well-defined message format, why would you rewrite the same format? The answers is that these redundant local structs are easier to work with. That means less headaches for you.
// StoreBlockRecord stores a block record in the block info database.
func (blockInfoDB *BlockInfoDatabase) StoreBlockRecord(hash string, blockRecord *BlockRecord)
As suggested by this function's name, the purpose of StoreBlockRecord
is to store block records in the BlockInfoDatabase.
In order to do this, we'll need to 1) encode the BlockRecord
as a protobuf, 2) convert the protobuf to the correct format and type (byte[]
) so that it can be inserted into the database, and 3), put the block record into the database.
func EncodeBlockRecord(br *BlockRecord) *pro.BlockRecord
func Marshal(m Message) ([]byte, error)
. See documentation here.func (db *DB) Put(key, value []byte, wo *opt.WriteOptions) error
. See documentation here.// GetBlockRecord returns a BlockRecord from the BlockInfoDatabase given
// the relevant block's hash.
func (blockInfoDB *BlockInfoDatabase) GetBlockRecord(hash string) *BlockRecord
This function retrieves block records from the database, and returns the block record.
In order to do this, we'll basically do the reverse of what we did in StoreBlockRecord
. Specifically, we'll 1) retrieve the block record from the database, 2) Convert the byte[]
returned by the database to a protobuf, and 3) convert the protobuf back into a BlockRecord
.
func DecodeBlockRecord(pbr *pro.BlockRecord) *BlockRecord
func Unmarshal(b []byte, m Message) error
. See documentation here.func (db *DB) Get(key []byte, ro *opt.ReadOptions) (value []byte, err error)
. See documentation here and be sure to account for the corresponding I/O errors.// WriteBlock writes a serialized Block to Disk and returns
// a FileInfo for storage information.
func (cw *ChainWriter) WriteBlock(serializedBlock []byte) *FileInfo
// WriteUndoBlock writes a serialized UndoBlock to Disk and returns
// a FileInfo for storage information.
func (cw *ChainWriter) WriteUndoBlock(serializedUndoBlock []byte) *FileInfo
Note: These two functions are nearly identical. Think about which ChainWriter fields you're modifying.
cw.CurrentBlockFileNumber
by one.dataDirectory/fileName_fileNumber.<file extension>
ChainWriter
fields as appropriatewriteToDisk(fileName, serializedBlock)
// StoreBlock handles storing a newly minted Block. It:
// (1) removes spent TransactionOutputs
// (2) stores new TransactionOutputs as Coins in the mainCache
// (3) stores CoinRecords for the Transactions in the db.
// We recommend you write a helper function for each subtask.
func (coinDB *CoinDatabase) StoreBlock(transactions []*block.Transaction)
makeCoinLocator(txi *block.TransactionInput)
coinDB.removeCoinFromDB(txHash string, cl CoinLocator)
coinDB.FlushMainCache()
coinDB.createCoinRecord(tx *block.Transaction, active bool)
coinDB.putRecordInDB(txHash string, cr *CoinRecord)
// UndoCoins handles reverting a Block. It:
// (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)
coinDB.removeCoinFromRecord(cr *CoinRecord, outputIndex uint32)
Delete(key []byte, wo *opt.WriteOptions)
coinDB.getCoinRecordFromDB(txHash string)
coinDB.addCoinsToRecord(cr *CoinRecord, ub chainwriter.UndoBlock)
coinDB.putRecordInDB(txHash string, cr *CoinRecord)
// HandleBlock handles a new Block. At a high level, it:
// (1) If valid, appends the Block onto the active chain.
// (2) Stores the Block and resulting Undoblock to Disk.
// (3) Stores the BlockRecord in the BlockInfoDatabase.
// (4) Handles a fork, if necessary.
// (5) Updates the BlockChain's fields.
func (bc *BlockChain) HandleBlock(block *block.Block)
blockchain.go
that we've crafted to make your life easier. Make sure you understand what each one is doing!unsafeHashes[0]
is the least recent hash. This means that when updating unsafeHashes
, we pop from the front (but should we always pop?) and push to the back of the slice. The helper function getForkedBlocks
handles unsafeHashes
as such. If you decide to keep track of unsafe hashes differently, getForkedBlocks
will not behave as expected. In that case, feel free to update getForkedBlocks
!unsafeHashes
and maxHashes = 6
as a heuristic for how many blocks back we need to check to find the origin of a fork. While in real life there is an infinitesimal chance that a Bitcoin fork requires a node to search back beyond 6 blocks, it's still possible! Such an event would (unfortunately) break our implementation.UndoCoins()
) is somewhat complicated and long. If you're stuck, collaborate with your peers or come to TA hours!This assignment is autograded, and you are able to run our test suite as many times as you like on Gradesceope.
In addition, you should write your own tests when things aren't working! We have provided several helper functions in test/utils.go
.
To test your project with the tests you've written, cd
into test
and run go test
. This will let you know which tests are failing, and why. It will likely be more convenient to run individual tests, which you can do using the GoLand UI.
Note: Running the autograder directly on the stencil will cause a system panic, since the application relies on the dereferencing of things like block records (which will be null if you have yet to implement anything). We have configured it such that you will still see the error message in cases like this, but it may not be as useful as those that were written by the staff.
go get ./...
to install the project's dependencies.This assignment is out of 140 points. Your grade will be determined by passing the following test functions:
Test Function | Points |
---|---|
TestHandleAppendingBlock |
5 |
TestHandleInvalidBlock |
5 |
TestHandle50Blocks |
5 |
TestHandleForkingBlock |
10 |
TestHandle2Forks |
10 |
Total | 35 |
Test Function | Points |
---|---|
TestGetSameRecord |
5 |
TestGetDifferentRecords |
5 |
Total | 10 |
Test Function | Points |
---|---|
TestReadBlock |
5 |
TestReadUndoBlock |
5 |
TestRead100Blocks |
5 |
TestStoreOrphanBlock |
5 |
TestStoreBlock |
5 |
TestWriteBlock |
5 |
TestWriteBlockSwitchesFiles |
5 |
TestWriteUndoBlock |
5 |
TestWriteUndoBlockSwitchesFiles |
5 |
Total | 45 |
Test Function | Points |
---|---|
TestGetCoin |
5 |
TestUndoCoins |
5 |
TestUpdateSpentCoinsInCache |
5 |
TestUpdateSpentCoinsNotInCache |
5 |
TestValidateValidBlock |
5 |
TestValidateInvalidBlock |
5 |
TestCacheFlushedWhenFull |
10 |
TestCoinsStoredInCacheAndDB |
10 |
Total | 50 |