Skip to content

Voter registration exercise

celestebarnaby edited this page Jul 19, 2017 · 46 revisions

Voter Registration

(See the Google Doc for the updated version of this exercise.)

You are tasked with designing a voter registration system for a small democratic nation. Every citizen of this country has a valid ID card with a corresponding number. The country maintains a ledger of the ID numbers of all citizens. The process of registering to vote is as follows:

  • A citizen who is not registered to vote can fill out and submit a registration application with certain pertinent information (e.g. name, age, ID number, etc.)
  • The application is then processed, in order to ascertain that the citizen meets all the requirements necessary in order to vote (e.g. they are old enough, they have a valid name and ID number, etc.). While the application is being processed, the citizen cannot vote, and they cannot submit another application.
  • Once an application has been processed, the citizen is either accepted or rejected. If accepted, they are able to vote in national elections. A citizen who has been accepted is not able to submit a new registration application.
  • If a citizen was rejected, they are not able to vote in any elections, and they cannot submit a new registration application.

Part 1

Design, using pseudocode, a program that takes a citizen's registration application and takes appropriate action based on the status of that citizen. Do not attempt to write code that resembles any particular language. We want to see the kind of code you would want to be able to write. The goal here is to see how people naturally go about solving problems like this. You can make the following assumptions:

  • There is a ledger of the names and ID numbers of all citizens, which you (the developer of the program) have access to.
  • On the registration form, a citizen fills in their name, age, and ID number.
  • The only criteria for being eligible to vote are that the citizen is over 18 and has a valid name and ID number.

Part 2

One possible approach to solving this problem is using states. Here is an example of what a state diagram for this program may look like:

Modify your system to include the specific states and state transitions shown in this diagram. State-specific actions should only be possible in the appropriate state. For instance, the information in the ledger of citizens should only be accessed/used in the processing state.

Part 3

(You can now look at the Obsidian tutorial.) Below is what an implementation of this system may look like in Obsidian. This implementation defines states and transactions, but transitions between states in the Citizen contract are still missing. Your task here is to add state transitions to this code. You may write them in whatever way makes the most sense for the design of the program. The result of calling submitApplication should be that the citizen is in either the CanVote or CannotVote state. Places where you may want to add state transitions are marked with //TODO.

contract Application {
    string name;
    int age;
    int idNum;

    Application(string name, int age, int idNum) {
        this.name = name;
        this.age = age;
        this.idNum = idNum;
    }

    transaction getAge() returns int {
        return this.age;
    }

    transaction getIdNum() returns int {
        return idNum;
    }

}

contract Citizen {

    Citizen() {
      //TODO
      
    }

    state Unregistered {

        transaction submitApplication(string name, int age, 
                                      int idNum) {
            Application app = new Application(name, age, idNum);
            //TODO
        }
    }

    state Processing {
        Application app;

        transaction processApplication() {
            bool canVote = false;
            // In this transaction we access the ledger and check whether 
            // the name, age, and ID number listed on app are valid.
            // If they are, canVote is set to true.
            // …

            if (canVote) {
                //TODO
     
            } else {
               //TODO
            }
        }
    }

    state CanVote {
        Application app;

        transaction submitApplication(string name, int age, 
                                      int idNum) {
            //TODO: this transaction handles what happens if a citizen who is
            //registered to vote attempts to submit a new application.
            }
    }

    state CannotVote {

        transaction submitApplication(string name, int age, 
                                      int idNum) {
            //TODO: this transaction handles what happens if a citizen who is
            //ineligible to vote attempts to submit a new application.
        }
    }
}

Part 4

Provided below are several different options for writing state transitions in Obsidian, as well as an unfinished Obsidian contract. Your task is to implement transactions a and b of the contract with the correct behavior, described in the comments of the transactions. Implement these transactions once using each style of state transition (so three times total). You can add additional code outside of transactions a and b if it is necessary. Places where you should add transitions are marked with //TODO.

Option 1

contract C {
    state Start {
        transaction t(int x) {
            ->S1{x1 = x};
            toS2();
        }
    }

    state S1 {
        int x1;

        transaction toS2() {
            ->S2({x2 = x1});
        }
    }

    state S2 {
        int x2;
    }
}

With this option, you are allowed to use a transaction or field that is defined in a state you are not currently in, if you have transitioned to that state. For instance, you are allowed to use the toS2 transaction inside the Start state, even though that transaction is defined with S1. This is because you have transitioned to S1 in the previous line.

Option 2

contract C {
    state Start {
        transaction t(int x) {
            ->S1{x1 = x}
              then toS2();
        }
    }

    state S1 {
        int x1;

        transaction toS2() {
            ->S2{x2 = x1};
        }
    }

    state S2 {
        int x2;
    }
}

With this option, you can only use transactions or fields that are defined within the state you are currently in - thus, a state transition must be the final line of a transaction.

The only exception to this is the optional indented then statement that follows a state transition, wherein you can call one transaction in the state you have just transitioned to.

Option 3

contract C {

    state Start {
        transaction t(int x) {
            ->S1{x1 = x}
            in S1 {
                ->S2{x2 = x1}
            }
            in S2 {
            // We do not need to do anything in this case, 
            // but this is here as an example.
            }
            x = … // This code runs only if the current state is Start. 
                  // Again, we do not do anything in this case,
                  // but this is here as an example.
        }
    }

    state S1 {
        int x1;
    }

    state S2 {
        int x2;
    }
}

With this option, we have an in {state} block within which we can use transactions and fields defined in the corresponding state. In this example, you are obviously in state S1after the line ->S1{x1 = x}, so the in S1 block is executed, and the in S2 block is not necessary.

Here is the unfinished Obsidian contract:

main contract C {

    C() {
        ->Start;
    }

    state Start {
        transaction a(int x, bool whichState) returns int {
            /* Here is what this transaction should do:
             * 1. Transitions to S1, with S1's x1 field equal to x, 
             *    and whichState1 field equal to whichState.
             * 2. Calls transaction b.
             * 3. - If you are in S2 after calling b, 
             *      call transaction c and return the resulting int.
             *    - If you are in S3 after calling b, 
             *      call transaction d and return the resulting int.
             */
        }
    }

    state S1 {
        int x1;
        bool whichState1;
        
        transaction b() {
            /* This transaction should transition to S2 if 
             * whichState1 is equal to true (with S2's x2 field equal to x1),
             * and transition to S3 if whichState1 is equal to false
             * (with S3's x3 field equal to x1). 
             */ 
            }
    }

    state S2 {
        int x2;

        transaction c() returns int {
            x2 = x2 * 2;
            return x2;
        }
    }

    state S3 {
        int x3;

        transaction d() returns int {
            x3 = x3 * 3;
            return x3;
        }
    }
}

Part 5

Modify your code from part 3 to use one of these options for state transitions, then answer the following questions:

  • Which option did you pick, and why?
  • Is there anything confusing about any of these options?