-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add blockchain update mechanism, add request timeout to Peer #115
Conversation
app/app.go
Outdated
|
||
// SyncBlockchain updates the local copy of the blockchain by requesting missing | ||
// blocks from peers. Returns error if we are not connected to any peers. | ||
func (a *App) SyncBlockchain() error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick, lets use SyncBlockChain
? (since the blockchain itself is pascal case)
app/app.go
Outdated
Head: blockchain.NilHash, | ||
} | ||
|
||
genisisBlock := blockchain.Genesis(user.HotWallet.Wallet.Public(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Im not in love with the User
API at the moment. I have some work to do there, this works for now.
app/app.go
Outdated
} | ||
|
||
// Pick a peer to send the request to (this won't always be the same one) | ||
p := a.PeerStore.Get(peerAddrs[0]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without diving deep into the peer package, its very difficult to appreciate what this line is supposed to be doing. For example, how is it that this generates a different peer each loop? Shouldn't the addresses be abstracted away at this level? Why do I care which peer I'm connecting to (or what their address is)? I just want to send block requests out to the network.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea I agree, I think there should just be a GetRandom
function for this. But we do need to select a peer to send the request to, because broadcasting a request doesn't really make much sense.
app/app.go
Outdated
continue | ||
} | ||
|
||
valid, validationCode := consensus.VerifyBlock(a.Chain, newBlock) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👏 thorough!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Drive by comments. Looking good so far 🎉
One general note about the approach we're using here to download the block chain, it seems like an unfortunately serial way to do it. I totally buy the idea of looking for the next block based on hash, but we basically do zero asynchronous torrenting from multiple peers like this (which was the goal I believe?)
@@ -43,6 +43,16 @@ func (bh *BlockHeader) Marshal() []byte { | |||
return buf | |||
} | |||
|
|||
// Equal returns true if all the fields (other than ExtraData) in each of | |||
// the BlockHeaders match, and false otherwise. | |||
func (bh *BlockHeader) Equal(otherHeader *BlockHeader) bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reads nicer as Equals
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was going to make it Equals
but that's not the Go way 🤷♂️.
blockchain/blockchain.go
Outdated
@@ -69,7 +69,7 @@ func (bc *BlockChain) ContainsTransaction(t *Transaction, start, stop uint32) (b | |||
return false, 0, 0 | |||
} | |||
|
|||
// CopyLocalBlockByIndex returns a copy of a block in the local chain by index. | |||
// CopyBlockByIndex returns a copy of a block in the local chain by index. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤦♂️ 🙏
app/app.go
Outdated
errChan := make(chan *msg.ProtocolError) | ||
|
||
// Define a handler for responses to our block requests | ||
blockResponseHandler := func(blockResponse *msg.Response) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This for example can be stubbed out for easy testing.
app/app.go
Outdated
|
||
// SyncBlockchain updates the local copy of the blockchain by requesting missing | ||
// blocks from peers. Returns error if we are not connected to any peers. | ||
func (a *App) SyncBlockchain() error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a huge function and very difficult to test. Is it possible to break it up a bit?
peer/peer.go
Outdated
@@ -159,11 +161,50 @@ func (p *Peer) Dispatch() { | |||
|
|||
// Request sends the given request over this peer's Connection and registers the | |||
// given response hadnler to be called when the response arrives at the dispatcher. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handler* - realize its old stuff
@@ -0,0 +1,253 @@ | |||
package peer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, I think its a good idea to move towards the testify
package and use assert statements. They really help with debugging, giving useful explanations of the failure (especially helpful when other people try to make changes in your code!)
Moved globals in the App package into the App struct. Changed CopyBlockByIndex() to CopyBlockByLastBlockHash() Change User to have Wallet instread of HotWallet Added mining integration to App Updated SyncBlockChain() Added App tests Updated tests and test_utils Added miner command to console for checking and toggling miner status Added mine flag to Cobra config
App: - Add blockchain download then target is specified - Fix bugs in RequestHandler - Fix block decoding bugs in SyncBlockChain Blockchain: - Add DecodeBlockJSON - Change CopyBlockByBlockHash to GetBlockByBlockHash Cmd/Conf: - Change no-miner to miner, so we don't mine by default Conn: - Replace TestConn with BufConn Consensus: - Fix Cloudbase transaction validation bugs Message: - Add BadRequest protocol error - Update decoding so it handles big numbers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice work.
app/app.go
Outdated
@@ -204,11 +231,19 @@ func (a *App) PushHandler(push *msg.Push) { | |||
} | |||
|
|||
// getLocalChain returns an instance of the blockchain. | |||
func getLocalChain() *blockchain.BlockChain { | |||
func getLocalChain(user *User) *blockchain.BlockChain { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is user a param?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like the function is overloaded with two separate situations:
- we want to start a new chain with a genesis block
- we want to continue an old chain by restoring from storage
We should have two separate functions for these two situations.
app/app.go
Outdated
genisisBlock := blockchain.Genesis(user.Wallet.Public(), | ||
consensus.CurrentTarget(), consensus.StartingBlockReward, []byte{}) | ||
|
||
bc.AppendBlock(genisisBlock) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo genisis -> genesis
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sad thing is I meant to write it like that... I thought that was how it was spelled :(.
app/app.go
Outdated
chainChanged, err := a.SyncBlockChain() | ||
if chainChanged && miner.IsMining() { | ||
miner.StopMining() | ||
go a.Mine() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
worthwhile to have a RestartMining
method? out of scope for this PR but make an issue for it if it's worth the time
// blocks from peers. Returns true if the blockchain changed as a result of | ||
// calling this function, false if it didn't and an error if we are not connected | ||
// to any peers. | ||
func (a *App) SyncBlockChain() (bool, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can probably just have this return false if no peers and omit the error return
I'd also recommend checking for no peers before doing anything else and exiting early if we aren't connected to anything
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's possible, whoever unlikely, that all our peers disconnect from us while this function is running, in which case the blockchain might have been partially updated, but the update is incomplete. Checking if we are connected to peers before calling this would not guarantee that we have peers to make requests to throughout the duration of the function. So I think we still need both return values here.
func (ps *PeerStore) GetRandom() *Peer { | ||
ps.lock.RLock() | ||
defer ps.lock.RUnlock() | ||
for _, p := range ps.peers { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lol
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, lots going on here! Couple notes:
-
assert
everywhere, literally saves you 50 lines in this pr and saves everyone else some serious head scratching. There is no reason not to use it at this point. -
Long functions are hard to test, especially in the app package which already suffers from low coverage (it makes it really hard for others to make changes to your code in the future, the
Run
function is totally untested, and now theSyncBlockChain
logic appears to be going the same way). -
The scope is so far blown out, I don’t even see where this PR addresses ticket 111? Three separate PRs here would be better than one if you want legit feedback - its actually difficult to sign off on all of this because it takes so long to grok it all! Plus you expose yourself to too many TODO’s from onlookers. Takes forever to merge.
Theres lots about this PR that is sweet, but I 🙏 to see two things before we merge:
- Reduce the complexity of
SyncBlockChain
so that it doesn't go forever untested. Test it piece by piece. - Use
assert
. It would take 10mins to swap theset.FailNow
's out.
EDIT: Im noticing that this is probably more geared towards issue 118? In that case the scope makes more sense! I thought this was literally random coding lol.
Pool *pool.Pool | ||
blockQueue chan *blockchain.Block | ||
transactionQueue chan *blockchain.Transaction | ||
quitChan chan bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
app/app.go
Outdated
@@ -80,6 +87,11 @@ func Run(cfg conf.Config) { | |||
// stream in. Kick off a worker to handle requests and pushes. | |||
go a.HandleWork() | |||
|
|||
if config.Mine { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This strikes me as a user-level configuration, but now I'm actually leaning towards one config file to rule them all. Perhaps we can move the BlockSize
variable into config
as well, then we can maybe do away with the user abstraction altogether 🤔 (Another PR)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Cobra library we're using already supports config files by default.
log.Debug("Received block request") | ||
|
||
// Block is requested by block hash. | ||
hashBytes, err := json.Marshal(req.Params["lastBlockHash"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
app/app.go
Outdated
// TODO: Look for local chain on disk. If doesn't exist, go rummaging | ||
// createBlockchain returns a new instance of a blockchain with only a genesis | ||
// block. | ||
func createBlockchain(user *User) *blockchain.BlockChain { | ||
// around on the internets for one. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might just blow this line away? 🔫
app/app.go
Outdated
func (a *App) Mine() { | ||
log.Info("Starting miner") | ||
for { | ||
// Make a new block form the transactions in the transaction pool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo
@@ -7,9 +7,9 @@ import ( | |||
"github.com/ubclaunchpad/cumulus/pool" | |||
) | |||
|
|||
func createNewTestBlockRequest(lastBlock interface{}) *msg.Request { | |||
func createNewTestBlockRequest(lastBlockHash interface{}) *msg.Request { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
blockchain/block_test.go
Outdated
} | ||
|
||
block2 := NewTestBlock() | ||
if (&block1.BlockHeader).Equal(&block2.BlockHeader) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use assert
. Its less lines, easier to read, easier to get other people to work with your unit tests. Just far superior!
b.Transactions = make([]*Transaction, len(blk.Transactions)) | ||
copy(b.Transactions, blk.Transactions) | ||
return &b, nil | ||
// GetBlockByLastBlockHash returns a copy of the block in the local chain that |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not a copy
|
||
// RollBack removes the last block from the blockchain. Returns the block that | ||
// was removed from the end of the chain, or nil if the blockchain is empty. | ||
func (bc *BlockChain) RollBack() *Block { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting
return nil | ||
} | ||
prevHead := bc.LastBlock() | ||
bc.Blocks = bc.Blocks[:len(bc.Blocks)-1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fancy golang pop:
prevHead, bc.Blocks = bc.Blocks[len(bc.Blocks)-1], bc.Blocks[:len(bc.Blocks)-1]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yours is probably more readable 😆
app/app.go
Outdated
@@ -343,15 +338,22 @@ func (a *App) Mine() { | |||
} | |||
} | |||
|
|||
// RestartMiner restarts the miner only if it is running when this is called. | |||
func (a *App) RestartMiner() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great!
Status
Ready for review
Related Issue
#111
Description
SyncBlockchain
function to iteratively request the next block from random peers. It is called when we boot with a target peer to connect to (download blockchain), and when we received a new block via a push message that is not valid wrt our blockchain. This way, if we are on the wrong fork of the blockchain, we miss a block, or we go offline for a while an then come back we will recognize that our blockchain is out of date and update it.mine
flag. Mining is turned off my default. The miner restarts every time the blockchain is updated.BadRequest
(400) error code to messageWIKI Updates
Todos
General: