SI 486I Spring 2022 / Labs


This is the archived website of SI 486I from the Spring 2022 semester. Feel free to browse around; you may also find more recent offerings at my teaching page.

Lab 4: Updating to the longest path

This week we will update your node’s server to also effectively handle /push requests, using caching and following the “longest” path rule to resolve potential ambiguities on which block should count as the head.

At the end of this lab, our nodes should start to converge on the same blockchain for the entire class. We will in fact have a complete distributed blockchain, missing only the (crucial!) proof of work aspect.

Part 0: Get your server.py working

You need to get the server from last week’s lab working perfectly first, to properly handle /head and /fetch/<hash> GET requests, using cached blocks and current.json. Talk to your classmates, test your code, and work together to get up to speed!

Background

POST requests in Flask

So far we have only dealt with GET requests, which makes sense when an HTTP request is mostly returning data to the client.

When we need HTTP to take a lot of data from the client, the HTTP POST protocol makes more sense. This is what is used, for example, when you fill out a web form and click “submit” - the form data is encoded to a POST request, with each field in the form as a separate entry.

To handle a POST request in Flask, you specify this in the function decorator with the methods to @app.route, like

@app.route('/path', methods=['POST'])
def foo():
    # your code here

In this case, the server will only respond to POST requests at the url server/path. (You could add 'GET' to the list of methods if you want it to respond to GET and POST requests at the same path.)

Of course, you probably want your handler function to be able to get the data that is encoded in the POST request. To do this, Flask provides a Python dict accessible via flask.request.form inside the function. So, for example, if we were writing a web form handler that anticipates a form with name and email strings, we might write something like

@app.rout('/signup', methods=['POST'])
def signup():
    name = flask.request.form['name']
    email = flask.request.form['email']
    with open('spam_list.txt', 'a') as fout:
        print(email, file=fout)
    return "<p>Hello " + name + ". I will send you spam now.</p>"

Note, you should never trust anything you read on the internet, and that especially includes HTTP POST data. In the above method, we should probably check first that 'name' and 'email' actually show up in the dictionary (to avoid throwing errors), and also check that the both fields are properly formatted etc.

Part 1: Check servers

Update your check_peers.py from two labs ago to also look at what’s running on port 5002. You should hopefully see a lot of working servers here!

Part 2: Handling POST requests to push

So far, your server.py server should be set on port 5002 and effectively handle HTTP GET requests to /head and /fetch/<hash> based on the stored cached files.

Now, we will make this complete by handling POST requests to /push. Read the background section above on how to handle POST requests, and look back on Lab 1 and your send_chat.py program to remember how the /push request will be formatted.

Here are some guidelines you should follow. However, I am intentionally not specifying every detail of this needs to work. You should think and reason and discuss: what needs to be checked, and when should the cached blocks and current.json files be updated?

Guidelines and tips:

  • All aspects of the incoming block should be verified (similar to in show_chat.py and check_peers.py). If the block is invalid, return an HTTP error code and reject the block.
  • Only consider the block valid if it is a genesis block, or if your server already has the previous block cached.
  • Any valid block should be added to your server’s block cache, even if it does not become the new head.
  • Update the head (via current.json) only when the new block makes a longer chain than the current head block.
  • As long as the incoming block is valid, even if the head does not get updated, return HTTP response code 200.
  • Your server should be robust to any kind of malformed or incorrect data coming in, and should return sensible error messages along with appropriate HTTP failure status codes, whenever it rejects a block.

Your task

Modify server.py so that it handles /push requests using HTTP POST, with block verification, caching, and updating the head hash only when there is a new longest chain.

Part 3: Adding to the longest chain

Previously, your send_chat.py program created a new block and sent it to just one server. Now, you should modify this so that it sends the new blocks (and any other required blocks) to all servers in hosts.txt, on port 5002.

But there is an ambiguity here: if you want to just create a single block and send to all servers, where should you get the head hash to use in creating this new block?

The answer is that you want to add to the longest chain of all existing servers. So, before creating the new block, you should check all servers in hosts.txt on port 5002 to see which one(s) have the longest valid chain. (This should be very similar logic to what you already have from check_peers.py.) If there are multiple competing longest chains, pick any one of them.

Now, your new block should use the prev_hash from the longest chain, and make a new block on top of that with whatever message the user types in at the terminal.

Next, you need to publish this new block by making appropriate /push requests. For starters, just try to /push the new block to all servers in hosts.txt. At least any correct servers that were serving the longest chain should now be updated.

Make sure your program is robust to the many kinds of failures that may occur when dealing with other servers that may be incorrectly configured etc. At the end, your program should list the names of all servers that successfully accepted the new block pushed.

Your task

Modify send_chat.py so that it adds the new block to the longest chain of all running servers in hosts.txt. Your program should still read a desired chat message on the terminal, and should also print a list of all successful hosts that were pushed to at the end.

Part 4: Pushing ALL needed blocks to other servers

The previous part will not lead to consensus in the blockchain between all the servers, because it will be impossible for nodes not on the longest chain to ever “catch up”.

Imagine that some server is missing the very last block for the longest chain, and then someone runs send_chat.py to create and push a new block. Well, this server that was one block behind, will reject the pushed block because it doesn’t have the previous block. Now that server is two blocks behind the longest chain, and it will never rejoin the consensus without some manual human intervention. Terrible!

To remedy this, your send_chat.py should be more intelligent in pushing blocks to servers. After creating the new block at the end of the longest chain, your program should push any needed blocks to each server in hosts.txt so that any correctly-running servers should accept your new block.

One way to do this (but not the only way!) would be to recursively back-track, following this logic:

  • First try to push the new block
  • If that fails, recursively try to push the previous block
  • Keep going backwards in your own blockchain, until the first time a block is successfully pushed to the given server.
  • Then go forwards back down the blockchain and push each block in order, which should now all succeed, ending with the new block.

Again, there can be failures here if some server is not running correctly. For example, you could try pushing every block down to the genesis block, and they all fail! Your program should handle such failures gracefully, and still print a list at the end of which servers eventually succeeded.

Your task

Modify send_chat.py further so that it fully attempts to update all nodes in hosts.txt with any needed blocks.

Testing: To test this part, get a classmate to manually remove a few recent blocks from their server by modifying current.json and deleting some block files in their cache folder. Then try running your send_chat and check that it sends all needed blocks to fully update their server. Testing will be challenging here, but worth it!

OPTIONAL Part 5: Gentle pen testing

There are many attacks available on our blockchain network right now. Try some out, but please be gentle. In particular, if you find some ways to take over the entire blockchain, please don’t fully do it and take everything down. And denial of service attacks should also be off limits.

If you can find some flaw in the way verification is working for one of the hosts, add a chat message to the blockchain calling them out for it! Be sure to check the chat for your own hostname to see if there are any flaws for you to try and fix.