diff --git a/auth/jsonfield.py b/auth/jsonfield.py
index ff63893e3a44cf841d1dd32350c36970cdc5c6f6..44574a14d973e88b0b12fde42df2c5b33d6fcfcd 100644
--- a/auth/jsonfield.py
+++ b/auth/jsonfield.py
@@ -30,18 +30,18 @@ class JSONField(models.TextField):
     def to_python(self, value):
         """Convert our string value to JSON after we load it from the DB"""
 
-        # must handle the case where the value is already ready
         if self.json_type:
             if isinstance(value, self.json_type):
                 return value
-        else:
-            if isinstance(value, dict) or isinstance(value, list):
-                return value
+
+        if isinstance(value, dict) or isinstance(value, list):
+            return value
 
         if value == "" or value == None:
             return None
 
         parsed_value = json.loads(value)
+
         if self.json_type and parsed_value:
             parsed_value = self.json_type.fromJSONDict(parsed_value, **self.deserialization_params)
                 
@@ -58,7 +58,7 @@ class JSONField(models.TextField):
         if value == None:
             return None
 
-        if self.json_type or hasattr(value,'toJSONDict'):
+        if self.json_type and isinstance(value, self.json_type):
             the_dict = value.toJSONDict()
         else:
             the_dict = value
diff --git a/helios/datatypes/__init__.py b/helios/datatypes/__init__.py
index 4735ba97a3b2cc55f191fc0597135e4e87af6bfe..8674a54462773108e41a654fe37ec526a82f5e24 100644
--- a/helios/datatypes/__init__.py
+++ b/helios/datatypes/__init__.py
@@ -41,6 +41,33 @@ def recursiveToDict(obj):
     else:
         return obj.toDict()
 
+def get_class(datatype):
+    # already done?
+    if not isinstance(datatype, basestring):
+        return datatype
+
+    # parse datatype string "v31/Election" --> from v31 import Election
+    parsed_datatype = datatype.split("/")
+    
+    # get the module
+    dynamic_module = __import__(".".join(parsed_datatype[:-1]), globals(), locals(), [], level=-1)
+    
+    if not dynamic_module:
+        raise Exception("no module for %s" % datatpye)
+
+    # go down the attributes to get to the class
+    try:
+        dynamic_ptr = dynamic_module
+        for attr in parsed_datatype[1:]:
+            dynamic_ptr = getattr(dynamic_ptr, attr)
+        dynamic_cls = dynamic_ptr
+    except AttributeError:
+        raise Exception ("no module for %s" % datatype)
+    
+        
+    return dynamic_cls
+        
+
 class LDObjectContainer(object):
     """
     a simple container for an LD Object.
@@ -54,7 +81,7 @@ class LDObjectContainer(object):
         return self._ld_object
 
     def toJSONDict(self):
-        return self.ld_object.toDict()
+        return self.ld_object.toJSONDict()
 
     def toJSON(self):
         return self.ld_object.serialize()
@@ -89,22 +116,6 @@ class LDObject(object):
         self.wrapped_obj = wrapped_obj
         self.structured_fields = {}
 
-    @classmethod
-    def get_class(cls, datatype):
-        # parse datatype string "v31/Election" --> from v31 import Election
-        parsed_datatype = datatype.split("/")
-
-        # get the module
-        dynamic_module = __import__(".".join(parsed_datatype[:-1]), globals(), locals(), [], level=-1)
-
-        # go down the attributes to get to the class
-        dynamic_ptr = dynamic_module
-        for attr in parsed_datatype[1:]:
-            dynamic_ptr = getattr(dynamic_ptr, attr)
-        dynamic_cls = dynamic_ptr
-        
-        return dynamic_cls
-        
     @classmethod
     def instantiate(cls, obj, datatype=None):
         if hasattr(obj, 'datatype') and not datatype:
@@ -117,12 +128,8 @@ class LDObject(object):
         if not obj:
             return None
 
-        # array
-        if isArray(datatype):
-            return [cls.instantiate(el, datatype = datatype.element_type) for el in obj]
-
         # the class
-        dynamic_cls = cls.get_class(datatype)
+        dynamic_cls = get_class(datatype)
 
         # instantiate it
         return_obj = dynamic_cls(obj)
@@ -154,6 +161,8 @@ class LDObject(object):
                 val[f] = self.process_value_out(f, getattr(self.wrapped_obj, f))
         return val
 
+    toJSONDict = toDict
+
     def loadDataFromDict(self, d):
         """
         load data from a dictionary
@@ -167,11 +176,18 @@ class LDObject(object):
         for f in self.FIELDS:
             if f in structured_fields:
                 # a structured ld field, recur
-                sub_ld_object = self.fromDict(d[f], type_hint = self.STRUCTURED_FIELDS[f])
+                try:
+                    sub_ld_object = self.fromDict(d[f], type_hint = self.STRUCTURED_FIELDS[f])
+                except TypeError:
+                    import pdb; pdb.set_trace()
+
                 self.structured_fields[f] = sub_ld_object
-                
+
                 # set the field on the wrapped object too
-                setattr(self.wrapped_obj, f, sub_ld_object.wrapped_obj)
+                try:
+                    setattr(self.wrapped_obj, f, sub_ld_object.wrapped_obj)
+                except AttributeError:
+                    import pdb; pdb.set_trace()
             else:
                 # a simple type
                 new_val = self.process_value_in(f, d[f])
@@ -184,9 +200,13 @@ class LDObject(object):
         ld_type = type_hint
 
         # get the LD class so we know what wrapped object to instantiate
-        ld_cls = cls.get_class(ld_type)
+        ld_cls = get_class(ld_type)
 
         wrapped_obj_cls = ld_cls.WRAPPED_OBJ_CLASS
+        
+        if not wrapped_obj_cls:
+            raise Exception("cannot instantiate wrapped object for %s" % ld_type)
+
         wrapped_obj = wrapped_obj_cls()
 
         # then instantiate the LD object and load the data
@@ -241,25 +261,33 @@ class LDObject(object):
         return other != None and self.uuid == other.uuid
   
 
-class ArrayOfObjects(LDObject):
+class BaseArrayOfObjects(LDObject):
     """
     If one type has, as a subtype, an array of things, then this is the structured field used
     """
+    ELEMENT_TYPE = None
+    WRAPPED_OBJ_CLASS = list
 
-    def __init__(self, wrapped_array, item_type):
-        self.item_type = item_type
-        self.items = [LDObject.instantiate(wrapped_item, item_type) for wrapped_item in wrapped_array]
+    def __init__(self, wrapped_obj):
+        self.items = []
+        super(BaseArrayOfObjects, self).__init__(wrapped_obj)
     
     def toDict(self):
-        return [item.serialize() for item in self.items]
+        return [item.toDict() for item in self.items]
 
-class arrayOf(object):
+    def loadDataFromDict(self, d):
+        "assumes that d is a list"
+        # TODO: should we be using ELEMENT_TYPE_CLASS here instead of LDObject?
+        self.items = [LDObject.fromDict(element, type_hint = self.ELEMENT_TYPE) for element in d]
+        
+
+def arrayOf(element_type):
     """
     a wrapper for the construtor of the array
     returns the constructor
     """
-    def __init__(self, element_type):
-        self.element_type = element_type
+    class ArrayOfTypedObjects(BaseArrayOfObjects):
+        ELEMENT_TYPE = element_type
+
+    return ArrayOfTypedObjects
 
