root / trunk / pyopencoin / oc / protocols.py

Revision 245, 53.5 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 
1"""
2Protocol have states, which are basically methods that consume messages, do
3something and return messages. The states are just methods, and one state
4might change the state of its protocol to another state.
5
6A protocol writes to a transport, using Transport.write. It receives messages
7from the transport with Protocol.newMessage.
8
9A state (a protocol method) returns messages, it does not write directly back
10to a transport (XXX not sure about this, what if a state needs to communicate
11with another enity). Instead newMessage by default writes back to the transport.
12(XXX maybe the transport could take the returned message, and write it up its own,
13ah write method?)
14
15Before returning the message, the state should set the protocols state to the next
16state (sounds a bit ackward, its easy, check the states code)
17"""
18
19from messages import Message
20import containers   
21import types
22
23class Protocol:
24    """A protocol ties messages and actions togehter, it is basically one side
25       of an interaction. E.g. when A exchanges a coin with B, A would use the
26       walletSenderProtocol, and B the walletRecipientProtocol."""
27
28    def __init__(self):
29        'Set the initial state'
30       
31        self.state = self.start
32        self.result = None
33        self.done = None
34
35    def setTransport(self,transport):
36        'get the transport we are working with'
37       
38        self.transport = transport
39       
40    def start(self,message):
41        'this should be the initial state of the protocol'
42       
43        pass
44
45    def goodbye(self,message=None):
46        """Denotes the end of a chain of messages in the protocol."""
47        if message == None:
48            #FIXME: Setting a blank message to goodbye forces a GOODBYE message to be sent automatically
49            message = Message('GOODBYE')
50        #we are not done
51        if not self.done:
52            #maybe we need to reset?
53            if message.type != 'GOODBYE' and hasattr(self, 'transport') and hasattr(self.transport, 'autoreset'):
54                self.transport.autoreset(self.transport)
55                return self.transport.protocol.state(message)
56            #well, then lets be done, say goodbye to signal the fact
57            else:
58                self.done = True
59                return Message('GOODBYE')
60        else:
61            pass
62                   
63    def newMessage(self,message):
64        'this is used by a transport to pass on new messages to the protocol'
65
66        out = self.state(message)
67        self.transport.write(out)
68        return out
69
70    def newState(self,method):
71        self.state = method
72       
73    def initiateHandshake(self,message):
74        self.newState(self.verifyHandshake)
75        return Message('HANDSHAKE',[['protocol', 'opencoin 1.0']])
76
77    def verifyHandshake(self, message):
78        if message.type == 'HANDSHAKE_ACCEPT':
79            self.newState(self.firstStep)
80            # FIXME: If we have handshakes that return things, we need to check for them here
81            return self.firstStep(message)
82
83        elif message.type == 'HANDSHAKE_REJECT':
84            self.newState(self.goodbye)
85            # FIXME: force a hangup here? maybe just a loop around and do another handshake?
86            # FIXME: We need to do something here.
87           
88        else:
89            self.newState(self.goodbye)
90            return ProtocolErrorMessage('vH')
91
92#ProtocolErrorMessage = lambda x: Message('PROTOCOL_ERROR', 'send again %s' % x)
93ProtocolErrorMessage = lambda x: Message('PROTOCOL_ERROR', 'send again')
94
95class answerHandshakeProtocol(Protocol):
96    """The answering side of a HANDSHAKE.
97
98    >>> ahp = answerHandshakeProtocol(None)
99    >>> ahp.state(Message('HANDSHAKE', [['protocol', 'opencoin 1.0']]))
100    <Message('HANDSHAKE_ACCEPT',[['protocol', 'opencoin 1.0']])>
101
102    >>> ahp = answerHandshakeProtocol(None)
103    >>> ahp.state(Message('HANDSHAKE', [['protocol', 'opencoin 1.0+']]))
104    <Message('HANDSHAKE_ACCEPT',[['protocol', 'opencoin 1.0']])>
105   
106    >>> ahp = answerHandshakeProtocol(None)
107    >>> ahp.state(Message('HANDSHAKE', [['protocol', 'opencoin 1.1']]))
108    <Message('HANDSHAKE_REJECT','did not like the protocol version')>
109   
110    >>> ahp = answerHandshakeProtocol(None)
111    >>> ahp.state(Message('HANDSHAKE', [['not_protocol', 'opencoin 1.0']]))
112    <Message('PROTOCOL_ERROR','please do a handshake')>
113
114    >>> ahp = answerHandshakeProtocol(None)
115    >>> ahp.state(Message('NOT_HANDSHAKE', [['protocol', 'opencoin 1.0']]))
116    <Message('PROTOCOL_ERROR','please do a handshake')>
117
118    """
119
120    def __init__(self, arguments, handshake_options=None, **mapping):
121        Protocol.__init__(self)
122        self.handshake_options = handshake_options
123        self.arguments = arguments
124        self.mapping = mapping
125
126    def start(self,message):
127        if message.type == 'HANDSHAKE':
128           
129            # NOTE: We do not do set up the newState. If this fails, it comes right back to handshakes
130            if not isinstance(message.data, types.ListType):
131                return ProtocolErrorMessage('aHP')
132           
133            for var in message.data:
134                try:
135                    key, value = var
136                except ValueError:
137                    return ProtocolErrorMessage('aHP')
138           
139            if not message.data:
140                return ProtocolErrorMessage('aHP')
141
142            # Make a dictionary of options in the handshake
143
144            options = {}
145            for var in message.data:
146                key, value = var
147                if key in options: # only one key allowed
148                    raise ProtocolErrorMessage('aHP')
149               
150                options[key] = value
151
152            if 'protocol' not in options:
153                return Message('PROTOCOL_ERROR','please do a handshake')
154
155            if options['protocol'] == 'opencoin 1.0' or options['protocol'] == 'opencoin 1.0+':
156                # Set up a state where the handshake no longer is needed.
157                self.old_start = self.start
158                self.start = self.dispatch # Now, if we restart we end up in dispatch
159               
160                self.newState(self.dispatch)
161               
162                send_options = [['protocol', 'opencoin 1.0']]
163                if self.handshake_options:
164                    send_options.extend(self.handshake_options)
165                return Message('HANDSHAKE_ACCEPT', send_options)
166            else:
167                self.newState(self.goodbye)
168                return Message('HANDSHAKE_REJECT','did not like the protocol version')
169        else:
170            return Message('PROTOCOL_ERROR','please do a handshake')
171
172
173    def dispatch(self,message):       
174
175        try:
176            nextprotocol = self.mapping[message.type](self.arguments)
177        except KeyError:
178            self.newState(self.goodbye)
179            return ProtocolErrorMessage('aHP')
180
181        self.transport.setProtocol(nextprotocol)
182        m = nextprotocol.newMessage(message)
183
184
185
186
187############################### Spending coins (w2w) ##########################
188# Spoken between two wallets to transfer coins / tokens                       #
189###############################################################################
190
191class TokenSpendSender(Protocol):
192    """
193    >>> from tests import coins
194    >>> coin1 = coins[0][0]
195    >>> coin2 = coins[1][0]
196    >>> css = TokenSpendSender([coin1,coin2],'foobar')
197    >>> css.state(Message(None))
198    <Message('HANDSHAKE',[['protocol', 'opencoin 1.0']])>
199    >>> css.state(Message('HANDSHAKE_ACCEPT',None))
200    <Message('SUM_ANNOUNCE',['...', '3', 'foobar'])>
201    >>> css.state(Message('SUM_ACCEPT'))
202    <Message('SPEND_TOKEN_REQUEST',['...', [[(...)], [(...)]], 'foobar'])>
203    >>> css.state(Message('SPEND_TOKEN_ACCEPT'))
204    <Message('GOODBYE',None)>
205    >>> css.state(Message('GOODBYE'))
206
207
208    And test to make sure we can skip handshake if we need to
209    >>> css = TokenSpendSender([coin1, coin2], 'foobar', skip_handshake=True)
210    >>> css.state(Message(None))
211    <Message('SUM_ANNOUNCE',['...', '3', 'foobar'])>
212   
213    """
214    def __init__(self, coins, target, skip_handshake = False):
215
216        self.coins = coins
217        self.amount = sum(coins)
218        self.target = target
219       
220        import base64
221        from crypto import _r as Random
222        self.transaction_id = Random.getRandomString(128)
223        self.encoded_transaction_id = base64.b64encode(self.transaction_id)
224
225        Protocol.__init__(self)
226
227        if skip_handshake:
228            self.state = self.firstStep
229        else:
230            self.state = self.initiateHandshake
231
232    def firstStep(self,message):       
233        self.state = self.spendCoin
234        standard_identifier = self.coins[0].standard_identifier # All the coins are
235        currency_identifier = self.coins[0].currency_identifier # same currency & CDD
236        return Message('SUM_ANNOUNCE',[self.encoded_transaction_id,
237                                       standard_identifier,
238                                       currency_identifier,
239                                       str(self.amount),
240                                       self.target])
241
242
243    def spendCoin(self,message):
244        if message.type == 'SUM_ACCEPT':
245
246            self.state = self.conclusion           
247            jsonCoins = [c.toPython() for c in self.coins]
248            return Message('SPEND_TOKEN_REQUEST',[self.encoded_transaction_id,
249                                          jsonCoins,
250                                          self.target])
251
252        elif message.type == 'PROTOCOL_ERROR':
253            self.newState(self.goodbye)
254            pass
255
256        else:
257            self.newState(self.goodbye)
258            return ProtocolErrorMessage('TokenSpendSender')
259
260    def conclusion(self,message):
261        self.newState(self.goodbye)
262
263        if message.type == 'SPEND_TOKEN_ACCEPT':
264            return self.goodbye()
265
266        elif message.type == 'PROTOCOL_ERROR':
267            pass
268
269        else:
270            return ProtocolErrorMessage('TokenSpendSender')
271
272
273class TokenSpendRecipient(Protocol):
274    """
275    >>> import entities
276    >>> from tests import coins, CDD
277    >>> coin1 = coins[0][0].toPython() # Denomination of 1
278    >>> coin2 = coins[1][0].toPython() # Denomination of 2
279    >>> w = entities.Wallet()
280    >>> w.setCurrentCDD(CDD)
281    >>> w.makeIssuerTransport = lambda loc: None
282    >>> csr = TokenSpendRecipient(w)
283    >>> csr.state(Message('SUM_ANNOUNCE',['1234','standard', 'currency', '3','a book']))
284    <Message('SUM_ACCEPT',None)>
285    >>> csr.state(Message('SPEND_TOKEN_REQUEST',['1234', [coin1, coin2], 'a book']))
286    <Message('SPEND_TOKEN_ACCEPT',None)>
287    >>> csr.state(Message('GOODBYE',None))
288    <Message('GOODBYE',None)>
289
290    After we have received a goodbye, we don't get anything else
291    >>> csr.state(Message(None))
292
293    TODO: Add PROTOCOL_ERROR checking
294    """
295   
296    def __init__(self, wallet):
297        self.wallet = wallet
298        Protocol.__init__(self)
299
300    def start(self,message):
301        import base64
302
303        self.newState(self.goodbye) # default is to end protocol. Reset if we continue
304
305        if message.type == 'SUM_ANNOUNCE':
306            try:
307                encoded_transaction_id, standard_identifier, currency_identifier, amount, self.target = message.data
308            except ValueError:
309                return ProtocolErrorMessage('SA')
310
311            if not isinstance(encoded_transaction_id, types.StringType):
312                return ProtocolErrorMessage('SA')
313
314            if not isinstance(standard_identifier, types.StringType):
315                return ProtocolErrorMessage('SA')
316
317            if not isinstance(currency_identifier, types.StringType):
318                return ProtocolErrorMessage('SA')
319
320            if not isinstance(amount, types.StringType):
321                return ProtocolErrorMessage('SA')
322            if str(int(amount)) != amount:
323                return ProtocolErrorMessage('SA')
324
325            if not isinstance(self.target, types.StringType):
326                return ProtocolErrorMessage('SA')
327
328            # Decode the transaction_id
329            try:
330                self.transaction_id = base64.b64decode(encoded_transaction_id)
331            except TypeError:
332                return ProtocolErrorMessage('SA')
333           
334            # Setup sum
335            self.sum = int(amount)
336
337            # And do stuff
338            # FIXME: get some feedback from interface somehow
339            action = self.wallet.confirmReceiveCoins('the other wallet id', self.sum, self.target)
340
341            if action == 'reject':
342                return Message('SUM_REJECT')
343
344            else:
345                self.action = action
346                self.newState(self.handleCoins)
347                return Message('SUM_ACCEPT')
348
349        elif message.type == 'PROTOCOL_ERROR':
350            pass
351
352        else:
353            return ProtocolErrorMessage('TokenSpendRecipient')
354
355    def handleCoins(self,message):
356        import base64
357
358        self.newState(self.goodbye)
359
360        if message.type == 'SPEND_TOKEN_REQUEST':
361            try:
362                encoded_transaction_id, tokens, target = message.data
363            except ValueError:
364                return ProtocolErrorMessage('TS')
365
366            if not isinstance(encoded_transaction_id, types.StringType):
367                return ProtocolErrorMessage('TS')
368
369           
370            if not isinstance(tokens, types.ListType):
371                return ProtocolErrorMessage('TS')
372           
373            if not tokens: # We require tokens
374                return ProtocolErrorMessage('TS')
375            for token in tokens:
376                if not isinstance(token, types.ListType):
377                    return ProtocolErrorMessage('TS')
378
379            # Convert transaction_id
380            try:
381                transaction_id = base64.b64decode(encoded_transaction_id)
382            except TypeError:
383                return ProtocolErrorMessage('TS')
384
385            # Convert the tokens
386            try:
387                tokens = [containers.CurrencyCoin().fromPython(c) for c in tokens]
388            except TypeError:
389                return ProtocolErrorMessage('TS')
390            except IndexError:
391                return ProtocolErrorMessage('TS')
392
393           
394            # And now do things
395
396            #be conservative
397            result = Message('SPEND_TOKEN_REJECT','default')
398           
399            if transaction_id != self.transaction_id:
400                result = Message('SPEND_TOKEN_REJECT','Rejected')
401           
402            elif sum(tokens) != self.sum:
403                result = Message('SPEND_TOKEN_REJECT','Rejected')
404           
405            elif target != self.target:
406                result = Message('SPEND_TOKEN_REJECT','Rejected')
407           
408            elif self.action in ['redeem', 'exchange', 'trust']:
409                out = self.wallet.handleIncomingCoins(tokens, self.action, target)
410                if out:
411                    result = Message('SPEND_TOKEN_ACCEPT')
412
413            return result
414
415        elif message.type == 'PROTOCOL_ERROR':
416            pass
417
418        else:
419            return ProtocolErrorMessage('TokenSendRecipient')
420
421
422
423
424############################### Transfer tokens  ##############################
425# This is spoken between a wallet (sender) and the issuer, for minting,       #
426# exchange and redemption                                                     #
427###############################################################################
428
429class TransferTokenSender(Protocol):
430    """
431    >>> from tests import coins
432    >>> coin1 = coins[0][0] # denomination of 1
433    >>> coin2 = coins[1][0] # denomination of 2
434
435    >>> tts = TransferTokenSender('my account',[],[coin1, coin2],type='redeem')
436    >>> tts.state(Message(None))
437    <Message('HANDSHAKE',[['protocol', 'opencoin 1.0']])>
438    >>> tts.state(Message('HANDSHAKE_ACCEPT',None))
439    <Message('TRANSFER_TOKEN_REQUEST',['...', 'my account', [], [[(...)], [(...)]], [['type', 'redeem']]])>
440    >>> tts.state(Message('TRANSFER_TOKEN_ACCEPT',[tts.encoded_transaction_id, []]))
441    <Message('GOODBYE',None)>
442    >>> tts.state == tts.goodbye
443    True
444    >>> tts.state(Message('foobar'))
445
446    And now test that we can skip the handshake if we want.
447    >>> tts = TransferTokenSender('my account', [], [coin1, coin2], skip_handshake=True, type='redeem')
448    >>> tts.state(Message(None))
449    <Message('TRANSFER_TOKEN_REQUEST',['...', 'my account', [], [[(...)], [(...)]], [['type', 'redeem']]])>
450
451    """
452
453    def __init__(self, target, blinds, coins, skip_handshake=False, **kwargs):
454        import base64
455        from crypto import _r as Random
456        self.transaction_id = Random.getRandomString(128)
457        self.encoded_transaction_id = base64.b64encode(self.transaction_id)
458
459        self.target = target
460        self.blinds = blinds
461        self.coins = [c.toPython() for c in coins]
462        self.kwargs = kwargs
463
464        Protocol.__init__(self)
465
466        if skip_handshake:
467            self.state = self.firstStep
468        else:
469            self.state = self.initiateHandshake
470   
471    def firstStep(self,message):
472        data = [self.encoded_transaction_id,
473                self.target,
474                self.blinds,
475                self.coins]
476        if self.kwargs:
477            data.append([list(i) for i in self.kwargs.items()])               
478        self.state = self.conclusion
479        return Message('TRANSFER_TOKEN_REQUEST',data)
480
481    def conclusion(self,message):
482        import base64
483       
484        #no matter what, this is going to be the last state, after this
485        #its goodbye
486        self.newState(self.goodbye)
487       
488        if message.type == 'TRANSFER_TOKEN_ACCEPT':
489
490            try:
491                encoded_transaction_id, blinds = message.data
492            except ValueError:
493                return ProtocolErrorMessage('TTA')
494
495            if not isinstance(blinds, types.ListType):
496                return ProtocolErrorMessage('TTA')
497            for blind in blinds:
498                if not isinstance(blind, types.StringType):
499                    return ProtocolErrorMessage('TTA')
500       
501            # decode the blinds
502            try:
503                self.blinds = [base64.b64decode(blind) for blind in blinds]
504            except TypeError:
505                return ProtocolErrorMessage('TTA')
506
507            #decode transaction_id
508            try:
509                transaction_id = base64.b64decode(encoded_transaction_id)
510            except TypeError:
511                return ProtocolErrorMessage('TTA')
512
513            # Start checking things
514            if transaction_id != self.transaction_id:
515                #FIXME: Wrong message, I think. We don't really have a way to handle this.
516                return Message('PROTOCOL_ERROR', 'incorrect transaction_id')
517       
518
519            if self.kwargs['type'] == 'exchange' or self.kwargs['type'] == 'mint':
520                if not blinds:
521                    return ProtocolErrorMessage('TTA')
522
523            else:
524                if len(blinds) != 0:
525                    return ProtocolErrorMessage('TTA')
526               
527        elif message.type == 'TRANSFER_TOKEN_DELAY':
528            try:
529                encoded_transaction_id, reason = message.data
530            except ValueError:
531                return ProtocolErrorMessage('TTD')
532
533            if not isinstance(reason, types.StringType):
534                return ProtocolErrorMessage('TTD')
535
536            # Decode the transaction_id
537            try:
538                transaction_id = base64.b64decode(encoded_transaction_id)
539            except TypeError:
540                return ProtocolErrorMessage('TTD')
541
542            # Start checking things
543            if transaction_id != self.transaction_id:
544                #FIXME: This seems like a wrong message....
545                return ProtocolErrorMessage('TTD')
546
547
548        elif message.type == 'TRANSFER_TOKEN_REJECT':
549            try:
550                encoded_transaction_id, type, reason, reason_detail = message.data
551            except ValueError:
552                return ProtocolErrorMessage('TTRj')
553
554            if not isinstance(encoded_transaction_id, types.StringType):
555                return ProtocolErrorMessage('TTRj')
556           
557            if not isinstance(type, types.StringType):
558                return ProtocolErrorMessage('TTRj')
559            if not type:
560                return ProtocolErrorMessage('TTRj')
561
562            if not isinstance(reason, types.StringType):
563                return ProtocolErrorMessage('TTRj')
564            if not reason:
565                return ProtocolErrorMessage('TTRj')
566           
567            if not isinstance(reason_detail, types.ListType):
568                return ProtocolErrorMessage('TTRj')
569           
570
571            # Decode the transaction_id
572            try:
573                transcation_id = base64.b64decode(encoded_transaction_id)
574            except TypeError:
575                return ProtocolErrorMessage('TTRj')
576
577            # Do checking of reason_detail
578            # If reason is see dtail, reason_detail is a list with
579            # entries, otherwise, reason_detail is an empty list
580            if reason == 'See detail':
581                if not reason_detail:
582                    return ProtocolErrorMessage('TTRj')
583            else:
584                if reason_detail:
585                    return ProtocolErrorMessage('TTRj')
586           
587            # Any checking of specific reasons for validity should be also
588            # be done, but reason_detail is always empty
589
590            # Start checking things
591            if transaction_id != self.transaction_id:
592                #FIXME: I don't like using this message for this..
593                return ProtocolErrorMessage('TTRj')
594
595            # FIXME: Do something here?
596
597        elif message.type != 'PROTOCOL_ERROR':
598            return ProtocolErrorMessage('TransferTokenSender')
599
600        # Really?!?
601        return self.goodbye()
602
603
604
605class TransferTokenRecipient(Protocol):
606    """
607    >>> import entities, tests, containers, base64, copy, calendar
608    >>> ie = tests.makeIssuerEntity()
609    >>> ie.getTime = lambda: calendar.timegm((2008,01,31,0,0,0))
610    >>> ie.mint.getTime = ie.issuer.getTime = ie.getTime
611
612    >>> ttr = TransferTokenRecipient(ie.issuer)
613    >>> coin1 = tests.coins[0][0].toPython() # denomination of 1
614    >>> coin2 = tests.coins[1][0].toPython() # denomination of 2
615   
616    >>> coin3 = tests.coins[0][0].toPython() # denomination of 1
617
618    This should not be accepted
619    >>> ttr.state(Message('TRANSFER_TOKEN_REQUEST',['1234', 'my account', [], ['foobar'], [['type', 'redeem']]]))   
620    <Message('PROTOCOL_ERROR','send again...')>
621
622    This should also not be accepted - no coins but redeem
623    >>> ttr.state = ttr.start
624
625    #FIXME: disabled for now. Figure out correct error and implement
626    >>> #ttr.state(Message('TRANSFER_TOKEN_REQUEST',['1234', 'my account', [], [], [['type', 'redeem']]]))
627    <Message('TRANSFER_TOKEN_REJECT',['Token', 'Rejected', []])>
628   
629    The malformed coin should be rejected
630    >>> malformed = copy.deepcopy(tests.coins[0][0])
631    >>> malformed.signature = 'Not a valid signature'
632    >>> ttr.state = ttr.start
633    >>> ttr.state(Message('TRANSFER_TOKEN_REQUEST',['0000', 'my account', [], [malformed.toPython()], [['type', 'redeem']]]))
634    <Message('TRANSFER_TOKEN_REJECT',['0000', 'Token', 'See detail', ['Invalid token']])>
635    >>> ttr.newState(ttr.start)
636    >>> ttr.state(Message('TRANSFER_TOKEN_RESUME', '0000'))
637    <Message('TRANSFER_TOKEN_REJECT',['0000', 'Token', 'See detail', ['Invalid token']])>
638
639    The unknown key_identifier should be rejected
640    >>> malformed = copy.deepcopy(tests.coins[0][0])
641    >>> malformed.key_identifier = 'Not a valid key identifier'
642    >>> ttr.state = ttr.start
643    >>> ttr.state(Message('TRANSFER_TOKEN_REQUEST',['1111', 'my account', [], [malformed.toPython()], [['type', 'redeem']]]))
644    <Message('TRANSFER_TOKEN_REJECT',['1111', 'Token', 'See detail', ['Invalid key_identifier']])>
645    >>> ttr.newState(ttr.start)
646    >>> ttr.state(Message('TRANSFER_TOKEN_RESUME', '1111'))
647    <Message('TRANSFER_TOKEN_REJECT',['1111', 'Token', 'See detail', ['Invalid key_identifier']])>
648
649    >>> ttr.state = ttr.start
650    >>> ttr.state(Message('TRANSFER_TOKEN_REQUEST',['2222', 'my account', [], [coin1, coin2], [['type', 'redeem']]]))
651    <Message('TRANSFER_TOKEN_ACCEPT',['2222', []])>
652    >>> ttr.newState(ttr.start)
653    >>> ttr.state(Message('TRANSFER_TOKEN_RESUME', '2222'))
654    <Message('TRANSFER_TOKEN_ACCEPT',['2222', []])>
655
656    Try to double spend. Should not work.
657    >>> ttr.state = ttr.start
658    >>> ttr.state(Message('TRANSFER_TOKEN_REQUEST',['3333', 'my account', [], [coin1, coin2], [['type', 'redeem']]]))
659    <Message('TRANSFER_TOKEN_REJECT',['3333', 'Token', 'See detail', ['Token already spent', 'Token already spent']])>
660    >>> ttr.newState(ttr.start)
661    >>> ttr.state(Message('TRANSFER_TOKEN_RESUME', '3333'))
662    <Message('TRANSFER_TOKEN_REJECT',['3333', 'Token', 'See detail', ['Token already spent', 'Token already spent']])>
663
664    >>> blank1 = containers.CurrencyBlank().fromPython(tests.coinA.toPython(nosig=1))
665    >>> blank2 = containers.CurrencyBlank().fromPython(tests.coinB.toPython(nosig=1))
666    >>> blind1 = base64.b64encode(blank1.blind_blank(tests.CDD,tests.mint_key1, blind_factor='a'*26))
667    >>> blind2 = base64.b64encode(blank2.blind_blank(tests.CDD,tests.mint_key2, blind_factor='a'*26))
668    >>> blindslist = [[tests.mint_key1.encodeField('key_identifier'),[blind1]],
669    ...               [tests.mint_key2.encodeField('key_identifier'),[blind2]]]
670
671    >>> ttr.state = ttr.start
672    >>> ttr.state(Message('TRANSFER_TOKEN_REQUEST',['4444', 'my account', blindslist, [], [['type', 'mint']]]))
673    <Message('TRANSFER_TOKEN_ACCEPT',['4444', ['Do0el3uxdyFMF8NdXtowBLBOxXM0r7xR9hXkaZWEhPUBQCe8yaYGO09wnxrWEVFlt0r9M6bCZxKtzNGDGw3/XQ==', 'dTnL8yTkdelG9fW//ZoKzUl7LTjBXiElaHkfyMLgVetEM7pmEzfcdfRWhm2PP3IhnkZ8CmAR1uOJ99rJ+XBASA==']])>
674    >>> ttr.newState(ttr.start)
675    >>> ttr.state(Message('TRANSFER_TOKEN_RESUME', '4444'))
676    <Message('TRANSFER_TOKEN_ACCEPT',['4444', ['Do0el3uxdyFMF8NdXtowBLBOxXM0r7xR9hXkaZWEhPUBQCe8yaYGO09wnxrWEVFlt0r9M6bCZxKtzNGDGw3/XQ==', 'dTnL8yTkdelG9fW//ZoKzUl7LTjBXiElaHkfyMLgVetEM7pmEzfcdfRWhm2PP3IhnkZ8CmAR1uOJ99rJ+XBASA==']])>
677
678    >>> ttr.state == ttr.goodbye
679    True
680
681    >>> ttr.state(Message('GOODBYE'))
682    <Message('GOODBYE',None)>
683
684
685    Now, check to make sure the implementation is good
686    >>> ttr.state = ttr.start
687    >>> ttr.done = 0
688    >>> ttr.state(Message('TRANSFER_TOKEN_REQUEST'))
689    <Message('PROTOCOL_ERROR','send again...')>
690
691    Okay. Have to reset DSDB to do this next trick
692    >>> ie = tests.makeIssuerEntity()
693    >>> ie.getTime = lambda: calendar.timegm((2008,01,31,0,0,0))
694    >>> ie.mint.getTime = ie.issuer.getTime = ie.getTime
695    >>> performMinting, ie.mint.performMinting = ie.mint.performMinting, lambda: None
696    >>> blank = tests.makeBlank(tests.mintKeys[0], 'a'*26, 'a'*26)
697    >>> blind = [[tests.mintKeys[0].encodeField('key_identifier'), [base64.b64encode(blank.blind_blank(tests.CDD, tests.mintKeys[0]))]]]
698    >>> ttr = TransferTokenRecipient(ie.issuer)
699    >>> ttr.newState(ttr.start)
700    >>> ttr.state(Message('TRANSFER_TOKEN_REQUEST',['1234', '', blind, [coin1], [['type', 'exchange']]]))
701    <Message('TRANSFER_TOKEN_DELAY',['1234', '0'])>
702
703    Test again since it hits a different codepath
704    >>> ttr.newState(ttr.start)
705    >>> ttr.state(Message('TRANSFER_TOKEN_RESUME','1234'))
706    <Message('TRANSFER_TOKEN_DELAY',['1234', '0'])>
707
708    Okay. Mint them and test the resume
709    >>> performMinting()
710    >>> ttr.newState(ttr.start)
711    >>> ttr.state(Message('TRANSFER_TOKEN_RESUME','1234'))
712    <Message('TRANSFER_TOKEN_ACCEPT',['1234', ['UIo2KtqK/6JqSWbtFFVR14fOjnzwr4tDiY/6kOnQ0h92EewY2vJBV2XaS43wK3RsNFg0sHzNh3v2BVDFV8cDvQ==']])>
713
714    >>> ttr.newState(ttr.start)
715    >>> ttr.state(Message('TRANSFER_TOKEN_RESUME', '0000'))
716    <Message('TRANSFER_TOKEN_REJECT',['0000', 'Generic', 'Unknown transaction_id', ()])>
717
718    """
719
720    def __init__(self,issuer):
721        self.issuer = issuer
722        Protocol.__init__(self)
723
724    def start(self,message):
725        from entities import LockingError
726        import base64
727
728        self.newState(self.goodbye) # No matter what, we have one shot
729       
730        if message.type == 'TRANSFER_TOKEN_REQUEST':
731            try:
732                encoded_transaction_id, target, blindslist, coins, options_list = message.data
733            except TypeError:
734                return ProtocolErrorMessage('TTRq')
735
736            if not isinstance(target, types.StringType):
737                return ProtocolErrorMessage('TTRq')
738
739            if not isinstance(blindslist, types.ListType):
740                return ProtocolErrorMessage('TTRq')
741
742            for blind in blindslist:
743           
744                if not isinstance(blind, types.ListType):
745                    return ProtocolErrorMessage('TTRq')
746                try:
747                    key, b = blind
748                except ValueError:
749                    return ProtocolErrorMessage('TTRq')
750               
751                if not isinstance(key, types.StringType):
752                    return ProtocolErrorMessage('TTRq')
753               
754                if not isinstance(b, types.ListType):
755                    return ProtocolErrorMessage('TTRq')
756               
757                for blindstring in b:
758                    if not isinstance(blindstring, types.StringType):
759                        return ProtocolErrorMessage('TTRq')
760                if len(b) == 0:
761                    return ProtocolErrorMessage('TTRq')
762
763            # Decode transaction_id
764            try:
765                transaction_id = base64.b64decode(encoded_transaction_id)
766            except TypeError:
767                return ProtocolErrorMessage('TTRq')
768
769            # Convert blindslist
770            try:
771                blindslist = [[base64.b64decode(key), [base64.b64decode(bl) for bl in blinds]] for key, blinds in blindslist]
772            except TypeError:
773                return ProtocolErrorMessage('TTRq')
774
775            #convert coins
776            if not isinstance(coins, types.ListType):
777                return ProtocolErrorMessage('TTRq')
778           
779            for coin in coins:
780                if not isinstance(coin, types.ListType):
781                    return ProtocolErrorMessage('TTRq')
782
783            try:
784                coins = [containers.CurrencyCoin().fromPython(c) for c in coins]
785            except TypeError:
786                return ProtocolErrorMessage('TTRq')
787            except IndexError:
788                return ProtocolErrorMessage('TTRq')
789
790            if not isinstance(options_list, types.ListType):
791                return ProtocolErrorMessage('TTRq')
792           
793            # check options (why isn't this higher?)
794            for options in options_list:
795                try:
796                    key, val = options
797                except ValueError:
798                    return ProtocolErrorMessage('TTRq')
799           
800                if not isinstance(key, types.StringType):
801                    return ProtocolErrorMessage('TTRq')
802               
803                if not isinstance(val, types.StringType):
804                    return ProtocolErrorMessage('TTRq')
805
806            # Decipher options
807            # FIXME: check to make sure we don't have the same key twice
808            options = dict(options_list)
809
810            # And have the IS to all the work.
811            response, additional = self.issuer.transferTokenRequestHelper(transaction_id, target,
812                                                                    blindslist, coins, options)
813            if response == 'ACCEPT':
814                signed_blinds = additional
815                return Message('TRANSFER_TOKEN_ACCEPT', [encoded_transaction_id, signed_blinds])
816            elif response == 'REJECT':
817                failures = additional
818                type, reason, reason_detail = failures
819                return Message('TRANSFER_TOKEN_REJECT', [encoded_transaction_id, type, reason, reason_detail])
820            elif response == 'DELAY':
821                time = additional
822                return Message('TRANSFER_TOKEN_DELAY', [encoded_transaction_id, time])
823            else:
824                raise NotImplementedError('Got an impossible response')
825
826        elif message.type == 'TRANSFER_TOKEN_RESUME':
827            encoded_transaction_id = message.data
828
829            if not isinstance(encoded_transaction_id, types.StringType):
830                return ProtocolErrorMessage('TTRs')
831
832            # Decode transaction_id
833            try:
834                transaction_id = base64.b64decode(encoded_transaction_id)
835            except TypeError:
836                return ProtocolErrorMessage('TTRs')
837
838            response, additional = self.issuer.resumeTransaction(transaction_id)
839            if response == 'PASS':
840                signed_blinds = additional
841                return Message('TRANSFER_TOKEN_ACCEPT', [encoded_transaction_id, signed_blinds])
842            elif response == 'REJECT':
843                failures = additional
844                type, reason, reason_detail = failures
845                return Message('TRANSFER_TOKEN_REJECT', [encoded_transaction_id, type, reason, reason_detail])
846            elif response == 'DELAY':
847                time = additional
848                return Message('TRANSFER_TOKEN_DELAY', [encoded_transaction_id, time])
849            else:
850                raise NotImplementedError('Got an impossible response')
851
852        elif message.type == 'PROTOCOL_ERROR':
853            #FIXME: actually do something for a PROTOCOL_ERROR
854            pass
855
856        else:
857            return ProtocolErrorMessage('TransferTokenRecipient')
858
859
860
861
862############################### Mint key exchange #############################
863#Between a wallet and the IS, to get the mint key                             #
864###############################################################################
865
866class fetchMintKeyProtocol(Protocol):
867    """Used by a wallet to fetch the mints keys, needed when creating blanks
868       
869    Lets fetch by denomination
870
871    >>> fmp = fetchMintKeyProtocol(denominations=['1'])
872    >>> fmp.state(Message(None))
873    <Message('HANDSHAKE',[['protocol', 'opencoin 1.0']])>
874    >>> fmp.state(Message('HANDSHAKE_ACCEPT',None))
875    <Message('MINT_KEY_FETCH_DENOMINATION',[['1'], '0'])>
876
877    >>> from tests import mintKeys
878    >>> mintKey = mintKeys[0]
879   
880    >>> fmp.state(Message('MINT_KEY_PASS',[mintKey.toPython()]))
881    <Message('GOODBYE',None)>
882
883    >>> fmp.state == fmp.goodbye
884    True
885    >>> fmp.state(Message('GOODBYE'))
886
887    And now by keyid
888
889    >>> fmp = fetchMintKeyProtocol(keyids=['sj17RxE1hfO06+oTgBs9Z7xLut/3NN+nHJbXSJYTks0='])
890    >>> fmp.state(Message(None))
891    <Message('HANDSHAKE',[['protocol', 'opencoin 1.0']])>
892    >>> fmp.state(Message('HANDSHAKE_ACCEPT'))
893    <Message('MINT_KEY_FETCH_KEYID',['sj17RxE1hfO06+oTgBs9Z7xLut/3NN+nHJbXSJYTks0='])>
894
895    >>> fmp.state(Message('MINT_KEY_PASS',[mintKey.toPython()]))
896    <Message('GOODBYE',None)>
897
898    >>> fmp.state == fmp.goodbye
899    True
900
901
902    Lets have some problems a failures (we set the state
903    to getKey to reuse the fmp object and save a couple
904    of lines)
905
906    >>> fmp.newState(fmp.getKey)
907    >>> fmp.done = 0
908    >>> fmp.state(Message('MINT_KEY_FAILURE',[['RxE1', 'Unknown key_identifier']]))
909    <Message('GOODBYE',None)>
910    >>> fmp.state == fmp.goodbye
911    True
912    >>> fmp.state(Message('GOODBYE'))
913
914    Now lets break something
915   
916    >>> fmp.newState(fmp.getKey)
917    >>> fmp.state(Message('FOOBAR'))
918    <Message('PROTOCOL_ERROR','send again...')>
919
920    Okay. Now we'll test every possible MINT_KEY_PASS.
921    The correct argument is a list of coins. Try things to
922    break it.
923   
924    >>> fmp.newState(fmp.getKey)
925    >>> fmp.state(Message('MINT_KEY_PASS', [['foo']]))
926    <Message('PROTOCOL_ERROR','send again...')>
927
928    >>> fmp.newState(fmp.getKey)
929    >>> fmp.state(Message('MINT_KEY_PASS', ['foo']))
930    <Message('PROTOCOL_ERROR','send again...')>
931
932    >>> fmp.newState(fmp.getKey)
933    >>> fmp.state(Message('MINT_KEY_PASS', 'foo'))
934    <Message('PROTOCOL_ERROR','send again...')>
935
936    >>> fmp.newState(fmp.getKey)
937    >>> fmp.state(Message('MINT_KEY_PASS', []))
938    <Message('PROTOCOL_ERROR','send again...')>
939
940    Now try every possible bad MINT_KEY_FAILURE.
941    Note: it may make sense to verify we have tood reasons
942    as well.
943
944    We need to make sure we are setup as handling keyids
945    >>> fmp.keyids and not fmp.denominations
946    True
947
948    Check base64 decoding causes failure
949    >>> fmp.newState(fmp.getKey)
950    >>> fmp.state(Message('MINT_KEY_FAILURE', [[1, '']]))
951    <Message('PROTOCOL_ERROR','send again...')>
952
953    And the normal tests
954    >>> fmp.newState(fmp.getKey)
955    >>> fmp.state(Message('MINT_KEY_FAILURE', [[]]))
956    <Message('PROTOCOL_ERROR','send again...')>
957   
958    Okay. Check the denomination branch now
959   
960    >>> fmp.denominations = ['1']
961    >>> fmp.keyids = None
962
963    Make sure we are in the denomination branch
964    >>> fmp.newState(fmp.getKey)
965    >>> fmp.state(Message('MINT_KEY_FAILURE', [['1', '']]))
966
967    >>> fmp.state == fmp.goodbye
968    True
969   
970    Do a check
971
972    >>> fmp.newState(fmp.getKey)
973    >>> fmp.state(Message('MINT_KEY_FAILURE', [[]]))
974    <Message('PROTOCOL_ERROR','send again...')>
975   
976    And now test that we can skip handshake if we want
977    >>> fmp = fetchMintKeyProtocol(denominations=['1'], skip_handshake=True)
978    >>> fmp.state(Message(None))
979    <Message('MINT_KEY_FETCH_DENOMINATION',[['1'], '0'])>
980
981    """
982
983    def __init__(self, denominations=None, keyids=None, time=None, skip_handshake=False):
984       
985        self.denominations = denominations
986        self.keyids = keyids
987        self.keycerts = []
988
989        if not time: # The encoded value of time
990            self.encoded_time = '0'
991        else:
992            self.encoded_time = containers.encodeTime(time)
993
994        Protocol.__init__(self)
995
996        if skip_handshake:
997            self.newState(self.firstStep)
998        else:
999            self.newState(self.initiateHandshake)
1000
1001    def firstStep(self,message):
1002        """Completes handshake, asks for the minting keys """
1003       
1004        if self.denominations:
1005            self.newState(self.getKey)
1006            return Message('MINT_KEY_FETCH_DENOMINATION',[self.denominations, self.encoded_time])
1007        elif self.keyids:
1008            self.newState(self.getKey)
1009            return Message('MINT_KEY_FETCH_KEYID',self.keyids)
1010
1011
1012    def getKey(self,message):
1013        """Gets the actual key"""
1014
1015        self.newState(self.goodbye)
1016
1017        if message.type == 'MINT_KEY_PASS':
1018
1019            if not isinstance(message.data, types.ListType):
1020                return ProtocolErrorMessage('MKP')
1021           
1022            if len(message.data) == 0: # Nothing in the message
1023                return ProtocolErrorMessage('MKP')
1024
1025            for key in message.data:
1026                if not isinstance(message.data, types.ListType):
1027                    return ProtocolErrorMessage('MKP')
1028
1029            try:
1030                keys = [containers.MintKey().fromPython(key) for key in message.data]
1031            except TypeError:
1032                return ProtocolErrorMessage('MKP')
1033            except IndexError:
1034                return ProtocolErrorMessage('MKP')
1035
1036            #TODO: Check to make sure we got the keys we asked for, probably?
1037
1038            # Note: keycerts stores the value of the MintKeys. They get checked by the
1039            # wallet explicitly
1040            self.keycerts.extend(keys)
1041           
1042               
1043
1044        elif message.type == 'MINT_KEY_FAILURE':
1045            reasons = message.data
1046
1047            if not isinstance(reasons, types.ListType):
1048                return ProtocolErrorMessage('MKF')
1049            if not reasons:
1050                return ProtocolErrorMessage('MKF')
1051
1052            for reasonlist in reasons:
1053                if not isinstance(reasonlist, types.ListType):
1054                    return ProtocolErrormessage('MKF')
1055
1056                try:
1057                    key, rea = reasonlist
1058                except ValueError:
1059                    return ProtocolErrorMessage('MKF')
1060
1061                if not isinstance(key, types.StringType):
1062                    return ProtocolErrorMessage('MKF')
1063                if not isinstance(rea, types.StringType):
1064                    return ProtocolErrorMessage('MKF')
1065
1066                # Do not do any conversions of keyid/denomination at this time. Have
1067                # to wait to do it after we know which set we have
1068
1069            self.reasons = []
1070            if self.denominations: # Was a denomination search
1071                for reasonlist in message.data:
1072                    denomination, reason = reasonlist
1073                       
1074                    #FIXME: Should we make sure valid reason?
1075                    #FIXME: Did we even ask for this denomination?
1076                    self.reasons.append((denomination, reason))
1077
1078            else: # Was a key_identifier search
1079                import base64
1080                for reasonlist in message.data:
1081                    key, reason = reasonlist
1082                       
1083                    #FIXME: Should we make sure valid reason?
1084                    #FIXME: Did we even ask for this denomination
1085                    # Note: Explicit b64decode here
1086                    try:
1087                        self.reasons.append((base64.b64decode(key), reason))
1088                    except TypeError:
1089                        return ProtocolErrorMessage('MKF')
1090
1091       
1092        elif message.type != 'PROTOCOL_ERROR':
1093            return ProtocolErrorMessage('fetchMintKeyProtocol')
1094
1095        # FIXME: Really?!?
1096        return self.goodbye(message)           
1097
1098
1099
1100class giveMintKeyProtocol(Protocol):
1101    """An issuer hands out a key. The other side of fetchMintKeyProtocol.
1102    >>> from entities import IssuerEntity
1103    >>> ie = IssuerEntity()
1104    >>> ie.createMasterKey(keylength=512)
1105    >>> ie.makeCDD(currency_identifier='http://opencent.net/OpenCent2', denominations=['1', '2', '5'],
1106    ...            short_currency_identifier='OC', options=[['version', '0']],
1107    ...            issuer_service_location='here')
1108    >>> ie.issuer.setCurrentCDDVersion('0')
1109    >>> ie.issuer.getTime = lambda: 750
1110    >>> now = 500; later = 1000; much_later = 2000; much_much_later = 3000
1111    >>> pub1 = ie.createSignedMintKey('1', now, later, much_later)
1112    >>> pub2 = ie.createSignedMintKey('1', later, much_later, much_much_later)
1113    >>> pub3 = ie.createSignedMintKey('5', now, later, much_later)
1114    >>> gmp = giveMintKeyProtocol(ie.issuer)
1115   
1116    >>> gmp.state(Message('MINT_KEY_FETCH_DENOMINATION',[['1'], '0']))
1117    <Message('MINT_KEY_PASS',[[('key_identifier'...('denomination', '1'), ('not_before', '1970-01-01T00:08:20Z')...')]]]])>
1118
1119    >>> ie.issuer.getTime = lambda: 1500
1120    >>> gmp.newState(gmp.start)
1121    >>> gmp.state(Message('MINT_KEY_FETCH_DENOMINATION',[['1'], '0']))
1122    <Message('MINT_KEY_PASS',[[('key_identifier'...('denomination', '1'), ('not_before', '1970-01-01T00:16:40Z')...')]]]])>
1123
1124    >>> ie.issuer.getTime = lambda: 1000
1125    >>> gmp.newState(gmp.start)
1126    >>> gmp.state(Message('MINT_KEY_FETCH_DENOMINATION',[['1'], '0']))
1127    <Message('MINT_KEY_PASS',[[('key_identifier'...('denomination', '1'), ('not_before', '1970-01-01T00:08:20Z')...')]]], [('key_identifier'...('denomination', '1'), ('not_before', '1970-01-01T00:16:40Z')...')]]]])>
1128
1129    Get two keys valid at time 750
1130    >>> ie.issuer.getTime = lambda: 750
1131    >>> gmp.newState(gmp.start)
1132    >>> gmp.state(Message('MINT_KEY_FETCH_DENOMINATION',[['1', '5'], '0']))
1133    <Message('MINT_KEY_PASS',[[('key_identifier'...('denomination', '1'), ('not_before', '1970-01-01T00:08:20Z')...')]]], [('key_identifier'...('denomination', '5'), ('not_before', '1970-01-01T00:08:20Z')...')]]]])>
1134
1135    Try to get keys from two denominations, but we only have one.
1136    >>> ie.issuer.getTime = lambda: 1500
1137    >>> gmp.newState(gmp.start)
1138    >>> gmp.state(Message('MINT_KEY_FETCH_DENOMINATION',[['1', '5'], '0']))
1139    <Message('MINT_KEY_FAILURE',[['5', 'Unknown denomination']])>
1140
1141    >>> gmp.newState(gmp.start)
1142    >>> gmp.state(Message('MINT_KEY_FETCH_KEYID',[pub1.encodeField('key_identifier')]))
1143    <Message('MINT_KEY_PASS',[...]])>
1144
1145    >>> gmp.newState(gmp.start)
1146    >>> gmp.state(Message('MINT_KEY_FETCH_DENOMINATION',[['2'], '0']))
1147    <Message('MINT_KEY_FAILURE',[['2', 'Unknown denomination']])>
1148   
1149
1150    >>> gmp.newState(gmp.start)
1151    >>> gmp.state(Message('MINT_KEY_FETCH_KEYID',['NonExistantIDxxx']))
1152    <Message('MINT_KEY_FAILURE',[['NonExistantIDxxx', 'Unknown key_identifier']])>
1153
1154    >>> gmp.newState(gmp.start)
1155    >>> gmp.state(Message('bla','blub'))
1156    <Message('PROTOCOL_ERROR','send again...')>
1157
1158    """
1159
1160    def __init__(self,issuer):
1161       
1162        self.issuer = issuer
1163        Protocol.__init__(self)
1164
1165
1166    def start(self,message):
1167        from entities import KeyFetchError
1168
1169        self.newState(self.goodbye)
1170
1171        errors = []
1172        keys = []
1173
1174        if message.type == 'MINT_KEY_FETCH_DENOMINATION':
1175            try:
1176                denominations, time = message.data
1177            except ValueError: # catch tuple unpack errors
1178                return ProtocolErrorMessage('MKFD')
1179
1180            if not isinstance(denominations, types.ListType):
1181                return ProtocolErrorMessage('MKFD')
1182            if not denominations: # no denominations sent
1183                return ProtocolErrorMessage('MKFD')
1184            for denomination in denominations:
1185                if not isinstance(denomination, types.StringType):
1186                    return ProtocolErrorMessage('MKFD')
1187
1188            if not isinstance(time, types.StringType):
1189                return ProtocolErrorMessage('MKFD')
1190
1191            if time == '0':
1192                time = self.issuer.getTime()
1193            else:
1194                try:
1195                    time = containers.decodeTime(time)
1196                except ValueError:
1197                    return ProtocolErrorMessage('MKFD')
1198               
1199            for denomination in denominations:
1200                try:
1201                    key = self.issuer.getKeyByDenomination(denomination, time)           
1202                    keys.append(key)
1203                except KeyFetchError:
1204                    errors.append([denomination, 'Unknown denomination'])
1205
1206            if not errors: # flatten the keys
1207                flat = []
1208                for denomination in keys:
1209                    flat.extend(denomination)
1210                keys = flat
1211       
1212        elif message.type == 'MINT_KEY_FETCH_KEYID':               
1213
1214            import base64
1215
1216            encoded_keyids = message.data
1217           
1218            if not isinstance(encoded_keyids, types.ListType):
1219                return ProtocolErrorMessage('MKFK1')
1220            if not encoded_keyids:
1221                return ProtocolErrorMessage('MKFK2')
1222            for encoded_keyid in encoded_keyids:
1223                if not isinstance(encoded_keyid, types.StringType):
1224                    return ProtocolErrorMessage('MKFK3')
1225           
1226            # Decode keyids
1227            try:
1228                keyids = [base64.b64decode(keyid) for keyid in encoded_keyids]
1229            except TypeError:
1230                return ProtocolErrorMessage('MKFK4')
1231
1232            for keyid in keyids:
1233                try:
1234                    key = self.issuer.getKeyById(keyid)
1235                    keys.append(key)
1236                except KeyFetchError:               
1237                    errors.append([base64.b64encode(keyid), 'Unknown key_identifier'])
1238       
1239        else:
1240            return ProtocolErrorMessage('MKFK5')
1241
1242        if not errors:           
1243            return Message('MINT_KEY_PASS',[key.toPython() for key in keys])
1244        else:
1245            return Message('MINT_KEY_FAILURE',errors)
1246
1247############################## CDD Exchange with IS ###########################
1248#Between a wallet and the IS, to get a specific CDD                           #
1249###############################################################################
1250
1251class requestCDDProtocol(Protocol):
1252    """Used by a wallet to fetch new CDDs from the IS
1253       
1254    >>> rcp = requestCDDProtocol('0')
1255    >>> rcp.state(Message(None))
1256    <Message('HANDSHAKE',[['protocol', 'opencoin 1.0']])>
1257    >>> rcp.state(Message('HANDSHAKE_ACCEPT'))
1258    <Message('FETCH_CDD_REQUEST','0')>
1259
1260    >>> from tests import CDD
1261    >>> rcp.state(Message('CDD_PASS', CDD.toPython()))
1262    <Message('GOODBYE',None)>
1263
1264    >>> rcp.state == rcp.goodbye
1265    True
1266    >>> rcp.state(Message('GOODBYE'))
1267   
1268    """
1269
1270    def __init__(self, cdd_version, skip_handshake=False):
1271       
1272        self.cdd_version = cdd_version
1273
1274        Protocol.__init__(self)
1275
1276        if skip_handshake:
1277            self.newState(self.firstStep)
1278        else:
1279            self.newState(self.initiateHandshake)
1280
1281    def firstStep(self, message):
1282
1283        self.newState(self.getResponse)
1284        return Message('FETCH_CDD_REQUEST', self.cdd_version)
1285
1286    def getResponse(self, message):
1287
1288        self.newState(self.goodbye)
1289       
1290        if message.type == 'CDD_PASS':
1291       
1292            raw_cdd = message.data
1293
1294            if not isinstance(raw_cdd, types.ListType):
1295                return ProtocolErrorMessage('FCR')
1296
1297            try:
1298                cdd = containers.CDD().fromPython(raw_cdd)
1299            except TypeError:
1300                return ProtocolErrorMessage('FCR')
1301            except IndexErro:
1302                return ProtocolErrorMessage('FCR')
1303
1304            if not cdd.verify_self():
1305                # FIXME: What should we do here?
1306                return ProtocolErrorMessage('FCR')
1307
1308            if dict(cdd.options)['version'] != self.cdd_version:
1309                # FIXME: actually do something
1310                pass
1311
1312            # FIXME: We should ensure that we have proper follow through
1313            # so a IS that gets compromised cannot change the issuer_master_public_key.
1314            # The way that makes sense to do that is to work off of an option of the
1315            # previous CDD
1316           
1317            # Example using previously published
1318            # if cdd.issuer_master_public_key != prev_ver.issuer_public_master_key:
1319            #     if next_issuer_public_master_key not in prev_ver.options:
1320            #         This key is invalid
1321            #     else:
1322            #         if (dict(prev_ver.options)[next_issuer_public_master_key] ==
1323            #            cdd.encode('issuer_public_master.key')):
1324            #             This key is valid
1325            #         else:
1326            #             This key is invalid
1327
1328            # FIXME: Actually do something with the key
1329
1330        elif message.type == 'FETCH_CDD_FAILURE':
1331
1332            if not isinstance(message.data, types.NoneType):
1333                return ProtocolErrorMessage('FCF')
1334
1335            # FIXME: Do something?
1336
1337        elif message.type != 'PROTOCOL_ERROR':
1338            return ProtocolErrorMessage('fCP')
1339
1340        else:
1341            return ProtocolErrorMessage('fCP')
1342
1343        # Really?!?
1344        return self.goodbye()
1345
1346
1347class giveCDDProtocol(Protocol):
1348    """An issuer hands out a CDD. The other side of fetchCDDProtocol.
1349    >>> from entities import IssuerEntity
1350    >>> ie = IssuerEntity()
1351    >>> ie.createMasterKey(keylength=512)
1352    >>> ie.makeCDD(currency_identifier='http://opencent.net/OpenCent2', denominations=['1', '2'],
1353    ...            short_currency_identifier='OC', options=[['version', '1']], issuer_service_location='here')
1354    >>> ie.issuer.setCurrentCDDVersion('1')
1355    >>> gcp = giveCDDProtocol(ie.issuer)
1356   
1357    >>> gcp.state(Message('FETCH_CDD_REQUEST','1'))
1358    <Message('FETCH_CDD_PASS',[(...['version', '1']...)]]])>
1359
1360    >>> gcp.newState(gcp.start)
1361    >>> gcp.state(Message('FETCH_CDD_REQUEST','0'))
1362    <Message('FETCH_CDD_FAILURE',None)>
1363
1364    >>> gcp.newState(gcp.start)
1365    >>> gcp.state(Message('FETCH_CDD_REQUEST', ['1']))
1366    <Message('PROTOCOL_ERROR','send again...')>
1367
1368    >>> gcp.newState(gcp.start)
1369    >>> gcp.state(Message('foo',None))
1370    <Message('PROTOCOL_ERROR','send again...')>
1371    """
1372
1373    def __init__(self, issuer):
1374
1375        self.issuer = issuer
1376        Protocol.__init__(self)
1377
1378    def start(self,message):
1379
1380        self.newState(self.goodbye)
1381
1382        if message.type == 'FETCH_CDD_REQUEST':
1383            version = message.data
1384
1385            if not isinstance(version, types.StringType):
1386                return ProtocolErrorMessage('FCD')
1387
1388            if version not in self.issuer.cdds:
1389                return Message('FETCH_CDD_FAILURE')
1390
1391            return Message('FETCH_CDD_PASS', self.issuer.cdds[version].toPython())
1392
1393        elif message.type == 'PROTOCOL_ERROR':
1394            pass
1395
1396        else:
1397            return ProtocolErrorMessage('gCP')
1398           
1399
1400
1401############################### For testing ########################################
1402
1403class WalletSenderProtocol(Protocol):
1404    """
1405    This is just a fake protocol, just showing how it works
1406
1407    >>> sp = WalletSenderProtocol(None)
1408   
1409    It starts with sending some money
1410    >>> sp.state(Message(None))
1411    <Message('sendMoney',[1, 2])>
1412   
1413    >>> sp.state(Message('Foo'))
1414    <Message('PROTOCOL_ERROR','send again...')>
1415
1416    Lets give it a receipt
1417    >>> sp.newState(sp.waitForReceipt)
1418    >>> sp.state(Message('Receipt'))
1419    <Message('GOODBYE',None)>
1420
1421    >>> sp.state(Message('GOODBYE'))
1422
1423    """
1424
1425    def __init__(self,wallet):
1426        'we would need a wallet for this to work'
1427
1428        self.wallet = wallet
1429        Protocol.__init__(self)
1430
1431    def start(self,message):
1432        'always set the new state before returning'
1433       
1434        self.newState(self.waitForReceipt)
1435
1436        return Message('sendMoney',[1,2])
1437
1438    def waitForReceipt(self,message):
1439        'after sending we need a receipt'
1440        self.newState(self.goodbye)
1441
1442        if message.type == 'Receipt':
1443            return self.goodbye()
1444        else:
1445            return ProtocolErrorMessage('WalletProtocol')
1446
1447class WalletRecipientProtocol(Protocol):
1448
1449    def __init__(self,wallet=None):
1450        self.wallet = wallet
1451        Protocol.__init__(self)
1452
1453    def start(self,message):
1454        self.newState(self.goodbye)
1455       
1456        if message.type == 'sendMoney':
1457            if self.wallet:
1458                self.wallet.coins.extend(message.data)
1459            return Message('Receipt')
1460        else:
1461            return ProtocolErrorMessage('WalletProtocol')
1462
1463
1464if __name__ == "__main__":
1465    import doctest,sys
1466    if len(sys.argv) > 1 and sys.argv[-1] != '-v':
1467        name = sys.argv[-1]
1468        gb = globals()
1469        verbose = '-v' in sys.argv
1470        doctest.run_docstring_examples(gb[name],gb,verbose,name)
1471    else:       
1472        doctest.testmod(optionflags=doctest.ELLIPSIS)
Note: See TracBrowser for help on using the browser.