| 1 | import protocols |
|---|
| 2 | from messages import Message |
|---|
| 3 | |
|---|
| 4 | class 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 ################################ |
|---|
| 24 | def getTime(): |
|---|
| 25 | import time |
|---|
| 26 | return time.time() |
|---|
| 27 | |
|---|
| 28 | #################### Wallet ############################### |
|---|
| 29 | |
|---|
| 30 | class 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 | |
|---|
| 506 | class UnableToDoError(Exception): |
|---|
| 507 | pass |
|---|
| 508 | |
|---|
| 509 | |
|---|
| 510 | #################### Issuer ############################### |
|---|
| 511 | |
|---|
| 512 | class 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 | |
|---|
| 614 | class 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 | |
|---|
| 1323 | class KeyFetchError(Exception): |
|---|
| 1324 | pass |
|---|
| 1325 | |
|---|
| 1326 | |
|---|
| 1327 | #################### dsdb ############################### |
|---|
| 1328 | |
|---|
| 1329 | class LockingError(Exception): |
|---|
| 1330 | pass |
|---|
| 1331 | |
|---|
| 1332 | |
|---|
| 1333 | class 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 | |
|---|
| 1646 | class 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 | |
|---|
| 1974 | class MintError(Exception): |
|---|
| 1975 | pass |
|---|
| 1976 | |
|---|
| 1977 | |
|---|
| 1978 | if __name__ == "__main__": |
|---|
| 1979 | import doctest |
|---|
| 1980 | doctest.testmod(optionflags=doctest.ELLIPSIS) |
|---|