-
Notifications
You must be signed in to change notification settings - Fork 10
Voter registration 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, address, age, ID number, etc.)
- On the application, the citizen can choose to register with one of the parties, party #1 or party #2, or to not register with a party.
- 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 either accepted or rejected. If they are accepted, they are able to vote in national elections. If they chose to register with a political party, they are also able to vote in elections specific to that party. 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.
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, ID number, and which party they want to register with: party #1 or party #2.
- The only criteria for being eligible to vote are that the citizen is over 18 and has a valid name and ID number.
One possible approach to solving this problem is using states. Here is an example of what a state diagram for this program may look:
(Note that Party1 and Party2 are substates of CanVote.)
Redesign 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.
Here is, broadly, 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;
int whichParty;
Application(string name, int age, int idNum, int whichParty) {
this.name = name;
this.age = age;
this.idNum = idNum;
this.whichParty = whichParty;
}
transaction getAge() returns int {
return this.age;
}
transaction getIdNum() returns int {
return idNum;
}
transaction getWhichParty() returns int {
return this.whichParty;
}
}
contract Citizen {
Citizen() {
//TODO
}
state Unregistered {
transaction submitApplication(string name, int age, int idNum, int whichParty) {
Application app = new Application(name, age, idNum, whichParty);
//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 checkParty() {
//assume that if they listed a number besides 1 or 2
//it means they chose not to register with a party
if (app.getWhichParty() == 1) {
//TODO
}
if (app.getWhichParty() == 2) {
//TODO
}
}
transaction submitApplication(string name, int age, int idNum, int whichParty) {
//TODO: this transaction handles what happens if a citizen who is
//registered to vote attempts to submit a new application.
}
state Party1 {
}
state Party2 {
}
}
state CannotVote {
transaction submitApplication(string name, int age, int idNum, int whichParty) {
//TODO: this transaction handles what happens if a citizen who is
//ineligible to vote attempts to submit a new application.
}
}
}
Provided below are several different options for writing state transitions in Obsidian. Examine them and modify your code 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?
contract C {
state Start {
transaction t(money m) {
this <- S1{m1 = m}
then toS2();
}
}
state S1 {
money m1;
transaction toS2() {
this <- S2{m2 = m1};
}
}
state S2 {
money m2;
}
}
contract C {
state Start {
transaction t(money m) {
->S1(m)
}
}
state S1 {
money m1;
S1(money m) { //state constructor
m1 = m;
->S2(m1);
}
}
state S2 {
money m2;
S2(money m) { //state constructor
m2 = m;
}
}
}
contract C {
state Start {
transaction t(int x) {
->S1({x1 = x})
in S1 {
// Asserts that the state is S1 and either
// proceeds or throws an exception.
->S2({x2 = x1})
}
in S2 {
// Actually, we don't 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.
// This code happens to be unreachable
// (for which the compiler gives an error).
}
}
state S1 {
int x1;
}
state S2 {
int x2;
}
}
You are provided with an unfinished Obsidian contract below. Your task is to implement transactions 'a' and 'b' with the correct behavior, described in the comments of the transactions. Implement these transaction 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.
main contract C {
state Start {
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.
*/
//TODO
}
}
}
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).
*/
//TODO
}
}
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;
}
}
}
- Which option did you prefer, and why?
- What, if anything, did you learn/realize about each option that was not obvious in part 4?