-def isArray(field_type):
-    return type(field_type) == arrayOf
diff --git a/helios/datatypes/legacy.py b/helios/datatypes/legacy.py
index 3f6902ab983bd8fcd376ca69498a4603752bcfd0..e646eb41e32daefa256b2b86f1b0d000d9b2143c 100644
--- a/helios/datatypes/legacy.py
+++ b/helios/datatypes/legacy.py
@@ -3,13 +3,17 @@ Legacy datatypes for Helios (v3.0)
 """
 
 from helios.datatypes import LDObject, arrayOf
+from helios.crypto import elgamal as crypto_elgamal
+from helios.workflows import homomorphic
 
 ##
 ## utilities
 
 class DictObject(object):
-    def __init__(self, d):
+    def __init__(self, d=None):
         self.d = d
+        if not self.d:
+            self.d = {}
         
     def __getattr__(self, k):
         return self.d[k]
@@ -31,8 +35,9 @@ class Election(LegacyObject):
         'voting_ends_at': 'core/Timestamp',
         'frozen_at': 'core/Timestamp'
         }
-        
+
 class EncryptedAnswer(LegacyObject):
+    WRAPPED_OBJ_CLASS = homomorphic.EncryptedAnswer
     FIELDS = ['choices', 'individual_proofs', 'overall_proof']
     STRUCTURED_FIELDS = {
         'choices': arrayOf('legacy/EGCiphertext'),
@@ -53,6 +58,7 @@ class EncryptedVote(LegacyObject):
     """
     An encrypted ballot
     """
+    WRAPPED_OBJ_CLASS = homomorphic.EncryptedVote
     FIELDS = ['answers', 'election_hash', 'election_uuid']
     STRUCTURED_FIELDS = {
         'answers' : arrayOf('legacy/EncryptedAnswer')
@@ -93,6 +99,7 @@ class Trustee(LegacyObject):
         'decryption_proofs' : arrayOf(arrayOf('legacy/DLogProof'))}
 
 class EGPublicKey(LegacyObject):
+    WRAPPED_OBJ_CLASS = crypto_elgamal.PublicKey
     FIELDS = ['y', 'p', 'g', 'q']
     STRUCTURED_FIELDS = {
         'y': 'core/BigInteger',
@@ -100,7 +107,15 @@ class EGPublicKey(LegacyObject):
         'q': 'core/BigInteger',
         'g': 'core/BigInteger'}
 
+class EGSecretKey(LegacyObject):
+    WRAPPED_OBJ_CLASS = crypto_elgamal.SecretKey
+    FIELDS = ['x','pk']
+    STRUCTURED_FIELDS = {
+        'x': 'core/BigInteger',
+        'pk': 'legacy/EGPublicKey'}
+
 class EGCiphertext(LegacyObject):
+    WRAPPED_OBJ_CLASS = crypto_elgamal.Ciphertext
     FIELDS = ['alpha','beta']
     STRUCTURED_FIELDS = {
         'alpha': 'core/BigInteger',
@@ -116,6 +131,7 @@ class EGZKProofCommitment(LegacyObject):
         super(EGZKProofCommitment, self).__init__(DictObject(wrapped_obj))
     
 class EGZKProof(LegacyObject):
+    WRAPPED_OBJ_CLASS = crypto_elgamal.ZKProof
     FIELDS = ['commitment', 'challenge', 'response']
     STRUCTURED_FIELDS = {
         'commitment': 'legacy/EGZKProofCommitment',
@@ -123,10 +139,15 @@ class EGZKProof(LegacyObject):
         'response' : 'core/BigInteger'}
         
 class EGZKDisjunctiveProof(LegacyObject):
+    WRAPPED_OBJ_CLASS = crypto_elgamal.ZKDisjunctiveProof
     FIELDS = ['proofs']
     STRUCTURED_FIELDS = {
         'proofs': arrayOf('legacy/EGZKProof')}
 
+    def loadDataFromDict(self, d):
+        "hijack and make sure we add the proofs name back on"
+        return super(EGZKDisjunctiveProof, self).loadDataFromDict({'proofs': d})
+
     def toDict(self):
         "hijack toDict and make it return the proofs array only, since that's the spec for legacy"
         return super(EGZKDisjunctiveProof, self).toDict()['proofs']
diff --git a/helios/datatypes/pkc/elgamal.py b/helios/datatypes/pkc/elgamal.py
index 21f7cc922f5ce62388b4c4781eb4a14f251f8233..0f0688dc45af8213c6b351873bde97216132422d 100644
--- a/helios/datatypes/pkc/elgamal.py
+++ b/helios/datatypes/pkc/elgamal.py
@@ -3,7 +3,7 @@ data types for 2011/01 Helios
 """
 
 from helios.datatypes import LDObject
-from helios.crypto import elgamal
+from helios.crypto import elgamal as crypto_elgamal
 
 class DiscreteLogProof(LDObject):
     FIELDS = ['challenge', 'commitment', 'response']
@@ -13,7 +13,7 @@ class DiscreteLogProof(LDObject):
         'response' : 'core/BigInteger'}
     
 class PublicKey(LDObject):
