root / trunk / pyopencoin / oc / entities.py

Revision 245, 76.3 kB (checked in by ocmathew, 4 years ago)

Fix autoresetting protocols (Well, if after a PROTOCOL_ERROR it is okay to continue on a new track)

  • Property svn:mime-type set to text/plain
  • Property svn:eol-style set to native
  • Property svn:executable set to *
Line 
1import protocols
2from messages import Message
3
4class Entity(object):
5
6    def toPython(self):
7        return self.__dict__
8
9    def fromPython(self,data):
10        self.__dict__ = data     
11       
12    def toJson(self):
13        return json.write(self.toPython())
14
15    def fromJson(self,text):
16        return self.fromPython(json.read(text))
17
18
19    def serialize(self):
20        import pickle,base64
21        return base64.b64encode(pickle.dumps(self))       
22
23##################### time ################################
24def getTime():
25    import time
26    return time.time()
27
28#################### Wallet ###############################
29
30class Wallet(Entity):
31    "A Wallet"
32
33    def __init__(self):
34        self.coins = [] # The coins in the wallet
35        self.waitingTransfers = {} # The transfers we have done a TRANSFER_TOKEN_REQUEST on
36                                   # key is transaction_id, val is the set of blanks
37        self.otherCoins = [] # Coins we received from another wallet, waiting to Redeem
38        self.getTime = getTime # The getTime function
39        self.keyids = {} # MintKeys by key_identifier
40        self.cdds = {} # CDDs by [CurrencyIdentifier][version]. version of None gives current version key
41        self.issuer_transports = {} # The issuer_transports open by location
42
43
44    def addCDD(self, CDD):
45        """addCDD adds a CDD to the wallet's CDDs. The CDDs are not trusted, just stored."""
46        version = dict(CDD.options)['version']
47        currencydict = self.cdds.setdefault(CDD.currency_identifier, {})
48        if version in currencydict:
49            import warnings
50            warnings.warn('Tried to add version "%s" which already existed' % version)
51        currencydict[version] = CDD
52
53    def setCurrentCDD(self, CDD):
54        """setCurrentCDD sets the default CDD for a currency to CDD. It adds if necessary."""
55        version = dict(CDD.options)['version']
56        if version not in self.cdds.setdefault(CDD.currency_identifier, {}):
57            self.addCDD(CDD)
58        self.cdds[CDD.currency_identifier][None] = version
59
60    def getCDD(self, currency_identifier, version=None):
61        """Returns a specific version of a CDD.
62       
63        If version is None, returns the current version.
64        """
65        if not version:
66            version = self.cdds[currency_identifier][None]
67        return self.cdds[currency_identifier][version]
68
69    def fetchMintKey(self, transport, denominations=None, keyids=None, time=None):
70        protocol = protocols.fetchMintKeyProtocol(denominations=denominations, keyids=keyids, time=time)
71        transport.setProtocol(protocol)
72        transport.start()
73        protocol.newMessage(Message(None))
74
75        # Get the keys direct from the protcool.
76        retreivedKeys = protocol.keycerts
77
78        for key in retreivedKeys:
79            try:
80                cdd = self.getCDD(key.currency_identifier)
81            except KeyError:
82                continue # try the other currencies
83
84            if key.key_identifier not in self.keyids:
85                if key.verify_with_CDD(cdd):
86                    self.keyids[key.key_identifier] = key
87                else:
88                    raise Exception('CDD: %s\n\nMintKey: %s' % (cdd, key))
89                    raise Exception('Got a bad key')
90
91    def sendMoney(self,transport):
92        """FOR TESTING PURPOSES ONLY. Sends some money to the given transport."""
93
94        protocol = protocols.WalletSenderProtocol(self)
95        transport.setProtocol(protocol)
96        transport.start()
97        #Trigger execution of the protocol
98        protocol.newMessage(Message(None))
99
100    def receiveMoney(self,transport):
101        """FOR TESTING PURPOSES ONLY. sets up the wallet to receive tokens from another wallet."""
102        protocol = protocols.WalletRecipientProtocol(self)
103        transport.setProtocol(protocol)
104        transport.start()
105
106    def sendCoins(self, transport, target, amount):
107        """sendCoins sends coins over a transport to a target of a certain amount.
108       
109        We need to be careful to try every possible working combination of coins to
110        to an amount. To test, we muck in the internals of the coin sending to make
111        sure that we get the right coins. (Note: This would be easier if we just
112        had a function to get the coins for an amount.)
113
114        To test the functionality we steal the transport, and when a message occurs,
115        we steal the tokens directly out of the protocol. This is highly dependant
116        on the internal details of TokenSpendSender and the transport/protocol
117        relationship.
118
119        >>> class transport:
120        ...     def setProtocol(self, protocol):
121        ...         protocol.transport = self
122        ...         self.protocol = protocol
123        ...     def start(self): pass
124        ...     def write(self, info): print sum(self.protocol.coins) # Steal the values
125       
126        >>> wallet = Wallet()
127        >>> test = lambda x: wallet.sendCoins(transport(), '', x)
128
129        >>> from tests import coins
130
131        Okay. Do some simple checks to make sure things work at all
132       
133        >>> wallet.coins = [coins[0][0]]
134        >>> test(1)
135        1
136
137        >>> wallet.coins = [coins[0][0], coins[2][0]]
138        >>> test(6)
139        6
140
141        >>> wallet.coins = [coins[2][0], coins[0][0]]
142        >>> test(6)
143        6
144
145        Okay. Now we'll do some more advanced tests of the system. We start off with
146        a specifically selected group of coins:
147        3 coins of denomination 2
148        1 coin of denomination 5
149        1 coin of denomination 10
150        >>> test_coins = [coins[1][0], coins[1][1], coins[1][2], coins[2][0], coins[3][0]]
151       
152        >>> test_coins[0].denomination == test_coins[1].denomination == test_coins[2].denomination
153        True
154        >>> test_coins[0].denomination
155        '2'
156        >>> test_coins[3].denomination
157        '5'
158        >>> test_coins[4].denomination
159        '10'
160        >>> sum(test_coins)
161        21
162
163        Now, this group of coins has some specific properties. There are only certain ways to
164        get certain values of coins. We'll be testing 6, 11, 15, 16, 19, and 21
165
166        6 = 2 + 2 + 2
167        >>> wallet.coins = test_coins
168        >>> sum(wallet.coins)
169        21
170        >>> test(6)
171        6
172
173        11 = 5 + 2 + 2 + 2
174        >>> wallet.coins = test_coins
175        >>> test(11)
176        11
177
178        15 = 10 + 5
179        >>> wallet.coins = test_coins
180        >>> test(15)
181        15
182
183        16 = 10 + 2 + 2 + 2
184        >>> wallet.coins = test_coins
185        >>> test(16)
186        16
187
188        19 = 10 + 5 + 2 + 2
189        >>> wallet.coins = test_coins
190        >>> test(19)
191        19
192
193        21 = 10 + 5 + 2 + 2 + 2
194        >>> wallet.coins = [coins[1][0], coins[1][1], coins[1][2], coins[2][0], coins[3][0]]
195        >>> test(21)
196        21
197
198        Okay. Some things we can't do.
199
200        8 = Impossible
201        >>> wallet.coins = test_coins
202        >>> test(8)
203        Traceback (most recent call last):
204        UnableToDoError: Not enough tokens
205
206        22 = Impossible
207        >>> wallet.coins = test_coins
208        >>> test(22)
209        Traceback (most recent call last):
210        UnableToDoError: Not enough tokens
211
212        Okay. Now we want to make sure we don't lose coins if there is an exception
213        that occurs.
214
215        >>> test = lambda x: wallet.sendCoins('foo', '', x)
216        >>> wallet.coins = test_coins
217        >>> test(21)
218        Traceback (most recent call last):
219        AttributeError: 'str' object has no attribute ...
220
221        >>> wallet.coins == test_coins
222        True
223
224        """
225        if sum(self.coins) < amount:
226            raise UnableToDoError('Not enough tokens')
227
228        denominations = {} # A dictionary of coins by denomination
229        denomination_list = [] # A list of the denomination of every coin
230        for coin in self.coins:
231            denominations.setdefault(coin.denomination, [])
232            denominations[coin.denomination].append(coin)
233            denomination_list.append(coin.denomination)
234           
235        int_denomination_list = [int(d) for d in denomination_list]
236        int_denomination_list.sort(reverse=True) # sort from high to low
237
238        def my_split(piece_list, sum):
239            # piece_list must be sorted from high to low
240           
241            # Delete all coins greater than sum
242            my_list = [p for p in piece_list if p <= sum]
243
244            while my_list:
245                test_piece = my_list[0]
246                del my_list[0]
247
248                if test_piece == sum:
249                    return [test_piece]
250
251                test_partition = my_split(my_list, sum - test_piece)
252
253                if test_partition == [] :
254                    # Partitioning the rest failed, so remove all pieces of this size
255                    my_list = [p for p in my_list if p < test_piece]
256                else :
257                    test_partition.append(test_piece)
258                    return test_partition
259
260            # if we are here, we don't have a set of coins that works
261            return []
262
263        if sum(int_denomination_list) != sum(self.coins):
264            raise Exception('denomination_list and self.coins differ!')
265
266        denominations_to_use = my_split(int_denomination_list, amount)
267
268        if not denominations_to_use:
269            raise UnableToDoError('Not enough tokens')
270
271        denominations_to_use = [str(d) for d in denominations_to_use]
272
273        to_use = []
274        for denomination in denominations_to_use:
275            to_use.append(denominations[denomination].pop()) # Make sure we remove the coins from denominations!
276
277        for coin in to_use: # Remove the coins to prevent accidental double spending
278            self.coins.remove(coin)
279
280        try:
281            protocol = protocols.TokenSpendSender(to_use,target)
282            transport.setProtocol(protocol)
283            transport.start()
284            protocol.newMessage(Message(None))
285        except: # Catch everything. Losing coins is death. We re-raise anyways.
286            # If we had an error at the protocol or transport layer, make sure we don't lose the coins
287            self.coins.extend(to_use)
288            raise
289
290        # FIXME: protocol.done is not the correct thing to be using here. protocol.done
291        # specifies that we are ready to hangup the connection, when instead, we want to
292        # know that the specific protocol we are using is complete (protocols.py:58)
293        if not protocol.done:
294            # If we didn't succeed, re-add the coins to the wallet.
295            # Of course, we may need to remint, so FIXME: look at this
296            self.coins.extend(to_use)
297
298    def listen(self,transport):
299        """listens on a transport, answers a handshake, and performs wallet server type things
300        >>> import transports
301        >>> w = Wallet()
302        >>> stt = transports.SimpleTestTransport()
303        >>> w.listen(stt)
304        >>> stt.send('HANDSHAKE',[['protocol', 'opencoin 1.0']])
305        <Message('HANDSHAKE_ACCEPT',[['protocol', 'opencoin 1.0']])>
306        >>> stt.send('sendMoney',[1,2])
307        <Message('Receipt',None)>
308        """
309        protocol = protocols.answerHandshakeProtocol(arguments=self,
310                                                     sendMoney=protocols.WalletRecipientProtocol,
311                                                     SUM_ANNOUNCE=protocols.TokenSpendRecipient)
312        transport.setProtocol(protocol)
313        transport.start()
314
315
316    def confirmReceiveCoins(self,walletid,sum,target):
317        """confirmReceiveCoins verifies with the user the transaction can occur.
318
319        confirmReceiveCoins returns 'trust' if they accept the transaction with a certain
320        wallet for a sum regarding a target.
321
322        Except that this argument then gets used as the action in 'redeem', 'exhange', 'trust'
323        telling us what to do with the tokens after we get them.
324        """
325        return 'redeem'
326
327
328    def transferTokens(self, transport, target, blanks, coins, type):
329        """transferTokens performs a TOKEN_TRANSFER with a issuer.
330
331        if blanks are provided, transferTokens performs all checks to convert
332        the tokens to blinds and unblind the signatures when complete.
333        """
334        import base64
335        blinds = []
336        keydict = {}
337
338        if blanks:
339            cdd = self.getCDD(blanks[0].currency_identifier)
340
341        for blank in blanks:
342            li = keydict.setdefault(blank.encodeField('key_identifier'), [])
343            li.append(base64.b64encode(blank.blind_blank(cdd, self.keyids[blank.key_identifier])))
344
345        for key_id in keydict:
346            blinds.append([key_id, keydict[key_id]])
347
348        protocol = protocols.TransferTokenSender(target, blinds, coins, type=type)
349        transport.setProtocol(protocol)
350        transport.start()
351        protocol.newMessage(Message(None))
352
353        if type == 'mint':
354            if protocol.result == 1: # If set, we got a TRANSFER_TOKEN_ACCEPT
355                self.addTransferBlanks(protocol.transaction_id, blanks)
356                self.finishTransfer(protocol.transaction_id, protocol.blinds)
357            elif protocol.result == 2: #If set, we got a TRANSFER_TOKEN_DELAY
358                self.addTransferBlanks(protocol.transaction_id, blanks)
359       
360        elif type == 'exchange':
361            if protocol.result == 1: # If set, we got a TRANSFER_TOKEN_ACCEPT
362                self.addTransferBlanks(protocol.transaction_id, blanks)
363                self.finishTransfer(protocol.transaction_id, protocol.blinds)
364            elif protocol.result == 2: # If set, we got a TRANSFER_TOKEN_DELAY
365                self.addTransferBlanks(protocol.transaction_id, blanks)
366                self.removeCoins(coins)
367
368        elif type == 'redeem':
369            if protocol.result == 1: # If set, we got a TRANSFER_TOKEN_ACCEPT
370                self.removeCoins(coins)
371               
372        else:
373            raise NotImplementedError()
374               
375    def handleIncomingCoins(self,coins,action,reason):
376        """handleIncomingCoins is a bridge between receiving coins from a wallet and redeeming.
377
378        it seems to be anther part of receiveCoins. Basically, given some coins, it attempts to
379        redeem them with the IS.
380
381        returns True if successful. Nothing if not
382        FIXME: It doesn't seem to know if the transfer works?
383        """
384        # Q: What is reason for reason?
385        # A: reason is describing what the tokens are going to be for, e.g 'a book'
386
387        cdd = self.getCDD(coins[0].currency_identifier) # The default CDD for the currency
388
389        # Get the IS location
390        issuer_service_location = cdd.issuer_service_location
391
392        if action == 'redeem':
393            transport = self.getIssuerTransport(issuer_service_location)
394            self.otherCoins.extend(coins) # Deposit them in otherCoins
395            if transport:
396                self.transferTokens(transport,'my account',[],coins,'redeem')
397
398        elif action == 'exchange':
399            transport = self.getIssuerTransport(issuer_service_location)
400            # FIXME: should make some amount of blanks and sends the blinds in a TTR
401            raise NotImplementedError
402       
403        elif action == 'trust':
404            self.coins.append(coins) # Move the coins into our wallet, trusting that they are good
405           
406        else:
407            raise Exception('Unknown action')
408
409        return True
410
411
412    def getIssuerTransport(self, location):
413        if location in self.issuer_transports:
414            return self.issuer_transports[location]
415        else:
416            return self.makeIssuerTransport(location)
417
418    def makeIssuerTransport(self, location):
419        """creates a transport to the issuer at a location."""
420        # NOTE: if using in testing and want an issuerTransport that you can not connect to,
421        # overwrite with lambda location: return None if you want things to silently fail,
422        # and lambda location: raise FIXME exception to signify the connection failed
423        if not location.startswith('opencoin://'):
424            raise Exception('Improperly formatted transport')
425
426        # strip off opencoin://
427        fullstring = location[len('opencoin://'):]
428
429        try:
430            address, port = fullstring.split(':')
431        except ValueError:
432            raise Exception('Improperly formatted transport')
433
434        import transports
435
436        sct = transports.SocketClientTransport(address, int(port))
437
438        self.addIssuerTransport(location, sct)
439
440        return sct
441       
442    def addIssuerTransport(self, location, transport):
443        self.issuer_transports[location] = transport
444
445    def delIssuerTransport(self, location):
446        del self.issuer_transports[location]
447
448    def removeCoins(self, coins):
449        """removeCoins removes a set of coins from self.coins or self.otherCoins
450
451        This is used so we can cleanly remove coins after a redemption. Coins being
452        used for a redemption may come from the wallet itself or from another wallet.
453        """
454        # NOTE: This assumes that self.coins and self.otherCoins do not contain
455        # copies of the same coin
456        for c in coins:
457            try:
458                self.coins.remove(c)
459            except ValueError:
460                self.otherCoins.remove(c)
461
462    def addTransferBlanks(self, transaction_id, blanks):
463        self.waitingTransfers[transaction_id] = blanks
464
465    def finishTransfer(self, transaction_id, blinds):
466        """Finishes a transfer where we minted. Takes blinds and makes coins."""
467        raise NotImplementedError("We never go here!") #FIXME: Completely untested! Use it!
468        from containers import BlankError
469
470        #FIXME: What do we do if a coin is bad?
471        coins = []
472        blanks = self.waitingTransfers[transaction_id]
473
474        # We have no way of being sure we have the same amount of blanks and blinds afaict.
475        # We'll try to make as many coins as we can, then error
476        shortest = min(len(blanks), len(blinds))
477
478        cdd = self.getCDD(blanks[0].currency_identifier) # all blanks have the same CDD in a transaction
479
480        #FIXME: We need to make sure we atleast have the same number of blanks and blinds at the protocol level!
481        # Well, maybe not the protocol level. We know we received the full message because we can decode the json.
482        # At this point, either our memory is screwed, or the IS screwed up. No real way to fix it either.
483        for i in range(shortest):
484            blank = blanks[i]
485            blind = blinds[i]
486            try:
487                signature = blank.unblind_signature(blind)
488            except CryptoError:
489                # Skip this one, go to the next
490                continue
491               
492            mintKey = self.keyids[blank.key_identifier]
493
494            try:
495                coins.append(blank.newCoin(signature, cdd, mintKey))
496            except BlankError:
497                # Skip this one, go to the next
498                continue
499
500        for coin in coins:
501            if coin not in self.coins:
502                self.coins.append(coin)
503
504        del self.waitingTransfers[transaction_id]
505
506class UnableToDoError(Exception):
507    pass
508
509
510#################### Issuer ###############################
511
512class IssuerEntity(Entity):
513    """The issuer.
514    IssuerEntity contains all the subparts of the issuer,
515    an IS, a DSDB, and a mint
516   
517    >>> i = IssuerEntity()
518    >>> i.createMasterKey(keylength=256)
519    """
520   
521    def __init__(self):
522        self.dsdb = DSDB()
523        self.mint = Mint()
524        self.issuer = Issuer(dsdb=self.dsdb, mint=self.mint)
525
526        self.masterKey = None
527
528        self.getTime = getTime
529   
530    def createMasterKey(self,keylength=1024):
531        import crypto
532
533        self.key_alg = crypto.createRSAKeyPair
534
535        masterKey = self.key_alg(keylength, public=False)
536        self.masterKey = masterKey
537
538    def makeCDD(self, currency_identifier, short_currency_identifier, denominations,
539                issuer_service_location, options):
540        from containers import CDD, Signature
541        import crypto
542       
543        ics = crypto.CryptoContainer(signing=crypto.RSASigningAlgorithm,
544                                     blinding=crypto.RSABlindingAlgorithm,
545                                     hashing=crypto.SHA256HashingAlgorithm)
546
547        public_key = self.masterKey.newPublicKeyPair()
548       
549        cdd = CDD(standard_identifier='http://opencoin.org/OpenCoinProtocol/1.0',
550                  currency_identifier=currency_identifier,
551                  short_currency_identifier=short_currency_identifier,
552                  denominations=denominations,
553                  options=options,
554                  issuer_cipher_suite=ics,
555                  issuer_service_location=issuer_service_location,
556                  issuer_public_master_key = public_key)
557
558
559        signature = Signature(keyprint=ics.hashing(str(public_key)).digest(),
560                              signature=ics.signing(self.masterKey).sign(ics.hashing(cdd.content_part()).digest()))
561
562        cdd.signature = signature
563
564        if not cdd.verify_self():
565            raise Exception('Just created an invalid CDD')
566     
567        self.issuer.addCDD(cdd)
568
569    def createSignedMintKey(self, denomination, not_before, key_not_after, token_not_after,
570                            signing_key=None, bypass_cdd_checks=False, size=1024):
571        """Have the Mint create a new key and sign the public key."""
572        import containers
573
574        cdd = self.issuer.getCDD()
575
576        if denomination not in cdd.denominations and not bypass_cdd_checks:
577            raise Exception('Trying to create a bad denomination')
578       
579        if not signing_key:
580            signing_key = self.masterKey
581
582        hash_alg = cdd.issuer_cipher_suite.hashing
583        sign_alg = cdd.issuer_cipher_suite.signing
584        key_alg = self.key_alg
585
586        public = self.mint.createNewKey(hash_alg, key_alg, size)
587
588        keyid = public.key_id(hash_alg)
589       
590        mintKey = containers.MintKey(key_identifier=keyid,
591                                     currency_identifier=cdd.currency_identifier,
592                                     denomination=denomination,
593                                     not_before=not_before,
594                                     key_not_after=key_not_after,
595                                     token_not_after=token_not_after,
596                                     public_key=public)
597
598        signer = sign_alg(signing_key)
599        hashed_content = hash_alg(mintKey.content_part()).digest()
600        sig = containers.Signature(keyprint = signing_key.key_id(hash_alg),
601                                   signature = signer.sign(hashed_content))
602
603        mintKey.signature = sig
604
605        if not bypass_cdd_checks:
606            if not mintKey.verify_with_CDD(cdd):
607                raise Exception('Created a bad mintKey')
608       
609        self.issuer.addMintKey(mintKey)
610
611        return mintKey
612
613
614class Issuer(Entity):
615    """An IS
616
617    >>> ie = IssuerEntity()
618    >>> issuer = ie.issuer
619    """
620    def __init__(self, dsdb, mint):
621        self.dsdb = dsdb
622        self.mint = mint
623
624        self.cdds = {} # CDDs by version
625        self.current_cdd_version = None
626        self.transactions = {} # transactions by transaction_id
627       
628        self.getTime = getTime
629
630        # Signed minting keys
631        self.mintKeysByDenomination = {} # List of mint keys for a denomination
632        self.mintKeysByKeyID = {}
633
634        self.keyids = self.mintKeysByKeyID
635
636    def getKeyByDenomination(self, denomination, time):
637        #FIXME: Is this supposed to return the mint keys valid for minting at a certain time?
638        try:
639            keys = self.mintKeysByDenomination[denomination]
640        except KeyError:
641            raise KeyFetchError
642
643        not_before = [k for k in keys if time >= k.not_before]
644        key_not_after = [k for k in keys if time <= k.key_not_after]
645
646        # If we were python2.4+, we could use sets and take the intersection
647        response = []
648        for k in not_before:
649            if k in key_not_after:
650                response.append(k)
651
652        if not response: # No denominations found valid at that time
653            raise KeyFetchError
654
655        return response
656   
657    def getKeyById(self,keyid):
658        try:
659            return self.mintKeysByKeyID[keyid]
660        except KeyError:           
661            raise KeyFetchError
662
663    def addMintKey(self, mintKey):
664        denomination = mintKey.denomination
665        self.mintKeysByDenomination.setdefault(denomination, []).append(mintKey)
666        self.mintKeysByKeyID[mintKey.key_identifier] = mintKey
667
668    def addCDD(self, cdd):
669        """Adds a CDD to the issuer
670
671        >>> import tests
672        >>> issuer = Issuer(mint=None, dsdb=None)
673        """
674        version = dict(cdd.options)['version']
675
676        if version in self.cdds:
677            import warnings
678            warnings.warn('Trying to add the same version of CDD')
679
680        if not cdd.verify_self():
681            raise Exception('Tried to add an invalid CDD')
682
683        self.cdds[version] = cdd
684
685    def setCurrentCDDVersion(self, version):
686        if self.current_cdd_version == version:
687            import warnings
688            warnings.warn('Setting CDD version to the same version')
689
690        self.current_cdd_version = version
691
692    def getCDD(self, version=None):
693        """Returns a specific CDD. If version=None, returns the current CDD."""
694        if version == None:
695            version = self.current_cdd_version
696
697        return self.cdds[version]
698
699    def getCurrentCDDVersion(self):
700        """Returns the current CDD version."""
701        if not self.current_cdd_version:
702            raise Exception('No current CDD version.')
703
704        return self.current_cdd_version
705
706    #### Entity protocols ####
707   
708    def giveMintKey(self,transport):
709        protocol = protocols.giveMintKeyProtocol(self)
710        transport.setProtocol(protocol)
711        transport.start()
712
713    def listen(self,transport):
714        """Listen is the main operator between an issuer and a wallet.
715        >>> import transports, tests, base64
716        >>> tid = base64.b64encode('foobar')
717        >>> ie = tests.makeIssuerEntity()
718        >>> import calendar
719        >>> ie.getTime = lambda: calendar.timegm((2008,01,31,0,0,0))
720        >>> ie.issuer.getTime = ie.mint.getTime = ie.dsdb.getTime = ie.getTime
721        >>> stt = transports.SimpleTestTransport()
722        >>> ie.issuer.listen(stt)
723        >>> stt.send('HANDSHAKE',[['protocol', 'opencoin 1.0']])
724        <Message('HANDSHAKE_ACCEPT',[['protocol', 'opencoin 1.0'], ['cdd_version', '0']])>
725
726        >>> stt.send('TRANSFER_TOKEN_REQUEST',[tid, 'my account', [], [tests.coinA.toPython()], [['type', 'redeem']]])
727        <Message('TRANSFER_TOKEN_ACCEPT',['Zm9vYmFy', []])>
728
729        >>> stt.send('MINT_KEY_FETCH_DENOMINATION',[['1'], '0'])
730        <Message('MINT_KEY_PASS',[...]])>
731
732        >>> stt.send('MINT_KEY_FETCH_KEYID', [tests.mint_key1.encodeField('key_identifier')])
733        <Message('MINT_KEY_PASS',[...]])>
734
735        >>> stt.send('FETCH_CDD_REQUEST', '0')
736        <Message('FETCH_CDD_PASS',[...])>
737
738        >>> stt.send('foo')
739        <Message('PROTOCOL_ERROR','send again...')>
740
741        >>> stt.send('FETCH_CDD_REQUEST', '0')
742        <Message('FETCH_CDD_PASS',[...])>
743
744        >>> stt.send('GOODBYE')
745        <Message('GOODBYE',None)>
746        >>> stt.send('foobar')
747        """
748        if hasattr(transport, 'protocol') and transport.protocol:
749            # transport will only have protocol if we are looping on ourselves. Don't
750            # setup protocol again. Just reset the transport to use the protocol and
751            # reset the protocol itself
752            transport.setProtocol(self.protocol)
753            self.protocol.newState(self.protocol.start)
754
755        else:
756            protocol = protocols.answerHandshakeProtocol(handshake_options=[['cdd_version',self.getCurrentCDDVersion()]],
757                                                         arguments=self,
758                                                         TRANSFER_TOKEN_REQUEST=protocols.TransferTokenRecipient,
759                                                         MINT_KEY_FETCH_DENOMINATION=protocols.giveMintKeyProtocol,
760                                                         MINT_KEY_FETCH_KEYID=protocols.giveMintKeyProtocol,
761                                                         FETCH_CDD_REQUEST=protocols.giveCDDProtocol)
762            self.protocol = protocol
763            transport.autoreset = self.listen
764            transport.setProtocol(protocol)
765            transport.start()
766
767
768    #### Helper functions ####
769
770    def transferToTarget(self,target,coins):
771        """Transfers coins to a target.
772       
773        Returns True if success, False if error.
774        """
775        return True
776
777    def debitTarget(self,target,blinds):
778        """Debits a target by an amount of what blinds is worth
779       
780        Returns True if success, False if error.
781        """
782        return True
783       
784    def transferTokenRequestHelper(self, transaction_id, target, blindslist, tokens, options):
785        if 'type' not in options:
786            return 'REJECT', ['Options', 'Reject', []]
787
788        # Start doing things
789        if options['type'] == 'redeem':
790
791            success, obsolete, failures = self.redeemTokens(transaction_id, tokens, options)
792
793            if not success:
794                # tokens are not locked if not successful
795                self.addTransaction(transaction_id, type='Redeem', status='Reject', added=self.getTime(),
796                                    obsolete=obsolete, response=failures)
797                return 'REJECT', failures
798
799            # transmit funds
800            if not self.transferToTarget(target, tokens):
801                self.dsdb.unlock(transaction_id)
802                failures = ['Target', 'Rejected', []]
803                self.addTransaction(transaction_id, type='Redeem', status='Reject', added=self.getTime(),
804                                    obsolete=obsolete, response=failures)
805                return 'REJECT', failures
806
807            # register the tokens as spent
808            self.dsdb.spend(transaction_id, tokens)
809
810            self.addTransaction(transaction_id, type='Redeem', status='Accept', added=self.getTime(),
811                                obsolete=obsolete, response=failures, signed_blinds=[])
812            return 'ACCEPT', []
813
814        elif options['type'] == 'mint':
815
816            # check that we have the keys
817            try:
818                blinds = [[self.keyids[keyid], blinds] for keyid, blinds in blindslist]
819            except KeyError:
820                obsolete = self.getTime() + 86400 # FIXME: Hardcoded min obsolete
821                details = []
822                for keyid, blinds in blindslist:
823                    try:
824                        mintKey = self.keyids[keyid]
825                        details.append('None')
826                        obsolete = max(obsolete, mintKey.token_not_after)
827                    except KeyError:
828                        details.append('None')
829                self.addTransaction(transaction_id, type='Mint', status='Failure', added=self.getTime(),
830                                    obsolete=obsolete, response=['Blind', 'See detail', details], numblinds=0)
831                return 'REJECT', ['Blind', 'See detail', details]
832
833            numblinds = 0
834            for key, blindslist in blinds:
835                numblinds = numblinds + len(blindslist)
836
837            # check that the keys are usable
838            success, obsolete, failures = self.verifyMintableBlinds(blinds, options)
839            if not success:
840                self.addTransaction(transaction_id, type='Mint', status='Failure', added=self.getTime(),
841                                    obsolete=obsolete, numblinds=numblinds, response=failures)
842                return 'REJECT', failures
843
844            #check target
845            if not self.debitTarget(target,blindslist):
846                self.addTransaction(transaction_id, type='Mint', status='Failure', added=self.getTime(),
847                                    obsolete=obsolete, numblinds=numblinds, response=['Target', 'Rejected', []])
848                return 'REJECT', ['Target', 'Rejected', []]
849
850            success, additional = self.submitMintableBlinds(transaction_id, blinds, options)
851            if not success:
852                failures = additional
853                self.addTransaction(transaction_id, type='Mint', status='Failure', added=self.getTime(),
854                                    obsolete=obsolete, numblinds=numblinds, response=failures)
855                return 'REJECT', failures
856
857            delay = additional
858            self.addTransaction(transaction_id, type='Mint', status='Delayed', added=self.getTime(),
859                                obsolete=obsolete, numblinds=numblinds, expected=self.getTime() + int(delay))
860            # FIXME: If we can only send a delay, the only useful logic is in the if
861            if delay != '0':
862                return 'DELAY', str(delay)
863
864            else:
865                response, additional = self.resumeTransaction(transaction_id)
866                if response == 'PASS':
867                    signed_blinds = additional
868                    return 'ACCEPT', signed_blinds
869                elif response == 'REJECT':
870                    failures = additional
871                    return 'REJECT', failures
872                elif response == 'DELAY':
873                    time = additional
874                    return 'DELAY', time
875                else:
876                    raise NotImplementedError('Got an impossible response')
877
878        elif options['type'] == 'exchange':
879
880            # check tokens
881            success, obsolete, failures = self.redeemTokens(transaction_id, tokens, options)
882            if not success:
883                self.addTransaction(transaction_id, type='Exchange', status='Reject', added=self.getTime(),
884                                    obsolete=obsolete, target=target, options=options, amount=0,
885                                    response=failures)
886                return 'REJECT', failures
887
888            # And onto the blinds
889
890            # check that we have the keys
891            try:
892                blinds = [[self.keyids[keyid], blinds] for keyid, blinds in blindslist]
893            except KeyError:
894                details = []
895                for keyid, blinds in blindslist:
896                    try:
897                        mintKey = self.keyids[keyid]
898                        details.append('None')
899                        obsolete = max(obsolete, mintKey.token_not_after)
900                    except KeyError:
901                        details.append('None')
902                self.addTransaction(transaction_id, type='Exchange', status='Failure', added=self.getTime(),
903                                    obsolete=obsolete, response=['Blind', 'See detail', details], numblinds=0,
904                                    target=target, options=options, amount=0)
905                return 'REJECT', ['Blind', 'See detail', details]
906
907            #check target
908            if not self.debitTarget(target,blindslist):
909                self.dsdb.unlock(transaction_id)
910                self.addTransaction(transaction_id, type='Exchange', status='Failure', added=self.getTime(),
911                                    obsolete=obsolete, response=['Target', 'Rejected', []], numblinds=0,
912                                    target=target, options=options, amount=0)
913                return 'REJECT', ['Target', 'Rejected', []]
914
915            # check mintifyable blinds
916            success, an_obsolete, failures = self.verifyMintableBlinds(blinds, options)
917            obsolete = max(obsolete, an_obsolete)
918
919            if not success:
920                self.addTransaction(transaction_id, type='Exchange', status='Failure', added=self.getTime(),
921                                    obsolete=obsolete, response=failures, numblinds=0,
922                                    target=target, options=options, amount=0)
923                return 'REJECT', failures
924
925            # Make sure that we have the same amount of tokens as mintings
926            total = 0
927            for b in blinds:
928                total += int(b[0].denomination) * len(b[1])
929
930            if total != sum(tokens):
931                self.dsdb.unlock(transaction_id)
932                self.addTransaction(transaction_id, type='Exchange', status='Failure', added=self.getTime(),
933                                    obsolete=obsolete, response=['Generic', 'Rejected', []], numblinds=0,
934                                    target=target, options=options, amount=0)
935                return 'REJECT', ['Generic', 'Rejected', []]
936
937            # FIXME: This code implements the 'mark as spent if we send a delay'
938            #        method of handling delayed minting and any problems. However
939            # FIXME  we have not implemented the solution, allowing reminting with
940            #        the value of the money stored.
941
942            success, additional = self.submitMintableBlinds(transaction_id, blinds, options)
943            if not success:
944                self.dsdb.unlock(transaction_id)
945                failures = additional
946                self.addTransaction(transaction_id, type='Exchange', status='Failure', added=self.getTime(),
947                                    obsolete=obsolete, response=failures, numblinds=0,
948                                    target=target, options=options, amount=0)
949                return 'REJECT', failures
950
951            delay = additional
952
953            # calculate numblinds and amount
954            numblinds = 0
955            for key, blindslist in blinds:
956                numblinds = numblinds + len(blindslist)
957
958            self.addTransaction(transaction_id, type='Exchange', status='Delayed', added=self.getTime(),
959                                obsolete=obsolete, response=failures, expected=self.getTime() + int(delay),
960                                numblinds=numblinds, target=target, options=options, amount=0)
961            # FIXME: If we can only send a delay, the only useful logic is in the if
962            if delay != '0':
963                self.updateTransaction(transaction_id, amount=total)
964                self.spend(transaction_id, tokens)
965                return 'DELAY', str(delay)
966
967            else:
968                response, additional = self.resumeTransaction(transaction_id)
969                if response == 'PASS':
970                    signed_blinds = additional
971                    self.updateTransaction(transaction_id, amount=total)
972                    self.dsdb.spend(transaction_id, tokens)
973                    return 'ACCEPT', signed_blinds
974                elif response == 'REJECT':
975                    self.dsdb.unlock(transaction_id)
976                    failures = additional
977                    return 'REJECT', failures
978                elif response == 'DELAY':
979                    time = additional
980                    self.updateTransaction(transaction_id, amount=total)
981                    self.dsdb.spend(transaction_id, tokens)
982                    return 'DELAY', str(time)
983                else:
984                    raise NotImplementedError('Got an impossible response')
985
986        else:
987            # FIXME: the transaction added pretends to be a Redeem since that carries little state
988            # FIXME: hardcoded obsolete
989            self.addTransaction(transaction_id, type='Redeem', status='Reject', added=self.getTime(),
990                                obsolete=self.getTime() + 86400, response=['Options', 'Rejected', []])
991            return 'REJECT', ['Option', 'Rejected', []]
992
993
994    def redeemTokens(self, transaction_id, tokens, options):
995        """verifies the tokens and locks them.
996       
997        Returns a tuple of (locked, obsolete, [failures|None]).
998        Locked is a boolean specifying if the tokens are locked or not
999        Obsolete is the time for obsolete
1000        Failures is a tuple of (type, reason, reason_detail) to return in case locked is false
1001       
1002        failures may be None if there were no failures
1003
1004        """
1005       
1006        # This will fail if we try to lock with an already-known request_id.
1007       
1008        failures = []
1009        obsolete = self.getTime() + 86400 # FIXME: hardcoded min obsolete
1010
1011        if not tokens:
1012            return ('TRANSFER_TOKEN_REJECT', obsolete, ('Token', 'Rejected', []))
1013
1014        #check if tokens are valid
1015        for token in tokens:
1016            mintKey = self.mintKeysByKeyID.get(token.key_identifier, None)
1017            if mintKey:
1018                obsolete = max(obsolete, mintKey.token_not_after)
1019                if not token.validate_with_CDD_and_MintKey(self.getCDD(), mintKey):
1020                    failures.append(token)
1021            else:
1022                failures.append(token)
1023       
1024        if failures:
1025            details = []
1026            for token in tokens:
1027                mintKey = self.mintKeysByKeyID.get(token.key_identifier, None)
1028                if not mintKey:
1029                    details.append('Invalid key_identifier')
1030                    continue
1031                if token not in failures:
1032                    details.append('None')
1033                else:
1034                    details.append('Invalid token')
1035
1036            return (False, obsolete, ('Token', 'See detail', details))
1037
1038        #and not double spent
1039        try:
1040            #XXX have adjustable time for lock - not really needed. We unlock anyways, or spend
1041            self.dsdb.lock(transaction_id, tokens, 86400)
1042        except LockingError, e:
1043            reasons = []
1044            locking_error = False
1045            for token in tokens:
1046                status = self.dsdb.check(token)
1047                if status == 'Locked':
1048                    reasons.append('Token already spent')
1049                    locking_error = True
1050                elif status == 'Spent':
1051                    reasons.append('Token already spent')
1052                    locking_error = True
1053                elif status == 'Unlocked':
1054                    reasons.append('None')
1055                else:
1056                    raise NotImplementedError('Impossible string')
1057            if locking_error:
1058                return (False, obsolete, ('Token', 'See detail', reasons))
1059            else: # The problem is that the transaction_id is locked
1060                return (False, obsolete, ('Token', 'Rejected')) #FIXME: Should this be a different error?
1061
1062        return (True, obsolete, None)
1063
1064    def verifyMintableBlinds(self, blindslist, options):
1065        """returns a tuple of (success, obsolete, [failures|None]).
1066       
1067        success is a boolean set to true if they are mintable, otherwise false
1068        obsolete is the time till obsolescence
1069        failures is a tuple of (type, reason, reason-detail) to return in case of a reject
1070       
1071        blindslist is a list of [ [MintKey, [blind1, blind2...]], [MintKey....]]
1072        """
1073       
1074        #check the MintKeys for validity
1075        timeNow = self.getTime()
1076        obsolete = timeNow + 86400 # FIXME: Hardcoded min obsolete
1077        failures = []
1078        for mintKey, blindlist in blindslist:
1079            can_mint, can_redeem = mintKey.verify_time(timeNow)
1080            if not can_mint:
1081                # TODO: We need more logic here. can_mint only specifies if we are
1082                # between not_before and key_not_after. We may also need to do the
1083                # checking of the period of time the mint can mint but the IS cannot
1084                # send the key to the mint.
1085                failures.append(mintKey.key_identifier)
1086            obsolete = max(obsolete, mintKey.token_not_after)
1087
1088        if failures:
1089            reasons = []
1090            for mintKey, blindlist in blindslist:
1091                if mintKey.key_identifier not in failures:
1092                    reasons.append('None')
1093                else:
1094                    if timeNow < mintKey.not_before:
1095                        reasons.append('Key too soon')
1096                    elif timeNow > mintKey.key_not_after: # TODO: Another place to put fudge time
1097                        reasons.append('Key expired')
1098                    else:
1099                        raise NotImplementedError('We failed for no reason')
1100            return (False, obsolete, ('Blind', 'See detail', reasons))
1101
1102        else:
1103            return (True, obsolete, None)
1104           
1105    def submitMintableBlinds(self, transaction_id, blindslist, options):
1106        """returns a tuple of (success, [time|failures]) after submitting blinds to the mint.
1107       
1108        success is a boolean set to true if we successfully submitted. False otherwise.
1109                Note: it can be false if we have JITM and it has already failed.
1110        time is a stringint time in seconds to pass with a 'DELAY'
1111        failures is a tuple of (type, reason, reason_detail) to be passed if a 'REJECT'.
1112        """
1113        #FIXME: Do something with options
1114        time = self.mint.submit(transaction_id, blindslist)
1115
1116        if not time: # we had an error
1117            response = self.resumeTransaction(transaction_id)
1118            failure, reasons = response
1119            if failure != 'REJECT':
1120                raise Exception('Got no time but failure was not reject')
1121            return (False, reasons)
1122
1123        remaining = self.getTime() - time
1124        if remaining < 0:
1125            remaining = 0
1126
1127        return (True, str(int(remaining)))
1128
1129    def resumeTransaction(self, transaction_id):
1130        """Attempts to resume a transaction."""
1131        #FIXME: Only resumes minting/exchanges right now
1132        self.resumeTransactionHelper()
1133        return self.getTransaction(transaction_id)
1134
1135    def resumeTransactionHelper(self):
1136        """Moves completed transactions from the mint to the IS."""
1137        try:
1138            transaction = self.mint.completedTransactions.pop(0) # FIFO
1139        except IndexError:
1140            return
1141
1142        while True:
1143            transaction_id = transaction['transaction_id']
1144            del transaction['transaction_id']
1145           
1146            self.updateTransaction(transaction_id, **transaction)
1147
1148            try:
1149                transaction = self.mint.completedTransactions.pop(0) # FIFO
1150            except IndexError:
1151                return
1152       
1153    # The transaction_id storage
1154    def addTransaction(self, transaction_id, type, status, added=None, obsolete=None, **kwargs):
1155        """adds a TRANSFER_TOKEN_REQUEST transaction
1156
1157        transactions are held in self.transactions. Each transaction is a dict
1158        with certain fields depending on its value
1159
1160        Each transaction has certain fields.
1161        All transactions have 'type', 'status', 'added', 'obsolete' fields.
1162       
1163        'added' is the time when the transaction was added
1164        'obsolete' is the time when the transaction will be obsolete
1165        If the 'type' is 'Mint':
1166            A field 'numblinds' of the number of blinds
1167            If 'status' is 'Delayed':
1168                A field 'expected' with the time expected to be complete
1169            If 'status' is 'Minted':
1170                A 'completed' field of when the minting was completed
1171                A 'signed_blinds' field of all the signed blinds
1172            If 'status' is 'Failure':
1173                A 'completed' field of when the minting was completed
1174                A 'response' field of the complete response for a _REJECT.
1175                        The response has all the information and should be
1176                        scrubbed of too much information somewhere else
1177
1178        If the 'type' is 'Redeem':
1179            If 'status' is 'Accept':
1180                A 'signed_blinds' field of the signed blinds (empty)
1181            If 'status' is 'Reject':
1182                A 'response' field like the one for a 'type' of 'Mint'
1183
1184        If the 'type' is 'Exchange':
1185            A field 'target' of the target for the exchange
1186            A 'options' field with the options for the exchange
1187            A 'amount' field with the amount of tokens in the exchange
1188            If 'status' is 'Reject':
1189                A 'response' field like the one for a 'type' of 'Mint'
1190            If 'status' is Failure':
1191                All the fields of a 'Failure' of type mint
1192            If 'status' is 'Delayed':
1193                All the fields of a 'Delayed' of type mint
1194            If 'status' is 'Minted':
1195                All the fields of a 'Minted' of type mint
1196
1197        If the 'type' is 'Deleted':
1198            No other fields
1199
1200        Now, it should be easy to see than an exchange just stores some extra
1201        information but otherwise works exactly like minting or redeeming.
1202
1203        """
1204        transaction = kwargs
1205        transaction['type'] = type
1206        transaction['status'] = status
1207        transaction['added'] = added
1208        transaction['obsolete'] = obsolete
1209
1210        transaction['lock'] = 'addTransaction'
1211
1212        the_transaction = self.transactions.setdefault(transaction_id, transaction)
1213        if the_transaction is not transaction:
1214            raise Exception('Trying to add a transaction that already exists')
1215
1216        del transaction['lock']
1217        return
1218
1219    def getTransaction(self, transaction_id, lockobj=None):
1220        """Looks up the transaction and returns information for the protocol.
1221
1222        Returns (type, value) where type is the string 'DELAY', 'REJECT', or
1223        'ACCEPT', and the value is the rest of the arguments needed (delay time,
1224        tuple of reject, or the signed blinds
1225        """
1226        try:
1227            transaction = self.transactions[transaction_id]
1228        except KeyError:
1229            return ('REJECT', ('Generic', 'Unknown transaction_id', ()))
1230
1231        if not lockobj:
1232            lockobj = 'getTransaction'
1233        lock = transaction.setdefault('lock', lockobj)
1234        while lock is not lockobj:
1235            lock = transaction.setdefault('lock', lockobj)
1236
1237        if transaction['type'] == 'Deleted':
1238            # The transaction has been deleted while we were locking, so
1239            # we need to get the transaction again and lock that one.
1240            # Ensure that everyone else can find out that the transaction
1241            # has been deleted, and call ourselves
1242            del transaction['lock']
1243            return self.getTransaction(transaction_id)
1244
1245        if transaction['status'] == 'Delayed': # Mint or Exchange
1246            time = max(0, int(self.getTime() - transaction['expected']))
1247            del transaction['lock']
1248            return ('DELAY', str(time))
1249
1250        elif transaction['status'] == 'Failure': # Mint or Exchange
1251            response = transaction['response']
1252            del transaction['lock']
1253            return ('REJECT', response)
1254
1255        elif transaction['status'] == 'Reject': # Redeem or Exchange
1256            response = transaction['response']
1257            del transaction['lock']
1258            return ('REJECT', response)
1259
1260        elif transaction['status'] == 'Minted': # Mint or Exchange
1261            signed_blinds = transaction['signed_blinds']
1262            del transaction['lock']
1263            return ('PASS', signed_blinds)
1264
1265        elif transaction['status'] == 'Accept': # Redeem
1266            del transaction['lock']
1267            return ('PASS', [])
1268
1269        else:
1270            raise NotImplementedError('Impossible status string: %s' % transaction['status'])
1271
1272    def updateTransaction(self, transaction_id, lockobj=None, **kwargs):
1273        transaction = self.transactions[transaction_id]
1274
1275        if not lockobj:
1276            lockobj = 'updateTransaction'
1277        lock = transaction.setdefault('lock', lockobj)
1278        while lock is not lockobj:
1279            lock = transaction.setdefault('lock', lockobj)
1280
1281        if transaction['type'] == 'Deleted':
1282            del transaction['lock']
1283            return self.updateTransaction(transaction_id, kwargs)
1284
1285        if transaction['type'] != 'Mint' and transaction['type'] != 'Exchange':
1286            del transaction['lock']
1287            raise Exception('Unable to update. Wrong type: %s' % transaction['type'])
1288
1289        try:
1290            transaction.update(kwargs)
1291        except:
1292            del transaction['lock']
1293            raise
1294
1295        if transaction['status'] != 'Delayed' and 'expected' in transaction:
1296            del transaction['expected']
1297
1298        del transaction['lock']
1299
1300
1301    def delTransaction(self, transaction_id, lockobj=None):
1302        """deletes a transaction."""
1303        try:
1304            transaction = self.transactions(transaction_id)
1305        except KeyError:
1306            # FIXME: what should we do here?
1307            pass
1308
1309        if not lockobj:
1310            lockobj = 'delTransaction'
1311        lock = transaction.setdefault('lock', lockobj)
1312        if lock is not lockobj:
1313            #FIXME: what should we do here. I'm just going to fail
1314            raise Exception('Trying to delete something in use')
1315
1316        del self.transactions[transaction_id]
1317        transaction['status'] = 'Deleted'
1318        del transaction['lock']
1319
1320        return
1321       
1322
1323class KeyFetchError(Exception):
1324    pass
1325
1326
1327#################### dsdb ###############################
1328
1329class LockingError(Exception):
1330    pass
1331
1332
1333class DSDB:
1334    """A double spending database.
1335
1336    This DSDB is a simple DSDB. It only allows locking, unlocking, and
1337    spending a list of tokens. It is designed in a way to make it easier
1338    to make it race-safe (although that is not done yet).
1339   
1340    FIXME: It does extremely lazy evaluation of expiration of locks.
1341           When it tries to lock a token, it may find that there is already
1342           a lock on the token. It checks the times, and if the time has
1343           passed on the lock, it removes lock on all tokens in the transaction.
1344
1345           This allows a possible DoS where they flood they DSDB with locks for
1346           fake keys, hoping to exhaust resources.
1347
1348    NOTE: The functions of the DSDB have very simple logic. They only care about
1349          the values of key_identifier and serial for each token passed in. Any
1350          checking of the base types should be vetted prior to actually
1351          interfacing with the DSDB
1352
1353          TODO: Maybe make an extractor for the token so we only see the parts
1354                we need. This would allow locking with one type and spending
1355                with another type. (e.g.: lock with Blanks, spend with Coins)
1356
1357    >>> dsdb = DSDB()
1358
1359    >>> class test:
1360    ...     def __init__(self, key_identifier, serial):
1361    ...         self.key_identifier = key_identifier
1362    ...         self.serial = serial
1363
1364    >>> tokens = []
1365    >>> for i in range(10):
1366    ...     tokens.append(test('2', i))
1367
1368    >>> token = tokens[-1]
1369   
1370    >>> dsdb.lock(3, (token,), 1)
1371    >>> dsdb.unlock(3)
1372    >>> dsdb.lock(3, (token,), 1)
1373    >>> dsdb.spend(3, (token,))
1374    >>> dsdb.unlock(3)
1375    Traceback (most recent call last):
1376       ...
1377    LockingError: Unknown transaction_id
1378
1379    >>> dsdb.lock(4, (token,), 1)
1380    Traceback (most recent call last):
1381       ...
1382    LockingError: Token already spent
1383 
1384    Ensure trying to lock the same token twice doesn't work
1385    >>> dsdb.lock(4, (tokens[0], tokens[0]), 1)
1386    Traceback (most recent call last):
1387       ...
1388    LockingError: Token locked
1389   
1390    >>> dsdb.spend(4, (tokens[0], tokens[0]))
1391    Traceback (most recent call last):
1392       ...
1393    LockingError: Token locked
1394
1395    Try to sneak in double token when already locked
1396    >>> dsdb.lock(4, tokens[:2], 1)
1397    >>> dsdb.spend(4, (tokens[0], tokens[0]))
1398    Traceback (most recent call last):
1399       ...
1400    LockingError: Unknown token
1401
1402    Try to feed different tokens between the lock and spend
1403    >>> dsdb.spend(4, (tokens[0], tokens[2]))
1404    Traceback (most recent call last):
1405       ...
1406    LockingError: Unknown token
1407
1408    Actually show we can handle multiple tokens for spending
1409    >>> dsdb.spend(4, (tokens[0], tokens[1]))
1410
1411    And that the tokens can be in different orders between lock and spend
1412    >>> dsdb.lock(5, (tokens[2], tokens[3]), 1)
1413    >>> dsdb.spend(5, (tokens[3], tokens[2]))
1414
1415    Respending a single token causes the entire transaction to fail
1416    >>> dsdb.spend(6, (tokens[4], tokens[3]))
1417    Traceback (most recent call last):
1418       ...
1419    LockingError: Token already spent
1420
1421    But doesn't cause other tokens to be affected
1422    >>> dsdb.spend(6, (tokens[4],))
1423
1424    We have to have locked tokens to spend if not automatic
1425    >>> dsdb.spend(7, (tokens[5],), automatic_lock=False)
1426    Traceback (most recent call last):
1427       ...
1428    LockingError: Unknown transaction_id
1429
1430    And we can't relock (FIXME: I just made up this requirement.)
1431    >>> dsdb.lock(8, (tokens[6],), 1)
1432   
1433    >>> dsdb.lock(8, (tokens[6],), 1)
1434    Traceback (most recent call last):
1435       ...
1436    LockingError: id already locked
1437
1438    Check to make sure that we can lock different key_id's and same serial
1439    >>> tokens[7].key_identifier = '2'
1440    >>> tokens[7].key_identifier
1441    '2'
1442    >>> dsdb.spend(9, (tokens[7], tokens[8]))
1443    """
1444
1445    def __init__(self, database=None, locks=None):
1446        self.database = database or {} # a dictionary by MintKey of (dictionaries by
1447                                       #   serial of tuple of ('Spent',), ('Locked', time_expire, id))
1448        self.locks = locks or {} # a dictionary by id of tuple of (time_expire, list(tokens), {'lock':[...]})
1449                                 # the dict is to ensure only one thread plays with a transaction at a time
1450        self.getTime = getTime
1451
1452    def lock(self, id, tokens, lock_duration, lockobj=None):
1453        """Lock the tokens.
1454        Tokens are taken as a group. It tries to lock each token one at a time. If it fails,
1455        it unwinds the locked tokens are reports a failure. If it succeeds, it adds the lock
1456        to the locks.
1457        Note: This function performs no checks on the validity of the coins, just blindly allows
1458        them to be locked
1459        """
1460       
1461        lock_time = lock_duration + self.getTime()
1462
1463        unlock = not lockobj # if passed in a lock obj, do not automatically unlock
1464
1465        if not lockobj:
1466            lockobj = ['lock']
1467        my_locks = (lock_time, [], {'Lock':lockobj})
1468        locks = self.locks.setdefault(id, my_locks)
1469
1470        if my_locks is not locks:
1471            raise LockingError('id already locked')
1472       
1473        tokens = list(tokens[:])
1474
1475        reason = None
1476       
1477        for token in tokens:
1478            key_dict = self.database.setdefault(token.key_identifier, {})
1479           
1480            my_lock = ('Locked', lock_time, id)
1481            lock = key_dict.setdefault(token.serial, my_lock)
1482
1483            exit = False
1484
1485            while lock is not my_lock:
1486                if lock[0] == 'Spent':
1487                    reason = 'Token already spent'
1488                    exit = True # break out of the for loop
1489                    break
1490                elif lock[0] == 'Locked':
1491                    # XXX: This implements lazy unlocking. Possible DoS attack vector
1492                    # Active unlocking would just break
1493                    if lock[1] > self.getTime(): # If the lock hasn't expired
1494                        reason = 'Token locked'
1495                        exit = True
1496                        break
1497                    else:
1498                        try:
1499                            self.unlock(lock[2])
1500                        except LockingError:
1501                            pass # Only locking error is if it is already unlocked
1502
1503                        # It should be unlocked. Now try to lock it again
1504                        lock = key_dict.setdefault(token.serial, my_lock)
1505                       
1506                else:
1507                    raise NotImplementedError('Impossible string')
1508
1509            if exit: # break out of for loop
1510                break
1511
1512            self.locks[id][1].append(token)
1513
1514        if reason:
1515            self.unlock(id, lockobj)
1516            raise LockingError(reason)
1517
1518        # clear the lock
1519        if unlock:
1520            del locks[2]['Lock']
1521
1522        return
1523
1524    def unlock(self, id, lockobj=None):
1525        """Unlocks an id from the dsdb.
1526        This only unlocks if a transaction is locked. If the transaction is
1527        completed and the token is spent, it cannot unlock.
1528        """
1529       
1530        lock = self.locks.get(id, None)
1531        if not lock:
1532            raise LockingError('Unknown transaction_id')
1533
1534        if not lockobj:
1535            lockobj = ['unlock']
1536
1537        lockcheck = lock[2].setdefault('Lock', lockobj)
1538        if lockcheck is not lockobj:
1539            raise LockingError('Unknown trasaction_id')
1540
1541        # check to make sure we have the real and current lock
1542        if lock is not self.locks.get(id, None):
1543            raise LockingError('Unknown transaction_id')
1544
1545        lazy_unlocked = False
1546        if lock[0] < self.getTime(): # unlock and then error
1547            lazy_unlocked = True
1548
1549        for token in lock[1]:
1550            del self.database[token.key_identifier][token.serial]
1551            # Can not delete self.database[token.key_identifier] if it
1552            # is empty since it introduces a race condition
1553
1554        del self.locks[id]
1555
1556        if lazy_unlocked:
1557            raise LockingError('Unknown transaction_id')
1558
1559        return
1560
1561    def spend(self, id, tokens, automatic_lock=True, lockobj=None):
1562        """Spend verifies the tokens are locked (or locks them) and marks the tokens as spent.
1563        FIXME: Small tidbit of code in place for lazy unlocking.
1564        FIXME: automatic_lock doesn't automatically unlock if it locked and the spending fails (how can it though?)
1565        """
1566        if not lockobj:
1567            lockobj = ['spend']
1568
1569        lock = self.locks.get(id, None)
1570        if not lock:
1571            if automatic_lock:
1572                # we can spend without locking, so lock now.
1573                self.lock(id, tokens, 86400, lockobj)
1574                lock = self.locks[id] # We have it locked
1575            else:
1576                raise LockingError('Unknown transaction_id')
1577
1578        selflock = lock[2].setdefault('Lock', lockobj)
1579        if selflock is not lockobj:
1580            raise LockingError('Unknown transaction_id')
1581
1582        if self.locks[id][0] < self.getTime():
1583            self.unlock(id, lockobj)
1584            raise LockingError('Unknown transaction_id')
1585       
1586        # check to ensure locked tokens are the same as current tokens.
1587        if len(set(tokens)) != len(self.locks[id][1]): # self.locks[id] is guarenteed to have unique values by lock
1588            del lock[2]['Lock']
1589            raise LockingError('Unknown token')
1590
1591        for token in self.locks[id][1]:
1592            if token not in tokens:
1593                del lock[2]['Lock']
1594                raise LockingError('Unknown token')
1595
1596        # we know all the tokens are valid. Change them to locked
1597        for token in self.locks[id][1]:
1598            self.database[token.key_identifier][token.serial] = ('Spent',)
1599
1600        del self.locks[id]
1601
1602        return
1603
1604    def check(self, token):
1605        """Checks to see if a token is locked
1606        It checks a single token to see if it is locked or not.
1607        """
1608       
1609        key_dict = self.database.setdefault(token.key_identifier, {})
1610           
1611        # XXX: Implements lazy unlocking. Only unlock once.
1612        try:
1613            lock = self.database[token.key_identifier][token.serial]
1614        except KeyError:
1615            return 'Unlocked'
1616
1617        if lock[0] == 'Spent':
1618            return 'Spent'
1619        elif lock[0] == 'Locked':
1620            # XXX: This implements lazy unlocking. Possible DoS attack vector
1621            # Active unlocking would just return 'Locked'
1622            if lock[1] > self.getTime(): # If the lock hasn't expired
1623                return 'Locked'
1624            else:
1625                try:
1626                    self.unlock(lock[2])
1627                except LockingError:
1628                    pass # Only locking error is if it is already unlocked
1629
1630                try:
1631                    lock = self.databasee[token.key_identifier][token.serial]
1632                except KeyError:
1633                    return 'Unlocked'
1634
1635                if lock[0] == 'Spent':
1636                    return 'Spent'
1637                elif lock[0] == 'Locked':
1638                    return 'Locked'
1639                       
1640                else:
1641                    raise NotImplementedError('Impossible string')
1642        else:
1643            raise NotImplementedError('Impossible string')
1644
1645
1646class Mint:
1647    """A Mint is the minting agent for a currency. It has the
1648    >>> m = Mint()
1649    >>> import calendar
1650    >>> m.getTime = lambda: calendar.timegm((2008,01,31,0,0,0))
1651
1652    >>> import tests, crypto, base64
1653    >>> mintKey = tests.mintKeys[0]
1654   
1655    This bit is a touch of a hack. Keys are normally made in the mint
1656    >>> m.privatekeys[mintKey.key_identifier] = tests.keys512[0]
1657
1658    >>> m.addMintKey(mintKey, crypto.RSASigningAlgorithm)
1659
1660    >>> base64.b64encode(m.signNow(mintKey.key_identifier, 'abcdefghijklmnop'))
1661    'Mq4dqFpKZEvbl+4HeXh0rGrqBk6Fm2bnUjNiVgirDvOuQf4Ty6ZkvpqB95jMyiwNlhx8A1qZmQv5biLM40emUg=='
1662   
1663    >>> m.signNow('abcd', 'efg')
1664    Traceback (most recent call last):
1665    ...
1666    MintError: KeyError: 'abcd'
1667
1668    """
1669    def __init__(self):
1670        self.keyids = {}
1671        self.privatekeys = {}
1672        self.sign_algs = {}
1673        self.getTime = getTime
1674
1675        self.waitingTransactions = []
1676        self.completedTransactions = []
1677
1678    def createNewKey(self, hash_alg, key_generator, size=1024):
1679        """creates a new keypair of a certain size.
1680        This is used by the IssuerEntity to create a new MintKey. The private
1681        key is only stored in the Mint
1682
1683        Note: A different but seperation of powers would be for the IssuerEntity
1684        to send the keypair to the Mint. This would prevent contamination of the
1685        IssuerEntity in case the Mint is compromised
1686
1687        >>> m = Mint()
1688        >>> import crypto
1689        >>> hash_alg = crypto.SHA256HashingAlgorithm
1690        >>> pub = m.createNewKey(hash_alg, crypto.createRSAKeyPair, 512)
1691        >>> pub.hasPrivate()
1692        False
1693        >>> key_id = pub.key_id(hash_alg)
1694        >>> pub == m.privatekeys[key_id].newPublicKeyPair()
1695        True
1696        """
1697        private, public = key_generator(size)
1698        self.privatekeys[private.key_id(hash_alg)] = private
1699        return public
1700
1701    def addMintKey(self, mintKey, sign_alg):
1702        """adds a mintkey and sign_alg for a mint key to the mint.
1703
1704        Requires the key to be in Mint.privateKeys (but verification
1705        does not occur to ensure the key is valid)
1706
1707        >>> import crypto, tests
1708        >>> m = Mint()
1709        >>> mintKey = tests.mintKeys[0]
1710        >>> sign_alg = crypto.RSASigningAlgorithm
1711
1712        First, make sure it fails when it doesn't know the key
1713        >>> m.addMintKey(mintKey, sign_alg)
1714        Traceback (most recent call last):
1715        MintError: Key not in Mint
1716        >>> mintKey.key_identifier not in m.keyids
1717        True
1718        >>> mintKey.key_identifier not in m.sign_algs
1719        True
1720
1721        >>> m.privatekeys[mintKey.key_identifier] = None
1722        >>> m.addMintKey(mintKey, sign_alg)
1723        >>> m.keyids[mintKey.key_identifier] == mintKey
1724        True
1725        >>> m.sign_algs[mintKey.key_identifier] == sign_alg
1726        True
1727        """
1728        if mintKey.key_identifier not in self.privatekeys:
1729            raise MintError('Key not in Mint')
1730        self.keyids[mintKey.key_identifier] = mintKey
1731        self.sign_algs[mintKey.key_identifier] = sign_alg
1732       
1733    def signNow(self, key_identifier, blind):
1734        """Performs JITM of a blind.
1735       
1736        >>> m = Mint()
1737        >>> import crypto, tests, base64, calendar
1738        >>> hash_alg = crypto.SHA256HashingAlgorithm
1739        >>> sign_alg = crypto.RSASigningAlgorithm
1740        >>> mintKey = tests.mint_key1
1741        >>> m.privatekeys = {mintKey.key_identifier:tests.mint_private_key1}
1742        >>> m.addMintKey(mintKey, sign_alg)
1743
1744        >>> blind = base64.b64decode('HIck+fim0TkjVupU1AeKpuSGN1CxLnDmT2jpBHMZSgdp' +
1745        ...                          'YhKE90XoAsQVznljEn4NTXvRs5cXslWUNvcUeAuv2A==')
1746
1747        This one passes
1748        >>> m.getTime = lambda: calendar.timegm((2008,01,31,0,0,0))
1749        >>> signedBlind = m.signNow(mintKey.key_identifier, blind)
1750        >>> base64.b64encode(signedBlind)
1751        'BJ597EK2lqlC4HN/C35v1MR5qG/476mzjS12qTomv8bjp6u9//W9RwOk6mijywTM6rg9quFuIXlTiVF9U6RJvA=='
1752
1753        >>> m.getTime = lambda: mintKey.not_before - 1
1754        >>> m.signNow(mintKey.key_identifier, blind)
1755        Traceback (most recent call last):
1756        MintError: MintKey not valid for minting
1757       
1758        >>> m.getTime = lambda: mintKey.key_not_after + 1
1759        >>> m.signNow(mintKey.key_identifier, blind)
1760        Traceback (most recent call last):
1761        MintError: MintKey not valid for minting
1762       
1763        """
1764        from crypto import CryptoError
1765        try:
1766            sign_alg = self.sign_algs[key_identifier]
1767            signing_key = self.privatekeys[key_identifier]
1768            mintKey = self.keyids[key_identifier]
1769        except KeyError, reason:
1770            raise MintError("KeyError: %s" % reason)
1771           
1772        signer = sign_alg(signing_key)
1773       
1774        if mintKey.verify_time(self.getTime())[0]: # if can_sign
1775            try:
1776                signature = signer.sign(blind)
1777            except CryptoError, reason:
1778                raise MintError("CryptoError: %s" % reason)
1779            return signature
1780        else:
1781            raise MintError("MintKey not valid for minting")
1782
1783    def submit(self, transaction_id, keys_and_blinds):
1784        """adds a minting transaction to the mint
1785
1786        transaction_id is used to allow the transactions to be recalled
1787        keys_and_blinds is a list of [key_identifier, [blinds]]
1788
1789        Returns the expected time the minting will be done
1790
1791        >>> m = Mint()
1792        >>> m.performMinting = lambda: None # Make into a noop
1793        >>> m.getTime = lambda: 15180
1794        >>> m.submit('abcd', [])
1795        15180
1796        >>> m.waitingTransactions
1797        [{'status': 'Minting', 'added': 15180, 'kandb': [], 'transaction_id': 'abcd'}]
1798
1799        >>> m.getTime = lambda: 15181
1800        >>> m.submit('efgh', [])
1801        15181
1802        >>> m.waitingTransactions
1803        [{'status': 'Minting', 'added': 15180, 'kandb': [], 'transaction_id': 'abcd'}, {'status': 'Minting', 'added': 15181, 'kandb': [], 'transaction_id': 'efgh'}]
1804        """
1805        # cheat for now and mint them before returning a time of now
1806
1807        transaction = {'status':'Minting', 'kandb':keys_and_blinds,
1808                       'added':self.getTime(), 'transaction_id':transaction_id}
1809        self.waitingTransactions.append(transaction)
1810
1811        # This is where we cheat
1812        self.performMinting()
1813
1814        return self.getTime() # expect it to be done right now
1815
1816    def performMinting(self):
1817        """Go through self.waitingTransactions and mint them all. Thread safe.
1818       
1819        >>> m = Mint()
1820        >>> import tests
1821        >>> realPerformMinting = m.performMinting
1822        >>> m.performMinting = lambda: None # Make into noop for now
1823        >>> m.getTime = lambda: 15180
1824        >>> m.submit('abcd', [])
1825        15180
1826        >>> m.getTime = lambda: 15181
1827        >>> m.submit('efgh', [])
1828        15181
1829        >>> m.getTime = lambda: 15182
1830        >>> m.performMinting = realPerformMinting
1831
1832        We have waiting transactions and no completed transactions
1833        >>> m.waitingTransactions and True
1834        True
1835        >>> m.completedTransactions or False
1836        False
1837
1838        And things work
1839        >>> m.performMinting()
1840        >>> m.waitingTransactions
1841        []
1842        >>> tests.printdictlist(m.completedTransactions)
1843        [{'added': 15180, 'completed': 15182, 'signed_blinds': [], 'status': 'Minted', 'transaction_id': 'abcd'}, {'added': 15181, 'completed': 15182, 'signed_blinds': [], 'status': 'Minted', 'transaction_id': 'efgh'}]
1844
1845        It doesn't fail if there are no transactions
1846        >>> m.waitingTransactions
1847        []
1848        >>> m.performMinting()
1849        >>> tests.printdictlist(m.completedTransactions)
1850        [{'added': 15180, 'completed': 15182, 'signed_blinds': [], 'status': 'Minted', 'transaction_id': 'abcd'}, {'added': 15181, 'completed': 15182, 'signed_blinds': [], 'status': 'Minted', 'transaction_id': 'efgh'}]
1851
1852        Okay. Now test failures
1853        >>> ie = tests.makeIssuerEntity()
1854        >>> m = ie.mint
1855        >>> m.getTime = lambda: tests.mint_key1.not_before
1856        >>> realPerformMinting = m.performMinting
1857        >>> m.performMinting = lambda: None
1858        >>> m.submit('abcd', [[tests.mint_key1, ['a' * (520/8)]]])
1859        1199145600
1860        >>> m.waitingTransactions and True
1861        True
1862        >>> m.completedTransactions
1863        []
1864        >>> realPerformMinting()
1865        >>> tests.printdictlist(m.completedTransactions)
1866        [{'added': 1199145600, 'completed': 1199145600, 'response': ['Blind', 'See detail', ['Unable to sign']], 'status': 'Failure', 'transaction_id': 'abcd'}]
1867
1868        Test a more complicated failure. Valid should always pass. Invalid has two failures.
1869        Partial has a good key and a bad key. key_id has a key that the mint doesn't know about.
1870        We make sure we only get one failure per mint_key.
1871        >>> m.completedTransactions = []
1872        >>> valid = [tests.mint_key1, ['a' * 40, 'b' * 40]]
1873        >>> invalid = [tests.mint_key2, ['a' * (520/8), 'b' * (520/8)]]
1874        >>> partial = [tests.mint_key2, ['a' * 40, 'b' * (520/8)]]
1875        >>> key_id = [tests.mint_key3, ['a' * 40, 'b' * (520/8)]]
1876        >>> m.submit('abcd', [valid, invalid, key_id])
1877        1199145600
1878        >>> realPerformMinting()
1879        >>> tests.printdictlist(m.completedTransactions)
1880        [{'added': 1199145600, 'completed': 1199145600, 'response': ['Blind', 'See detail', ['None', 'Unable to sign', 'Invalid key_identifier']], 'status': 'Failure', 'transaction_id': 'abcd'}]
1881
1882        Now I'm not sure if two sets with the same key_id is invalid or not.
1883        I'm testing seperately for now with partial since it has the same mintkey
1884        as invalid.
1885        >>> m.completedTransactions = []
1886        >>> m.submit('abcd', [partial, valid])
1887        1199145600
1888        >>> realPerformMinting()
1889        >>> tests.printdictlist(m.completedTransactions)
1890        [{'added': 1199145600, 'completed': 1199145600, 'response': ['Blind', 'See detail', ['Unable to sign', 'None']], 'status': 'Failure', 'transaction_id': 'abcd'}]
1891
1892        And Key too soon and Key expired
1893        >>> m.completedTransactions = []
1894        >>> m.getTime = lambda: tests.mint_key1.not_before - 1
1895        >>> m.submit('abcd', [invalid])
1896        1199145599
1897        >>> realPerformMinting()
1898        >>> tests.printdictlist(m.completedTransactions)
1899        [{'added': 1199145599, 'completed': 1199145599, 'response': ['Blind', 'See detail', ['Key too soon']], 'status': 'Failure', 'transaction_id': 'abcd'}]
1900
1901        >>> m.completedTransactions = []
1902        >>> m.getTime = lambda: tests.mint_key1.key_not_after + 1
1903        >>> m.submit('abcd', [invalid])
1904        1201824001
1905        >>> realPerformMinting()
1906        >>> tests.printdictlist(m.completedTransactions)
1907        [{'added': 1201824001, 'completed': 1201824001, 'response': ['Blind', 'See detail', ['Key expired']], 'status': 'Failure', 'transaction_id': 'abcd'}]
1908
1909        """
1910        import base64
1911        try:
1912            transaction = self.waitingTransactions.pop(0) # FIFO
1913        except IndexError:
1914            return
1915
1916        while transaction:
1917            keys_and_blinds = transaction['kandb']
1918            minted = []
1919            for key, blinds in keys_and_blinds:
1920                this_set = ['Success']
1921                for blind in blinds:
1922                    try:
1923                        signature = self.signNow(key.key_identifier, blind)
1924                    except MintError, reason:
1925                        this_set = ['Failure']
1926                        if reason.args[0].startswith('CryptoError'):
1927                            this_set.append('Unable to sign')
1928                        elif reason.args[0].startswith('KeyError'):
1929                            this_set.append('Invalid key_identifier')
1930                        elif reason.args[0] == 'MintKey not valid for minting':
1931                            if key.not_before > self.getTime():
1932                                this_set.append('Key too soon')
1933                            elif key.key_not_after < self.getTime():
1934                                this_set.append('Key expired')
1935                            else:
1936                                raise
1937                                # FIXME: This is where revoked key checks would go
1938                        else:
1939                            raise
1940                        break # Stop this key
1941
1942                    this_set.append(base64.b64encode(signature))
1943
1944                minted.append(this_set)
1945
1946            if 'Failure' not in [k[0] for k in minted]:
1947                signed = []
1948                for k in minted:
1949                    signed.extend(k[1:])
1950                transaction['status'] = 'Minted'
1951                transaction['signed_blinds'] = signed
1952                transaction['completed'] = self.getTime()
1953                del transaction['kandb']
1954            else:
1955                details = []
1956                for m in minted:
1957                    if m[0] == 'Success':
1958                        details.append('None')
1959                    else:
1960                        details.append(m[1])
1961                transaction['status'] = 'Failure'
1962                transaction['response'] = ['Blind', 'See detail', details]
1963                transaction['completed'] = self.getTime()
1964                del transaction['kandb']
1965
1966            self.completedTransactions.append(transaction)
1967
1968            try:
1969                transaction = self.waitingTransactions.pop(0) # FIFO
1970            except IndexError:
1971                return
1972       
1973
1974class MintError(Exception):
1975    pass
1976
1977
1978if __name__ == "__main__":
1979    import doctest
1980    doctest.testmod(optionflags=doctest.ELLIPSIS)
Note: See TracBrowser for help on using the browser.