Released: March 6th, 11:59pm EST
Due: March 24th, 11:59pm EST
In Chain, we learned how a cryptocurrency might implement a blockchain for block storage and validation. Now it's time to flesh out the rest of the system! You've heard about nodes, miners, and wallets in class (or will soon). Now's your chance to dive into the details of how these parts come together.
This project builds on the blockchain that you implemented for your first project. Don't worry if you didn't pass all of our tests on Chain – we've provided our solution for the blockchain.
Before you begin: After reviewing your feedback from Chain, we wanted to supply you with as much useful information up front as we could. While this handout is admittedly quite long, we expect Coin to take much less time than Chain.
Coin consists of several main components. Here's a quick overview:
For this assignment, you will implement the functions listed below. We recommend that you tackle them in the following order:
CalculateNonce
GenerateCoinbaseTransaction
Mine
HandleBlock
RequestTransaction
BroadcastTransaction
HandleMinerBlock
To see how difficult we think each function is, check out our grading breakdown.
The infamous miners. Greedy miners compile the transactions that give them the highest reward into blocks, which they then race to find a winning nonce (number only used once) for. As soon as they hear about another block, they have to start from scratch. It's a rat race.
Not only does the miner get to keep all transaction fees, they also get the minting reward for each block. Recall that minting rewards halve every 10,000 blocks. Our halvings occur more frequently.
Here's an overview of the relevant Miner
fields:
Config
: our miner’s configuration options. For this project, you'll only really have to worry about Config.NonceLimit
TxPool
all of the transactions that the miner can pull from to create their block. TxPool
is an enhanced priority queue that also keeps track of total priority, to know whether or not the miner should mine at all.MiningPool
: the transactions that the miner is actively mining in the current block. This is a subset of the TxPool
.PreviousHash
: the hash of the current last block on the main chain, so that our miner knows where to point to when constructing its new block.ChainLength
: used by the miner to calculate the current minting reward.SendBlock
: the channel that the node monitors to know when the miner has successfully mined a block. See node.Start
for how this is used.PoolUpdated
: the channel used to send information about TxPool updates to the miner.DifficultyTarget
: the level of difficulty that a winning nonce's resulting hash must beat.context.Context
?context
to send cancelleation signals to goroutines, which take on lives of their own once spawned.atomic
variables?atomic
package provides low-level atomic memory primitives useful for implementing synchronization algorithms – it basically allows us to read/write to shared memory without having to deal with mutexes.Important! A note on select
:
select
statement allows you to wait for results from multiple channels. It's really just a fancy version of the classic switch statement, where your cases are channel results.node.Start
. We usually combine select statements with infinite loops, so that a process just monitors relevant channels until it's terminated.header.Nonce
value until your resulting hash is less than the difficulty target.
0x0003eab192
(decimal: 65,712,530), you'll need to find a smaller hash, such as 0x0002ffffff
(decimal: 50,331,647). Note: our hashes are not technically hex, so we'll be comparing byte values.context
! How can we use it to monitor whether our nonce calculations are still a worthwhile pursuit?Done() <-chan struct{}
: we need to know whether we should stop mining.Id
?func CalculateMintingReward(c *Config, chainLength uint32) uint32
: we need to know what the reward is for this chain length!func (m *Miner) getInputSums(txs []*block.Transaction) ([]uint32, error)
: our miner uses its GetInputSums
channel to request this information from the node. Once it receives the request, the node will get the input sums for each transaction from the blockchain and return it to the miner via the InputSums
channelTxPool
's functions.Mining
field to true.MiningPool
.Mining
field to false. We're done.TxPool
)func (x *Bool) Store(v bool)
: this is how we update an atomic boolean.The vast majority of cryptocurrency holders trust a third-party to take care of their coins (e.g. services like Coinbase and Crypto.com). Even financial services like Venmo and Paypal are starting to join the party. Nodes, on the other hand, nearly always keep track of their own holdings. You'll learn more about wallets in a future lecture.
Here's an overview of the relevant Wallet
fields:
Config
: our wallet's configuration options. For this project, you only need to worry about Config.SafeBlockAmount
.Id
: our wallet's SimpleID
keeps track of the node's public and private keys. More complicated configurations–such as hierarachical deterministic wallets–are both more secure and more complex. We opted for keeping things simple: our wallet can only process transactions sent to our sole PublicKey
.TransactionRequests
: the channel that our wallet uses to send transaction requests to the Node
, which can then broadcast this transaction to its peers.CoinCollection
: this mapping contains all of our coins! This only contains coins that have been confirmed enough times (see config.SafeBlockAmount
).UnseenSpentCoins
: this mapping contains all of the coins that we've spent (by requesting a transaction) but haven't yet seen in a Block. Until we see our transaction actually included in a Block on the Chain, it hasn't happened!UnconfirmedSpentCoins
: Once we've seen one of our spent coins on the chain, we should move it from UnseenSpentCoins
to UnconfirmedSpentCoins
. Even though we've seen our coin included in a block, we still have to wait until enough confirmations (new blocks added to the chain) have occurred until we can safely spend our coin.UnconfirmedReceivedCoins
: Since we're an active cryto-user, we need a way to handle coins sent to us from our friends and peers, as well as the change from our own transactions! Like with UnconfirmedSpentCoins
, we'll have to wait until enough confirmations have occured until we can spend any Coin that we receive.Ok, I think I understand how wallets work. But what are CoinInfos
?
CoinInfos
are convenient structs that hold the information about TransactionOutputs necessary for turning them into TransactionInputs. You'll see them as either keys or values in all of the wallet's mappings.
Note: HandleBlock
and RequestTransaction
will likely be the two most time-consuming functions that you have to write (given the amount of steps involved). But we're pretty confident you'll feel like a superhuman when you do finish them
HandleBlock
moves coins (via coinInfos) between our various mappings. This function looks at all of the transactions in a block and sees if any of them involve the wallet's owner.coinInfo
from UnseenSpentCoins
to UnconfirmedSpentCoins
.UnconfirmedReceivedCoins
.UnconfirmedSpentCoins
and UnconfirmedReceivedCoins
. If any of the coins contained in those mappings have had enough confirmations, we can safely add them to our CoinCollection
(if received) or remove them (if spent).CoinCollection
and move them to UnseenSpentCoins
. Note: Our implementation doesn't allow users to (1) send multiple transactions with the same coin or (2) rebroadcast transactions that they haven't seen on the Chain in a while.TransactionRequests
channel. Otherwise, the node won't be able to broadcast it!func (txo *TransactionOutput) MakeSignature(id id.ID) (string, error)
func (id *SimpleID) GetPublicKeyString() string
generateTransactionInputs
and generateTransactionOutputs
, we did leave you the function headers as a guide to how you might want to approach this task.✨ Extra Credit Opportunity (10 bonus points):
Currently, our wallet can't recover from a fork. But it wouldn't take much additional functionality to do so!
handleFork
––and its use of coinDB's undoCoins
––what fields need to be updated when reverting?TestHandleFork
Nodes are the workhouses of cryptocurrencies like Bitcoin. They send information to their peers, run miners, and generally do a lot of validation.
Our node should take advantage of non-blocking goroutines to delegate work to our miner/wallet/blockchain and continue on with its duties. That way, the nodes won't have to wait for the miner/wallet/blockchain to finish before we can continue. Head back to Lab 1 for a refresher on goroutines.
Here are Node
fields that you have to be aware of for this project:
Config
: the node's configuration. You'll want to know whether this node has a minerBlockchain
, Wallet
, and Miner
: self-explanatorySeenTransactions
: the transactions that our node has either (1) heard about from other nodes or (2) broadcast itselfSeenBlocks
: same as SeenTransactions
, but for blocksPeerDb
: the database of peers that the Node is currently connected to.TXPool
somehow, since it can now include this transaction in one of its own blocks.PeerDb
. Make sure to wrap your RPC call in a goroutine!func (m *Miner) HandleTransaction(t *block.Transaction)
func (a *Address) ForwardTransactionRPC(request *pro.Transaction) (*pro.Empty, error)
: this function allows us to forward a transaction to other nodes.BroadcastTransaction
func (a *Address) ForwardBlockRPC(request *pro.Block) (*pro.Empty, error)
: this function allows us to forward a block to other nodes.This assignment is autograded, and you are able to run our test suite as many time as you like.
In addition, you should write your own tests when things aren't working! We have provided several helper functions in test/mocking_utils.go
and test/testing_utils.go
.
If you have written your own tests, you can 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.
Since many parts of this project make use of concurrency, you may find it a bit tricky to debug. Luckily, the GoLand debugger has support for debugging goroutines. You can read all about it here.
go get ./...
to install the project's dependencies.This assignment is out of 100 points. If you complete the extra credit, you may score up to 110 points. Your grade will be determined by passing the following test functions:
Test Function | Points |
---|---|
TestGenerateCoinbaseTransaction |
5 |
TestCalculateNonce |
5 |
TestCalculateNonceContext |
5 |
TestMine |
15 |
Total | 30 |
Test Function | Points |
---|---|
TestHandleBlock |
15 |
TestBasicTransactionRequest |
10 |
TestMultipleTransactionRequests |
15 |
TestRequestTransactionFails |
15 |
Total | 55 |
Test Function | Points |
---|---|
TestHandleMinerBlock |
15 |
Total | 15 |
Test Function | Points |
---|---|
TestHandleFork |
10 |
Total | 10 |