-    WRAPPED_OBJ_CLASS = elgamal.PublicKey
+    WRAPPED_OBJ_CLASS = crypto_elgamal.PublicKey
 
     FIELDS = ['y', 'p', 'g', 'q']
     STRUCTURED_FIELDS = {
diff --git a/helios/models.py b/helios/models.py
index 3ea75ed9f0cb1062bec0b410256e4ac461a70c26..516c7af466d34338cfb2acb1d8559fa413fd223e 100644
--- a/helios/models.py
+++ b/helios/models.py
@@ -906,7 +906,7 @@ class Trustee(HeliosModel):
                                  null=True)
 
   decryption_proofs = JSONField(datatypes.LDObject,
-                                deserialization_params = {'type_hint' : datatypes.arrayOf(datatypes.arrayOf('legacy/DLogProof'))},
+                                deserialization_params = {'type_hint' : datatypes.arrayOf(datatypes.arrayOf('legacy/EGZKProof'))},
                                 null=True)
   
   def save(self, *args, **kwargs):
diff --git a/helios/workflows/__init__.py b/helios/workflows/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/helios/workflows/homomorphic.py b/helios/workflows/homomorphic.py
new file mode 100644
index 0000000000000000000000000000000000000000..39d9bc6a7bee75c9be379a4416cc772d32d73a84
--- /dev/null
+++ b/helios/workflows/homomorphic.py
@@ -0,0 +1,613 @@
+"""
+homomorphic workflow and algorithms for Helios
+
+Ben Adida
+2008-08-30
+reworked 2011-01-09
+"""
+
+from helios.crypto import algs, utils
+import logging
+import uuid
+import datetime
+
+class EncryptedAnswer(object):
+  """
+  An encrypted answer to a single election question
+  """
+
+  def __init__(self, choices=None, individual_proofs=None, overall_proof=None, randomness=None, answer=None):
+    self.choices = choices
+    self.individual_proofs = individual_proofs
+    self.overall_proof = overall_proof
+    self.randomness = randomness
+    self.answer = answer
+    
+  @classmethod
+  def generate_plaintexts(cls, pk, min=0, max=1):
+    plaintexts = []
+    running_product = 1
+    
+    # run the product up to the min
+    for i in range(max+1):
+      # if we're in the range, add it to the array
+      if i >= min:
+        plaintexts.append(algs.EGPlaintext(running_product, pk))
+        
+      # next value in running product
+      running_product = (running_product * pk.g) % pk.p
+      
+    return plaintexts
+
+  def verify_plaintexts_and_randomness(self, pk):
+    """
+    this applies only if the explicit answers and randomness factors are given
+    we do not verify the proofs here, that is the verify() method
+    """
+    if not hasattr(self, 'answer'):
+      return False
+    
+    for choice_num in range(len(self.choices)):
+      choice = self.choices[choice_num]
+      choice.pk = pk
+      
+      # redo the encryption
+      # WORK HERE (paste from below encryption)
+    
+    return False
+    
+  def verify(self, pk, min=0, max=1):
+    possible_plaintexts = self.generate_plaintexts(pk)
+    homomorphic_sum = 0
+      
+    for choice_num in range(len(self.choices)):
+      choice = self.choices[choice_num]
+      choice.pk = pk
+      individual_proof = self.individual_proofs[choice_num]
+      
+      # verify the proof on the encryption of that choice
+      if not choice.verify_disjunctive_encryption_proof(possible_plaintexts, individual_proof, algs.EG_disjunctive_challenge_generator):
+        return False
+
+      # compute homomorphic sum if needed
+      if max != None:
+        homomorphic_sum = choice * homomorphic_sum
+    
+    if max != None:
+      # determine possible plaintexts for the sum
+      sum_possible_plaintexts = self.generate_plaintexts(pk, min=min, max=max)
+
+      # verify the sum
+      return homomorphic_sum.verify_disjunctive_encryption_proof(sum_possible_plaintexts, self.overall_proof, algs.EG_disjunctive_challenge_generator)
+    else:
+      # approval voting, no need for overall proof verification
+      return True
+        
+  @classmethod
+  def fromElectionAndAnswer(cls, election, question_num, answer_indexes):
+    """
+    Given an election, a question number, and a list of answers to that question
+    in the form of an array of 0-based indexes into the answer array,
+    produce an EncryptedAnswer that works.
+    """
+    question = election.questions[question_num]
+    answers = question['answers']
+    pk = election.public_key
+    
+    # initialize choices, individual proofs, randomness and overall proof
+    choices = [None for a in range(len(answers))]
+    individual_proofs = [None for a in range(len(answers))]
+    overall_proof = None
+    randomness = [None for a in range(len(answers))]
+    
+    # possible plaintexts [0, 1]
+    plaintexts = cls.generate_plaintexts(pk)
+    
+    # keep track of number of options selected.
+    num_selected_answers = 0;
+    
+    # homomorphic sum of all
+    homomorphic_sum = 0
+    randomness_sum = 0
+
+    # min and max for number of answers, useful later
+    min_answers = 0
+    if question.has_key('min'):
+      min_answers = question['min']
+    max_answers = question['max']
+
+    # go through each possible answer and encrypt either a g^0 or a g^1.
+    for answer_num in range(len(answers)):
+      plaintext_index = 0
+      
+      # assuming a list of answers
+      if answer_num in answer_indexes:
+        plaintext_index = 1
+        num_selected_answers += 1
+
+      # randomness and encryption
+      randomness[answer_num] = algs.Utils.random_mpz_lt(pk.q)
+      choices[answer_num] = pk.encrypt_with_r(plaintexts[plaintext_index], randomness[answer_num])
+      
+      # generate proof
+      individual_proofs[answer_num] = choices[answer_num].generate_disjunctive_encryption_proof(plaintexts, plaintext_index, 
+                                                randomness[answer_num], algs.EG_disjunctive_challenge_generator)
+                                                
+      # sum things up homomorphically if needed
+      if max_answers != None:
+        homomorphic_sum = choices[answer_num] * homomorphic_sum
+        randomness_sum = (randomness_sum + randomness[answer_num]) % pk.q
+
+    # prove that the sum is 0 or 1 (can be "blank vote" for this answer)
+    # num_selected_answers is 0 or 1, which is the index into the plaintext that is actually encoded
+    
+    if num_selected_answers < min_answers:
+      raise Exception("Need to select at least %s answer(s)" % min_answers)
+    
+    if max_answers != None:
+      sum_plaintexts = cls.generate_plaintexts(pk, min=min_answers, max=max_answers)
+    
+      # need to subtract the min from the offset
+      overall_proof = homomorphic_sum.generate_disjunctive_encryption_proof(sum_plaintexts, num_selected_answers - min_answers, randomness_sum, algs.EG_disjunctive_challenge_generator);
+    else:
+      # approval voting
+      overall_proof = None
+    
+    return cls(choices, individual_proofs, overall_proof, randomness, answer_indexes)
+    
+# WORK HERE
+
+class EncryptedVote(object):
+  """
+  An encrypted ballot
+  """
+  
+  def verify(self, election):
+    # right number of answers
+    if len(self.encrypted_answers) != len(election.questions):
+      return False
+    
+    # check hash
+    if self.election_hash != election.hash:
+      # print "%s / %s " % (self.election_hash, election.hash)
+      return False
+      
+    # check ID
+    if self.election_uuid != election.uuid:
+      return False
+      
+    # check proofs on all of answers
+    for question_num in range(len(election.questions)):
+      ea = self.encrypted_answers[question_num]
+
+      question = election.questions[question_num]
+      min_answers = 0
+      if question.has_key('min'):
+        min_answers = question['min']
+        
+      if not ea.verify(election.public_key, min=min_answers, max=question['max']):
+        return False
+        
+    return True
+    
+  @classmethod
+  def fromElectionAndAnswers(cls, election, answers):
+    pk = election.public_key
+
+    # each answer is an index into the answer array
+    encrypted_answers = [EncryptedAnswer.fromElectionAndAnswer(election, answer_num, answers[answer_num]) for answer_num in range(len(answers))]
+    return cls(encrypted_answers=encrypted_answers, election_hash=election.hash, election_uuid = election.uuid)
+    
+
+def one_question_winner(question, result, num_cast_votes):
+  """
+  determining the winner for one question
+  """
+  # sort the answers , keep track of the index
+  counts = sorted(enumerate(result), key=lambda(x): x[1])
+  counts.reverse()
+
+  # if there's a max > 1, we assume that the top MAX win
+  if question['max'] > 1:
+    return [c[0] for c in counts[:question['max']]]
+
+  # if max = 1, then depends on absolute or relative
+  if question['result_type'] == 'absolute':
+    if counts[0][1] >=  (num_cast_votes/2 + 1):
+      return [counts[0][0]]
+    else:
+      return []
+
+  if question['result_type'] == 'relative':
+    return [counts[0][0]]    
+
+class Election(object):
+  
+  FIELDS = ['uuid', 'questions', 'name', 'short_name', 'description', 'voters_hash', 'openreg',
+      'frozen_at', 'public_key', 'private_key', 'cast_url', 'result', 'result_proof', 'use_voter_aliases', 'voting_starts_at', 'voting_ends_at', 'election_type']
+
+  JSON_FIELDS = ['uuid', 'questions', 'name', 'short_name', 'description', 'voters_hash', 'openreg',
+      'frozen_at', 'public_key', 'cast_url', 'use_voter_aliases', 'voting_starts_at', 'voting_ends_at']
+
+  # need to add in v3.1: use_advanced_audit_features, election_type, and probably more
+
+  def init_tally(self):
+    return Tally(election=self)
+        
+  def _process_value_in(self, field_name, field_value):
+    if field_name == 'frozen_at' or field_name == 'voting_starts_at' or field_name == 'voting_ends_at':
+      if type(field_value) == str or type(field_value) == unicode:
+        return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S')
+      
+    if field_name == 'public_key':
+      return algs.EGPublicKey.fromJSONDict(field_value)
+      
+    if field_name == 'private_key':
+      return algs.EGSecretKey.fromJSONDict(field_value)
+    
+  def _process_value_out(self, field_name, field_value):
+    # the date
+    if field_name == 'frozen_at' or field_name == 'voting_starts_at' or field_name == 'voting_ends_at':
+      return str(field_value)
+
+    if field_name == 'public_key' or field_name == 'private_key':
+      return field_value.toJSONDict()
+    
+  @property
+  def registration_status_pretty(self):
+    if self.openreg:
+      return "Open"
+    else:
+      return "Closed"
+    
+  @property
+  def winners(self):
+    """
+    Depending on the type of each question, determine the winners
+    returns an array of winners for each question, aka an array of arrays.
+    assumes that if there is a max to the question, that's how many winners there are.
+    """
+    return [one_question_winner(self.questions[i], self.result[i], self.num_cast_votes) for i in range(len(self.questions))]
+    
+  @property
+  def pretty_result(self):
+    if not self.result:
+      return None
+    
+    # get the winners
+    winners = self.winners
+
+    raw_result = self.result
+    prettified_result = []
+
+    # loop through questions
+    for i in range(len(self.questions)):
+      q = self.questions[i]
+      pretty_question = []
+      
+      # go through answers
+      for j in range(len(q['answers'])):
+        a = q['answers'][j]
+        count = raw_result[i][j]
+        pretty_question.append({'answer': a, 'count': count, 'winner': (j in winners[i])})
+        
+      prettified_result.append({'question': q['short_name'], 'answers': pretty_question})
+
+    return prettified_result
+
+    
+class Voter(object):
+  """
+  A voter in an election
+  """
+  FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id', 'name', 'alias']
+  JSON_FIELDS = ['election_uuid', 'uuid', 'voter_type', 'voter_id_hash', 'name']
+  
+  # alternative, for when the voter is aliased
+  ALIASED_VOTER_JSON_FIELDS = ['election_uuid', 'uuid', 'alias']
+  
+  def toJSONDict(self):
+    fields = None
+    if self.alias != None:
+      return super(Voter, self).toJSONDict(self.ALIASED_VOTER_JSON_FIELDS)
+    else:
+      return super(Voter,self).toJSONDict()
+
+  @property
+  def voter_id_hash(self):
+    if self.voter_login_id:
+      # for backwards compatibility with v3.0, and since it doesn't matter
+      # too much if we hash the email or the unique login ID here.
+      return utils.hash_b64(self.voter_login_id)
+    else:
+      return utils.hash_b64(self.voter_id)
+
+class Trustee(object):
+  """
+  a trustee
+  """
+  FIELDS = ['uuid', 'public_key', 'public_key_hash', 'pok', 'decryption_factors', 'decryption_proofs', 'email']
+
+  def _process_value_in(self, field_name, field_value):
+    if field_name == 'public_key':
+      return algs.EGPublicKey.fromJSONDict(field_value)
+      
+    if field_name == 'pok':
+      return algs.DLogProof.fromJSONDict(field_value)
+    
+  def _process_value_out(self, field_name, field_value):
+    if field_name == 'public_key' or field_name == 'pok':
+      return field_value.toJSONDict()
+          
+class CastVote(object):
+  """
+  A cast vote, which includes an encrypted vote and some cast metadata
+  """
+  FIELDS = ['vote', 'cast_at', 'voter_uuid', 'voter_hash', 'vote_hash']
+  
+  def __init__(self, *args, **kwargs):
+    super(CastVote, self).__init__(*args, **kwargs)
+    self.election = None
+  
+  @classmethod
+  def fromJSONDict(cls, d, election=None):
+    o = cls()
+    o.election = election
+    o.set_from_args(**d)
+    return o
+    
+  def toJSONDict(self, include_vote=True):
+    result = super(CastVote,self).toJSONDict()
+    if not include_vote:
+      del result['vote']
+    return result
+
+  @classmethod
+  def fromOtherObject(cls, o, election):
+    obj = cls()
+    obj.election = election
+    obj.set_from_other_object(o)
+    return obj
+  
+  def _process_value_in(self, field_name, field_value):
+    if field_name == 'cast_at':
+      if type(field_value) == str:
+        return datetime.datetime.strptime(field_value, '%Y-%m-%d %H:%M:%S')
+      
+    if field_name == 'vote':
+      return EncryptedVote.fromJSONDict(field_value, self.election.public_key)
+      
+  def _process_value_out(self, field_name, field_value):
+    # the date
+    if field_name == 'cast_at':
+      return str(field_value)
+
+    if field_name == 'vote':
+      return field_value.toJSONDict()
+      
+  def issues(self, election):
+    """
+    Look for consistency problems
+    """
+    issues = []
+    
+    # check the election
+    if self.vote.election_uuid != election.uuid:
+      issues.append("the vote's election UUID does not match the election for which this vote is being cast")
+    
+    return issues
+
+class DLogTable(object):
+  """
+  Keeping track of discrete logs
+  """
+  
+  def __init__(self, base, modulus):
+    self.dlogs = {}
+    self.dlogs[1] = 0
+    self.last_dlog_result = 1
+    self.counter = 0
+    
+    self.base = base
+    self.modulus = modulus
+    
+  def increment(self):
+    self.counter += 1
+    
+    # new value
+    new_value = (self.last_dlog_result * self.base) % self.modulus
+    
+    # record the discrete log
+    self.dlogs[new_value] = self.counter
+    
+    # record the last value
+    self.last_dlog_result = new_value
+    
+  def precompute(self, up_to):
+    while self.counter < up_to:
+      self.increment()
+  
+  def lookup(self, value):
+    return self.dlogs.get(value, None)
+      
+    
+class Tally(object):
+  """
+  A running homomorphic tally
+  """
+  
+  FIELDS = ['num_tallied', 'tally']
+  JSON_FIELDS = ['num_tallied', 'tally']
+  
+  def __init__(self, *args, **kwargs):
+    super(Tally, self).__init__(*args, **kwargs)
+
+    self.election = kwargs.get('election',None)
+
+    if self.election:
+      self.init_election(self.election)
+    else:
+      self.questions = None
+      self.public_key = None
+      
+      if not self.tally:
+        self.tally = None
+      
+    # initialize
+    if self.num_tallied == None:
+      self.num_tallied = 0    
+
+  def init_election(self, election):
+    """
+    given the election, initialize some params
+    """
+    self.questions = election.questions
+    self.public_key = election.public_key
+    
+    if not self.tally:
+      self.tally = [[0 for a in q['answers']] for q in self.questions]
+    
+  def add_vote_batch(self, encrypted_votes, verify_p=True):
+    """
+    Add a batch of votes. Eventually, this will be optimized to do an aggregate proof verification
+    rather than a whole proof verif for each vote.
+    """
+    for vote in encrypted_votes:
+      self.add_vote(vote, verify_p)
+    
+  def add_vote(self, encrypted_vote, verify_p=True):
+    # do we verify?
+    if verify_p:
+      if not encrypted_vote.verify(self.election):
+        raise Exception('Bad Vote')
+
+    # for each question
+    for question_num in range(len(self.questions)):
+      question = self.questions[question_num]
+      answers = question['answers']
+      
+      # for each possible answer to each question
+      for answer_num in range(len(answers)):
+        # do the homomorphic addition into the tally
+        enc_vote_choice = encrypted_vote.encrypted_answers[question_num].choices[answer_num]
+        enc_vote_choice.pk = self.public_key
+        self.tally[question_num][answer_num] = encrypted_vote.encrypted_answers[question_num].choices[answer_num] * self.tally[question_num][answer_num]
+
+    self.num_tallied += 1
+
+  def decryption_factors_and_proofs(self, sk):
+    """
+    returns an array of decryption factors and a corresponding array of decryption proofs.
+    makes the decryption factors into strings, for general Helios / JS compatibility.
+    """
+    # for all choices of all questions (double list comprehension)
+    decryption_factors = []
+    decryption_proof = []
+    
+    for question_num, question in enumerate(self.questions):
+      answers = question['answers']
+      question_factors = []
+      question_proof = []
+
+      for answer_num, answer in enumerate(answers):
+        # do decryption and proof of it
+        dec_factor, proof = sk.decryption_factor_and_proof(self.tally[question_num][answer_num])
+
+        # look up appropriate discrete log
+        # this is the string conversion
+        question_factors.append(str(dec_factor))
+        question_proof.append(proof.toJSONDict())
+        
+      decryption_factors.append(question_factors)
+      decryption_proof.append(question_proof)
+    
+    return decryption_factors, decryption_proof
+    
+  def decrypt_and_prove(self, sk, discrete_logs=None):
+    """
+    returns an array of tallies and a corresponding array of decryption proofs.
+    """
+    
+    # who's keeping track of discrete logs?
+    if not discrete_logs:
+      discrete_logs = self.discrete_logs
+      
+    # for all choices of all questions (double list comprehension)
+    decrypted_tally = []
+    decryption_proof = []
+    
+    for question_num in range(len(self.questions)):
+      question = self.questions[question_num]
+      answers = question['answers']
+      question_tally = []
+      question_proof = []
+
+      for answer_num in range(len(answers)):
+        # do decryption and proof of it
+        plaintext, proof = sk.prove_decryption(self.tally[question_num][answer_num])
+
+        # look up appropriate discrete log
+        question_tally.append(discrete_logs[plaintext])
+        question_proof.append(proof)
+        
+      decrypted_tally.append(question_tally)
+      decryption_proof.append(question_proof)
+    
+    return decrypted_tally, decryption_proof
+  
+  def verify_decryption_proofs(self, decryption_factors, decryption_proofs, public_key, challenge_generator):
+    """
+    decryption_factors is a list of lists of dec factors
+    decryption_proofs are the corresponding proofs
+    public_key is, of course, the public key of the trustee
+    """
+    
+    # go through each one
+    for q_num, q in enumerate(self.tally):
+      for a_num, answer_tally in enumerate(q):
+        # parse the proof
+        proof = algs.EGZKProof.fromJSONDict(decryption_proofs[q_num][a_num])
+        
+        # check that g, alpha, y, dec_factor is a DH tuple
+        if not proof.verify(public_key.g, answer_tally.alpha, public_key.y, int(decryption_factors[q_num][a_num]), public_key.p, public_key.q, challenge_generator):
+          return False
+    
+    return True
+    
+  def decrypt_from_factors(self, decryption_factors, public_key):
+    """
+    decrypt a tally given decryption factors
+    
+    The decryption factors are a list of decryption factor sets, for each trustee.
+    Each decryption factor set is a list of lists of decryption factors (questions/answers).
+    """
+    
+    # pre-compute a dlog table
+    dlog_table = DLogTable(base = public_key.g, modulus = public_key.p)
+    dlog_table.precompute(self.num_tallied)
+    
+    result = []
+    
+    # go through each one
+    for q_num, q in enumerate(self.tally):
+      q_result = []
+
+      for a_num, a in enumerate(q):
+        # coalesce the decryption factors into one list
+        dec_factor_list = [df[q_num][a_num] for df in decryption_factors]
+        raw_value = self.tally[q_num][a_num].decrypt(dec_factor_list, public_key)
+        
+        q_result.append(dlog_table.lookup(raw_value))
+
+      result.append(q_result)
+    
+    return result
+
+  def _process_value_in(self, field_name, field_value):
+    if field_name == 'tally':
+      return [[algs.EGCiphertext.fromJSONDict(a) for a in q] for q in field_value]
+      
+  def _process_value_out(self, field_name, field_value):
+    if field_name == 'tally':
+      return [[a.toJSONDict() for a in q] for q in field_value]    
+