More '<?$' => '<?php$'
[Trubanc.git] / lib / server.php
blob71c57e3098eec990edfc54629d1f761d20506382
1 <?php
3 // server.php
4 // Implement the server protocol
6 require_once "tokens.php";
7 require_once "ssl.php";
8 require_once "utility.php";
9 require_once "parser.php";
10 require_once "timestamp.php";
12 class server {
14 var $db; // fsdb instance
15 var $ssl; // ssl instance
16 var $t; // tokens instance
17 var $parser; // parser instance
18 var $u; // utility instance
19 var $random; // random instance
20 var $timestamp; // timestamp instance
22 var $pubkeydb;
23 var $bankname;
24 var $bankurl;
25 var $regfee;
26 var $tranfee;
28 var $privkey;
29 var $bankid;
31 // True to always verify sigs. False to disable for DB access during
32 // expensive operations.
33 var $alwaysverifysigs = true;
35 // Debugging. See setdebugmsgs()
36 var $debugmsgs;
38 var $unpack_reqs_key = 'unpack_reqs';
40 // $db is an object that does put(key, value), get(key), and dir(key)
41 // $ssl is an object that does the protocol of ssl.php
42 // $bankname is used to initialize the bank name in a new database. Ignored otherwise.
43 function server($db, $ssl=false, $passphrase=false, $bankname='', $bankurl=false) {
44 $this->db = $db;
45 if (!$ssl) $ssl = new ssl();
46 $this->ssl = $ssl;
47 $this->t = new tokens();
48 $this->pubkeydb = $db->subdir($this->t->PUBKEY);
49 $this->parser = new parser($this->pubkeydb, $ssl);
50 $this->parser->alwaysverifysigs = $this->alwaysverifysigs;
51 $this->u = new utility($this->t, $this->parser, $this);
52 $this->timestamp = new timestamp();
53 $this->bankname = $bankname;
54 $this->bankurl = $bankurl;
55 $this->setupDB($passphrase);
58 // For utility->bankgetter
59 function bankid() {
60 return $this->bankid;
63 function gettime() {
64 $db = $this->db;
65 $t = $this->t;
66 $lock = $db->lock($t->TIME);
67 $res = $db->get($t->TIME);
68 $res = $this->timestamp->next($res);
69 $db->put($t->TIME, $res);
70 $db->unlock($lock);
71 return $res;
74 function getacctlast($id) {
75 return $this->db->get($this->acctlastkey($id));
78 function getacctreq($id) {
79 return $this->db->get($this->acctreqkey($id));
82 function accountdir($id) {
83 return $this->t->ACCOUNT . "/$id" . '/';
86 function accttimekey($id) {
87 return $this->accountdir($id) . $this->t->TIME;
90 function acctlastkey($id) {
91 return $this->accountdir($id) . $this->t->LAST;
94 function acctreqkey($id) {
95 return $this->accountdir($id) . $this->t->REQ;
98 function balancekey($id) {
99 return $this->accountdir($id) . $this->t->BALANCE;
102 function acctbalancekey($id, $acct=false) {
103 if ($acct === false) $acct = $this->t->MAIN;
104 return $this->balancekey($id) . "/$acct";
107 function assetbalancekey($id, $asset, $acct=false) {
108 if ($acct === false) $acct = $this->t->MAIN;
109 return $this->acctbalancekey($id, $acct) . "/$asset";
112 function fractionbalancekey($id, $asset) {
113 return $this->accountdir($id) . $this->t->FRACTION . "/$asset";
116 function assetbalance($id, $asset, $acct=false) {
117 $t = $this->t;
118 $db = $this->db;
120 $key = $this->assetbalancekey($id, $asset, $acct);
121 $msg = $db->get($key);
122 if (!$msg) return 0;
123 return $this->unpack_bankmsg($msg, $t->ATBALANCE, $t->BALANCE, $t->AMOUNT);
126 // Get the values necessary to compute the storage fee.
127 // Inputs:
128 // $id - the user ID
129 // $assetid - the asset ID
130 // Return value: storage fee $percent
131 // On output (set only if $percent != 0):
132 // $issuer - the ID of the asset issuer
133 // $fraction - the fraction balance for $id/$assetid
134 // $fractime - the time of the fraction
135 function storageinfo($id, $assetid, &$issuer, &$fraction, &$fractime) {
136 $t = $this->t;
137 $u = $this->u;
138 $parser = $this->parser;
139 $db = $this->db;
141 $msg = $db->get($t->ASSET . "/$assetid");
142 if ($msg) {
143 $reqs = $parser->parse($msg);
144 if (!$reqs) return 0;
146 $req = $reqs[1];
147 if (!$req) return 0;
148 $args = $u->match_pattern($req);
149 if (is_string($args)) return 0;
150 if ($args[$t->REQUEST] != $t->ATSTORAGE) return 0;
151 $req = $args[$t->MSG];
152 $args = $u->match_pattern($req);
153 if (is_string($args)) return 0;
154 if ($args[$t->REQUEST] != $t->STORAGE) return 0;
155 $issuer = $args[$t->CUSTOMER];
156 if ($issuer == $id) return 0;
158 $percent = $args[$t->PERCENT];
160 $fraction = 0;
161 $fractime = 0;
162 $key = $this->fractionbalancekey($id, $assetid);
163 $msg = $db->get($key);
164 if ($msg) {
165 $args = $this->unpack_bankmsg($msg, $t->ATFRACTION, $t->FRACTION);
166 if (!is_string($args)) {
167 $fraction = $args[$t->AMOUNT];
168 $fractime = $args[$t->TIME];
172 return $percent;
175 function outboxkey($id) {
176 return $this->accountdir($id) . $this->t->OUTBOX;
179 function outboxhashkey($id) {
180 return $this->accountdir($id) . $this->t->OUTBOXHASH;
183 function inboxkey($id) {
184 return $this->accountdir($id) . $this->t->INBOX;
187 function storagefeekey($id, $assetid=false) {
188 $res = $this->accountdir($id) . $this->t->STORAGEFEE;
189 if ($assetid) $res .= "/$assetid";
190 return $res;
193 function outboxdir($id) {
194 return $this->accountdir($id) . $this->t->OUTBOX;
197 function outboxhash($id, $newitem=false, $removed_items=false) {
198 $db = $this->db;
199 $u = $this->u;
201 return $u->dirhash($db, $this->outboxkey($id), $this, $newitem, $removed_items);
204 function outboxhashmsg($id) {
205 $t = $this->t;
207 $array = $this->outboxhash($id);
208 $hash = $array[$t->HASH];
209 $count = $array[$t->COUNT];
210 return $this->bankmsg($this->t->OUTBOXHASH,
211 $this->bankid,
212 $this->getacctlast($id),
213 $count,
214 $hash);
217 function balancehashkey($id) {
218 return $this->accountdir($id) . $this->t->BALANCEHASH;
221 function is_asset($assetid) {
222 return $this->db->get($this->t->ASSET . "/$assetid");
225 function lookup_asset($assetid) {
226 $t = $this->t;
227 $u = $this->u;
229 $asset = $this->is_asset($assetid);
230 if (!$asset) return false;
231 $res = $this->unpack_bankmsg($asset, $t->ATASSET, $t->ASSET);
232 if (is_string($res)) return $res;
233 $req1 = $res[$this->unpack_reqs_key][1];
234 if ($req1) {
235 $args = $u->match_pattern($req1);
236 if (is_string($args)) return "While matching asset storage fee: $args";
237 $res[$t->PERCENT] = $args[$t->PERCENT];
239 return $res;
242 function lookup_asset_name($assetid) {
243 $assetreq = $this->lookup_asset($assetid);
244 return $assetreq[$this->t->ASSETNAME];
247 function is_alphanumeric($char) {
248 $ord = ord($char);
249 return ($ord >= ord('0') && $ord <= ord('9')) ||
250 ($ord >= ord('A') && $ord <= ord('Z')) ||
251 ($ord >= ord('a') && $ord <= ord('z'));
254 function is_acct_name($acct) {
255 for ($i=0; $i<strlen($acct); $i++) {
256 if (!$this->is_alphanumeric(substr($acct, $i, 1))) return false;
258 return true;
261 // Initialize the database, if it needs initializing
262 function setupDB($passphrase) {
263 $db = $this->db;
264 $ssl = $this->ssl;
265 $t = $this->t;
266 $u = $this->u;
268 $bankname = $this->bankname;
269 if (!$db->get($t->PRIVKEY)) {
270 // http://www.rsa.com/rsalabs/node.asp?id=2004 recommends that 3072-bit
271 // RSA keys are equivalent to 128-bit symmetric keys, and they should be
272 // secure past 2031.
273 $privkey = $ssl->make_privkey(3072, $passphrase);
274 $db->put($t->PRIVKEY, $privkey);
275 $privkey = $ssl->load_private_key($privkey, $passphrase);
276 $this->privkey = $privkey;
277 $pubkey = $ssl->privkey_to_pubkey($privkey);
278 $bankid = $ssl->pubkey_id($pubkey);
279 $this->bankid = $bankid;
280 $db->put($t->TIME, 0);
281 $db->put($t->BANKID, $this->bankmsg($t->BANKID, $bankid));
282 $regmsg = $this->bankmsg($t->REGISTER, $bankid, "\n$pubkey", $bankname);
283 $regmsg = $this->bankmsg($t->ATREGISTER, $regmsg);
284 $db->put($t->PUBKEY . "/$bankid", $pubkey);
285 $db->put($t->PUBKEYSIG . "/$bankid", $regmsg);
286 $token_name = "Usage Tokens";
287 if ($this->bankname) $token_name = "$bankname $token_name";
288 $tokenid = $u->assetid($bankid, 0, 0, $token_name);
289 $this->tokenid = $tokenid;
290 $db->put($t->TOKENID, $this->bankmsg($t->TOKENID, $tokenid));
291 $asset = $this->bankmsg($t->ASSET, $bankid, $tokenid, 0, 0, $token_name);
292 $db->put($t->ASSET . "/$tokenid", $this->bankmsg($t->ATASSET, $asset));
293 $this->regfee = 10;
294 $db->put($t->REGFEE, $this->bankmsg($t->REGFEE, $bankid, 0, $tokenid, $this->regfee));
295 $this->tranfee = 2;
296 $db->put($t->TRANFEE, $this->bankmsg($t->TRANFEE, $bankid, 0, $tokenid, $this->tranfee));
297 $accountdir = $t->ACCOUNT . "/$bankid";
298 $db->put($this->accttimekey($bankid), 0);
299 $db->put($this->acctlastkey($bankid), 0);
300 $db->put($this->acctreqkey($bankid), 0);
301 $mainkey = $this->acctbalancekey($bankid);
302 // $t->BALANCE => array($t->BANKID,$t->TIME, $t->ASSET, $t->AMOUNT, $t->ACCT=>1),
303 $msg = $this->bankmsg($t->BALANCE, $bankid, 0, $tokenid, -1);
304 $msg = $this->bankmsg($t->ATBALANCE, $msg);
305 $db->put("$mainkey/$tokenid", $msg);
306 } else {
307 $privkey = $ssl->load_private_key($db->get($t->PRIVKEY), $passphrase);
308 $this->privkey = $privkey;
309 $this->bankid = $this->unpack_bank_param($db, $t->BANKID);
310 $this->tokenid = $this->unpack_bank_param($db, $t->TOKENID);
311 $this->regfee = $this->unpack_bank_param($db, $t->REGFEE, $t->AMOUNT);
312 $this->tranfee = $this->unpack_bank_param($db, $t->TRANFEE, $t->AMOUNT);
316 // Unpack wrapped initialization parameter
317 function unpack_bank_param($db, $type, $key=false) {
318 if (!$key) $key = $type;
319 return $this->unpack_bankmsg($db->get($type), $type, false, $key, true);
322 // Bank sign a message
323 function banksign($msg) {
324 $sig = $this->ssl->sign($msg, $this->privkey);
325 return "$msg:\n$sig";
329 // Make an unsigned message from the args.
330 // Takes as many args as you care to pass.
331 function makemsg() {
332 $t = $this->t;
333 $u = $this->u;
335 $req = func_get_args();
336 $args = $u->match_pattern($req);
337 // I don't like this at all, but I don't know what else to do
338 if (!$args) return call_user_func_array(array($this, 'failmsg'), $req);
339 if (is_string($args)) return $this->failmsg($args);
340 $msg = '(';
341 $skip = false;
342 foreach ($args as $k => $v) {
343 if (is_int($k)) {
344 if ($skip) $skip = false;
345 else {
346 if ($msg != '(') $msg .= ',';
347 $msg .= $u->escape($v);
349 } elseif ($k == $t->MSG) {
350 $skip = true;
351 $msg .= ",$v";
354 $msg .= ')';
355 return $msg;
358 // Make a bank signed message from the args.
359 // Takes as many args as you care to pass
360 function bankmsg() {
361 $req = func_get_args();
362 $req = array_merge(array($this->bankid), $req);
363 $msg = call_user_func_array(array($this, 'makemsg'), $req);
364 return $this->banksign($msg);
367 function shorten_failmsg_msg($msg) {
368 if (strlen($msg) > 1024) {
369 $msg = substr($msg, 0, 1021) . "...";
371 return $msg;
374 // Takes as many args as you care to pass
375 function failmsg() {
376 $args = func_get_args();
377 if (count($args) > 0) $args[0] = $this->shorten_failmsg_msg($args[0]);
378 $msg = array_merge(array($this->bankid, $this->t->FAILED), $args);
379 return $this->banksign($this->u->makemsg($msg));
382 function maybedie($msg, $die) {
383 if ($die) die("$msg\n");
384 return $msg;
387 // Reverse the bankmsg() function, optionally picking one field to return
388 function unpack_bankmsg($msg, $type=false, $subtype=false, $idx=false, $fatal=false) {
389 $bankid = $this->bankid;
390 $parser = $this->parser;
391 $t = $this->t;
392 $u = $this->u;
394 $reqs = $parser->parse($msg);
395 if (!$reqs) return $this->maybedie($parser->errmsg, $fatal);
396 $req = $reqs[0];
397 $args = $u->match_pattern($req);
398 if (is_string($args)) $this->maybedie("While matching bank-wrapped message: $args", $fatal);
399 if ($args[$t->CUSTOMER] != $bankid && $bankid) {
400 return $this->maybedie("bankmsg not from bank", $fatal);
402 if ($type && $args[$t->REQUEST] != $type) {
403 if ($fatal) die("Bankmsg wasn't of type: $type\n");
404 return false;
406 if (!$subtype) {
407 if ($idx) {
408 $res = $args[$idx];
409 return $this->maybedie($res, $fatal && !$res);
411 $args[$this->unpack_reqs_key] = $reqs; // save parse results
412 return $args;
415 $req = $args[$t->MSG]; // this is already parsed
416 if (!$req) return $this->maybedie("No wrapped message", $fatal);
417 $args = $u->match_pattern($req);
418 if (is_string($args)) return $this->maybedie("While matching wrapped customer message: $args", $fatal);
419 if (is_string($subtype) && !$args[$t->REQUEST] == $subtype) {
420 if ($fatal) die("Wrapped message wasn't of type: $subtype\n");
421 return false;
423 if ($idx) {
424 $res = $args[$idx];
425 return $this->maybedie($res, $fatal && !$res);
427 $args[$this->unpack_reqs_key] = $reqs; // save parse results
428 return $args;
431 function scaninbox($id) {
432 $db = $this->db;
433 $inboxkey = $this->inboxkey($id);
434 $times = $db->contents($inboxkey);
435 $res = array();
436 foreach ($times as $time) {
437 $item = $db->get("$inboxkey/$time");
438 if ($item) $res[] = $item;
440 return $res;
443 function signed_balance($time, $asset, $amount, $acct=false) {
444 if ($acct) {
445 return $this->bankmsg($this->t->BALANCE, $time, $asset, $amount, $acct);
446 } else {
447 return $this->bankmsg($this->t->BALANCE, $time, $asset, $amount);
451 function signed_spend($time, $id, $assetid, $amount, $note=false, $acct=false) {
452 $bankid = $this->bankid;
453 if ($note && $acct) {
454 return $this->bankmsg($this->t->SPEND, $bankid, $time, $id, $assetid, $amount, $note, $acct);
455 } elseif ($note) {
456 return $this->bankmsg($this->t->SPEND, $bankid, $time, $id, $assetid, $amount, $note);
457 } elseif ($acct) {
458 return $this->bankmsg($this->t->SPEND, $bankid, $time, $id, $assetid, $amount, "acct=$acct");
459 } else return $this->bankmsg($this->t->SPEND, $bankid, $time, $id, $assetid, $amount);
462 function enq_time($id) {
463 $db = $this->db;
464 $time = $this->gettime();
465 $key = $this->accttimekey($id);
466 $lock = $db->lock($key);
467 $q = $db->get($key);
468 if (!$q) $q = $time;
469 else $q .= ",$time";
470 $db->put($key, $q);
471 $db->unlock($lock);
472 return $q;
475 function deq_time($id, $time) {
476 $db = $this->db;
477 $key = $this->accttimekey($id);
478 $lock = $db->lock($key);
479 $q = $db->get($key);
480 $res = false;
481 if ($q) {
482 $times = explode(',', $q);
483 foreach ($times as $k => $v) {
484 if ($v == $time) {
485 $res = $time;
486 unset($times[$k]);
487 $q = implode(',', $times);
488 $db->put($key, $q);
492 $db->unlock($lock);
493 if (!$res) return "Timestamp not enqueued: $time";
494 $unixtime = $this->timestamp->stripfract($time);
495 if ($unixtime > ($time + 10*60)) {
496 return "Timestamp too old: $time";
498 return false;
501 function match_bank_signed_message($inmsg) {
502 $t = $this->t;
503 $u = $this->u;
504 $parser = $this->parser;
506 $req = $parser->parse($inmsg);
507 if (!$req) return $parser->errmsg;
508 if ($req) $req = $req[0];
509 $args = $u->match_pattern($req);
510 if (is_string($args)) return "Failed to match bank-signed message";
511 if ($args[$t->CUSTOMER] != $this->bankid) {
512 return "Not signed by this bank";
514 $msg = $args[$t->MSG];
515 $req = $parser->parse($msg);
516 if (!$req) return $parser->errmsg;
517 if ($req) $req = $req[0];
518 return $u->match_pattern($req);
521 // Add $amount to the bank balance for $assetid in the main account
522 // Any non-false return value is an error string
523 function add_to_bank_balance($assetid, $amount) {
524 $bankid = $this->bankid;
525 $db = $this->db;
527 if ($amount == 0) return;
528 $key = $this->assetbalancekey($bankid, $assetid);
529 $lock = $db->lock($key);
530 $res = $this->add_to_bank_balance_internal($key, $assetid, $amount);
531 $db->unlock($lock);
532 return $res;
535 function add_to_bank_balance_internal($key, $assetid, $amount) {
536 $bankid = $this->bankid;
537 $db = $this->db;
538 $t = $this->t;
540 $balmsg = $db->get($key);
541 $balargs = $this->unpack_bankmsg($balmsg, $t->ATBALANCE, $t->BALANCE);
542 if (is_string($balargs) || !$balargs) {
543 return "Error unpacking bank balance: '$balargs'";
544 } elseif ($balargs[$t->ACCT] && $balargs[$t->ACCT] != $t->MAIN) {
545 return "Bank balance message not for main account";
546 } else {
547 $bal = $balargs[$t->AMOUNT];
548 $newbal = bcadd($bal, $amount);
549 $balsign = bccomp($bal, 0);
550 $newbalsign = bccomp($newbal, 0);
551 if (($balsign >= 0 && $newbalsign < 0) ||
552 ($balsign < 0 && $newbalsign >= 0)) {
553 return "Transaction would put bank out of balance.";
554 } else {
555 // $t->BALANCE => array($t->BANKID,$t->TIME, $t->ASSET, $t->AMOUNT, $t->ACCT=>1)
556 $msg = $this->bankmsg($t->BALANCE, $bankid, $this->gettime(), $assetid, $newbal);
557 $msg = $this->bankmsg($t->ATBALANCE, $msg);
558 $db->put($key, $msg);
559 $key = $this->acctreqkey($bankid);
560 // Make sure clients update the balance
561 $db->put($key, bcadd(1, $db->get($key)));
564 return false;
567 // True return is an error string
568 function checkreq($args, $msg) {
569 $t = $this->t;
570 $db = $this->db;
572 $id = $args[$t->CUSTOMER];
573 $req = $args[$t->REQ];
574 $reqkey = $this->acctreqkey($id);
575 $res = false;
576 $lock = $db->lock($reqkey);
577 $oldreq = $db->get($reqkey);
578 if (bccomp($req, $oldreq) <= 0) $res = "New req <= old req";
579 else $db->put($reqkey, $req);
580 $db->unlock($lock);
581 if ($res) $res = $this->failmsg($msg, $res);
582 return $res;
585 // Deal with an (<id>,balance,...) item from the customer for a
586 // spend or processinbox request.
587 // $id: the customer id
588 // $msg: the signed (<id>,balance,...) message, as a string
589 // $args: parser->parse(), then utility->match_pattern() output on $balmsg
590 // &$state: an array of input and outputs:
591 // 'acctbals' => array(<acct> => array(<asset> => $msg))
592 // 'bals => array(<asset> => <amount>)
593 // 'tokens' => total new /account/<id>/balance/<acct>/<asset> files
594 // 'accts => array(<acct> => true);
595 // 'oldneg' => array(<asset> => <acct>), negative balances in current account
596 // 'newneg' => array(<asset> => <acct>), negative balances in updated account
597 // 'time' => the transaction time
598 // Returns an error string on error, or false on no error.
599 function handle_balance_msg($id, $msg, $args, &$state, $creating_asset=false) {
600 $t = $this->t;
601 $u = $this->u;
602 $db = $this->db;
603 $bankid = $this->bankid;
605 $asset = $args[$t->ASSET];
606 $amount = $args[$t->AMOUNT];
607 $acct = $args[$t->ACCT];
608 if (!$acct) $acct = $t->MAIN;
610 $state['accts'][$acct] = true;
612 if ((!$creating_asset || $asset != $creating_asset) && !$this->is_asset($asset)) {
613 return "Unknown asset id: $asset";
615 if (!is_numeric($amount)) return "Not a number: $amount";
616 if (!$this->is_acct_name($acct)) {
617 return "<acct> may contain only letters and digits: $acct";
619 if ($state['acctbals'][$acct][$asset]) {
620 return $this->failmsg($msg, "Duplicate acct/asset balance pair");
622 $state['acctbals'][$acct][$asset] = $msg;
623 $state['bals'][$asset] = bcsub($state['bals'][$asset], $amount);
624 if (bccomp($amount, 0) < 0) {
625 if ($state['newneg'][$asset]) {
626 return 'Multiple new negative balances for asset: $asset';
628 $state['newneg'][$asset] = $acct;
631 $assetbalancekey = $this->assetbalancekey($id, $asset, $acct);
632 $acctmsg = $db->get($assetbalancekey);
633 if (!$acctmsg) {
634 if ($id != $bankid) $state['tokens']++;
635 $amount = 0;
636 $acctargs = false;
637 } else {
638 $acctargs = $this->unpack_bankmsg($acctmsg, $t->ATBALANCE, $t->BALANCE);
639 if (is_string($acctargs) || !$acctargs ||
640 $acctargs[$t->ASSET] != $asset ||
641 $acctargs[$t->CUSTOMER] != $id) {
642 return "Balance entry corrupted for acct: $acct, asset: " .
643 $this->lookup_asset_name($asset) . " - $acctmsg";
645 $amount = $acctargs[$t->AMOUNT];
646 $state['bals'][$asset] = bcadd($state['bals'][$asset], $amount);
647 if (bccomp($amount, 0) < 0) {
648 if ($state['oldneg'][$asset]) {
649 return "Account corrupted. Multiple negative balances for asset: $asset";
651 $state['oldneg'][$asset] = $acct;
655 // Compute storage charges:
656 // $state['charges'] =
657 // array($assetid =>
658 // array('percent' => <Storage fee percent>,
659 // 'issuer' => <Asset issuer>,
660 // 'fraction' => <Fraction balance after fraction storage charge>,
661 // 'digits' => <Digits of precision to keep on fraction>
662 // 'storagefee' => <Total storage fee for asset>))
663 $charges = $state['charges'];
664 if (!$charges) $state['charges'] = $charges = array();
665 $assetinfo = $charges[$asset];
666 if (!$assetinfo) {
667 $assetinfo = array();
668 $tokenid = $this->tokenid;
669 if ($asset != $tokenid) {
670 $percent = $this->storageinfo($id, $asset, $issuer, $fraction, $fractime);
671 if ($percent) {
672 $digits = $u->fraction_digits($percent);
673 if ($fraction) {
674 $time = $state['time'];
675 $fracfee = $u->storagefee($fraction, $fractime, $time, $percent, $digits);
676 } else $fracfee = 0;
677 $assetinfo['percent'] = $percent;
678 $assetinfo['issuer'] = $issuer;
679 $assetinfo['fraction'] = $fraction;
680 $assetinfo['storagefee'] = $fracfee;
681 $assetinfo['digits'] = $digits;
685 $percent = $assetinfo['percent'];
686 if ($percent && bccomp($amount, 0) > 0) { // no charges for asset issuer
687 $digits = $assetinfo['digits'];
688 $accttime = $acctargs[$t->TIME];
689 $time = $state['time'];
690 $fee = $u->storagefee($amount, $accttime, $time, $percent, $digits);
691 $storagefee = bcadd($assetinfo['storagefee'], $fee, $digits);
692 $assetinfo['storagefee'] = $storagefee;
694 $charges[$asset] = $assetinfo;
695 $state['charges'] = $charges;
696 return false;
699 /*** Debugging ***/
700 function setdebugmsgs($debugmsgs) {
701 $this->debugmsgs = $debugmsgs;
704 function debugmsg($msg) {
705 if ($this->debugmsgs) {
706 $this->debugstr .= $msg;
710 /*** Request processing ***/
712 // Look up the bank's public key
713 function do_bankid($args, $reqs, $inmsg) {
714 $t = $this->t;
715 $db = $this->db;
716 $parser = $this->parser;
718 $bankid = $this->bankid;
719 $coupon = $args[$t->COUPON];
721 // $t->BANKID => array($t->PUBKEY)
722 $msg = $db->get($t->PUBKEYSIG . "/$bankid");
723 $args = $this->unpack_bankmsg($msg, $t->ATREGISTER);
724 if (is_string($args)) return $this->failmsg($msg, "Bank's pubkey is hosed");
725 $req = $args[$t->MSG];
726 $res = $parser->get_parsemsg($req);
728 if ($coupon) {
729 // Validate a coupon number
730 $coupon_number_hash = sha1($coupon);
731 $key = $t->COUPON . "/$coupon_number_hash";
732 $coupon = $db->get($key);
733 if ($coupon) {
734 $args = $this->unpack_bankmsg($coupon, $t->ATSPEND, $t->SPEND);
735 if (is_string($args)) return $this->failmsg($msg, "Can't parse coupon: $args");
736 $assetid = $args[$t->ASSET];
737 if ($assetid != $this->tokenid) {
738 return $this->failmsg($msg, "Coupon not for usage tokens");
740 $amount = $args[$t->AMOUNT];
741 if ($amount < ($this->regfee + 10)) {
742 return $this->failmsg($msg, "Coupon for less than 10 tokens more than registration fee");
744 $msg = $this->bankmsg($t->COUPONNUMBERHASH, $coupon_number_hash);
745 } else {
746 $msg = $this->failmsg($inmsg, "Coupon invalid or already redeemed");
748 $res .= ".$msg";
751 return $res;
754 // Lookup a public key
755 function do_id($args, $reqs, $msg) {
756 $t = $this->t;
757 $db = $this->db;
758 // $t->ID => array($t->BANKID,$t->ID)
759 $customer = $args[$t->CUSTOMER];
760 $id = $args[$t->ID];
761 $key = $db->get($t->PUBKEYSIG . "/$id");
762 if ($key) return $key;
763 else return $this->failmsg($msg, 'No such public key');
766 // Register a new account
767 function do_register($args, $reqs, $msg) {
768 $t = $this->t;
769 $db = $this->db;
770 $u = $this->u;
772 // $t->REGISTER => array($t->BANKID,$t->PUBKEY,$t->NAME=>1)
773 $id = $args[$t->CUSTOMER];
774 $pubkey = $args[$t->PUBKEY];
775 if ($db->get($this->acctlastkey($id))) {
776 return $this->failmsg($msg, "Already registered");
778 if ($this->ssl->pubkey_id($pubkey) != $id) {
779 return $this->failmsg($msg, "Pubkey doesn't match ID");
781 if ($this->ssl->pubkey_bits($pubkey) > 4096) {
782 return $this->failmsg($msg, "Key sizes larger than 4096 not allowed");
785 // Process included coupons
786 $reqargslist = array();
787 for ($i=1; $i<count($reqs); $i++) {
788 $req = $reqs[$i];
789 $reqargs = $u->match_pattern($req);
790 $reqid = $reqargs[$t->CUSTOMER];
791 $request = $reqargs[$t->REQUEST];
792 if ($request != $t->COUPONENVELOPE) {
793 return $this->failmsg($msg, "Non-coupon request with register: $request");
795 $reqargslist[] = $reqargs;
797 foreach ($reqargslist as $reqargs) {
798 $err = $this->do_couponenvelope_raw($reqargs, $id);
799 if ($err) return $this->failmsg($msg, $err);
802 $regfee = $this->regfee;
803 $tokenid = $this->tokenid;
804 $success = false;
805 if ($regfee > 0) {
806 $inbox = $this->scaninbox($id);
807 foreach ($inbox as $inmsg) {
808 $inmsg_args = $this->unpack_bankmsg($inmsg, false, true);
809 if (is_string($inmsg_args)) {
810 return $this->failmsg($msg, "Inbox parsing failed: $inmsg_args");
812 if ($inmsg_args && $inmsg_args[$t->REQUEST] == $t->SPEND) {
813 // $t->SPEND = array($t->BANKID,$t->TIME,$t->ID,$t->ASSET,$t->AMOUNT,$t->NOTE=>1))
814 $asset = $inmsg_args[$t->ASSET];
815 $amount = $inmsg_args[$t->AMOUNT];
816 if ($asset == $tokenid && $amount >= $regfee) {
817 $success = true;
818 break;
822 if (!$success) {
823 return $this->failmsg($msg, "Insufficient usage tokens for registration fee");
826 $bankid = $this->bankid;
827 $db->put($t->PUBKEY . "/$id", $pubkey);
828 $msg = $this->parser->get_parsemsg($reqs[0]);
829 $res = $this->bankmsg($t->ATREGISTER, $msg);
830 $db->put($t->PUBKEYSIG . "/$id", $res);
831 $time = $this->gettime();
832 if ($regfee != 0) {
833 $spendmsg = $this->signed_spend($time, $id, $tokenid, -$regfee, "Registration fee");
834 $spendmsg = $this->bankmsg($t->INBOX, $time, $spendmsg);
835 $db->put($this->inboxkey($id) . "/$time", $spendmsg);
837 $db->put($this->acctlastkey($id), 1);
838 $db->put($this->acctreqkey($id), 0);
839 return $res;
842 // Process a getreq
843 function do_getreq($args, $reqs, $msg) {
844 $t = $this->t;
845 $id = $args[$t->CUSTOMER];
846 return $this->bankmsg($t->REQ,
847 $id,
848 $this->db->get($this->acctreqkey($id)));
851 // Process a time request
852 function do_gettime($args, $reqs, $msg) {
853 $db = $this->db;
855 $lock = $db->lock($this->accttimekey($id));
856 $res = $this->do_gettime_internal($msg, $args);
857 $db->unlock($lock);
858 return $res;
861 function do_gettime_internal($msg, $args) {
862 $t = $this->t;
863 $db = $this->db;
865 $err = $this->checkreq($args, $msg);
866 if ($err) return $err;
868 $id = $args[$t->CUSTOMER];
869 $time = $this->gettime();
870 $db->put($this->accttimekey($id), $time);
871 return $this->bankmsg($t->TIME, $id, $time);
874 function do_getfees($args, $reqs, $msg) {
875 $t = $this->t;
876 $db = $this->db;
878 $err = $this->checkreq($args, $msg);
879 if ($err) return $err;
881 $regfee = $db->get($t->REGFEE);
882 $tranfee = $db->get($t->TRANFEE);
883 return "$regfee.$tranfee";
886 // Process a spend
887 function do_spend($args, $reqs, $msg) {
888 $t = $this->t;
889 $db = $this->db;
890 $parser = $this->parser;
892 $parser->verifysigs(false);
894 $id = $args[$t->CUSTOMER];
895 $lock = $db->lock($this->accttimekey($id));
896 $res = $this->do_spend_internal($args, $reqs, $msg,
897 $ok, $assetid, $issuer, $storagefee, $digits);
898 $db->unlock($lock);
899 // This is outside the customer lock to avoid deadlock with the issuer account.
900 if ($ok && $storagefee) {
901 $err = $this->post_storagefee($assetid, $issuer, $storagefee, $digits);
902 if ($err) {
903 $this->debugmsg("post_storagefee failed: $err\n");
907 $parser->verifysigs(true);
909 return $res;
912 function do_spend_internal($args, $reqs, $msg,
913 &$ok, &$assetid, &$issuer, &$storagefee, &$digits) {
914 $t = $this->t;
915 $u = $this->u;
916 $db = $this->db;
917 $bankid = $this->bankid;
918 $parser = $this->parser;
920 $ok = false;
922 // $t->SPEND => array($t->BANKID,$t->TIME,$t->ID,$t->ASSET,$t->AMOUNT,$t->NOTE=>1),
923 $id = $args[$t->CUSTOMER];
924 $time = $args[$t->TIME];
925 $id2 = $args[$t->ID];
926 $assetid = $args[$t->ASSET];
927 $amount = $args[$t->AMOUNT];
928 $note = $args[$t->NOTE];
930 // Burn the transaction, even if balances don't match.
931 $err = $this->deq_time($id, $time);
932 if ($err) return $this->failmsg($msg, $err);
934 if ($id2 == $bankid) {
935 return $this->failmsg($msg, "Spends to the bank are not allowed.");
938 $asset = $this->lookup_asset($assetid);
939 if (!$asset) {
940 return $this->failmsg($msg, "Unknown asset id: $assetid");
942 if (is_string($asset)) {
943 return $this->failmsg($msg, "Bad asset: $asset");
945 if (!is_numeric($amount)) {
946 return $this->failmsg($msg, "Not a number: $amount");
949 // Make sure there are no inbox entries older than the highest
950 // timestamp last read from the inbox
951 $inbox = $this->scaninbox($id);
952 $last = $this->getacctlast($id);
953 foreach ($inbox as $inmsg) {
954 $inmsg_args = $this->unpack_bankmsg($inmsg);
955 if ($last < 0 || bccomp($inmsg_args[$t->TIME], $last) <= 0) {
956 return $this->failmsg($msg, "Please process your inbox before doing a spend");
960 $tokens = 0;
961 $tokenid = $this->tokenid;
962 $feemsg = '';
963 $storagemsg = '';
964 $fracmsg = '';
965 if ($id != $id2 && $id != $bankid) {
966 // Spends to yourself are free, as are spends from the bank
967 $tokens = $this->tranfee;
970 $bals = array();
971 $bals[$tokenid] = 0;
972 if ($id != $id2) {
973 // No money changes hands on spends to yourself
974 $bals[$assetid] = bcsub(0, $amount);
976 $acctbals = array();
977 $accts = array();
978 $oldneg = array();
979 $newneg = array();
981 $state = array('acctbals' => $acctbals,
982 'bals' => $bals,
983 'tokens' => $tokens,
984 'accts' => $accts,
985 'oldneg' => $oldneg,
986 'newneg' => $newneg,
987 'time' => $time);
988 $outboxhashreq = false;
989 $balancehashreq = false;
990 for ($i=1; $i<count($reqs); $i++) {
991 $req = $reqs[$i];
992 $reqargs = $u->match_pattern($req);
993 if (is_string($req_args)) return $this->failmsg($msg, $reqargs); // match error
994 $reqid = $reqargs[$t->CUSTOMER];
995 $request = $reqargs[$t->REQUEST];
996 $reqtime = $reqargs[$t->TIME];
997 if ($reqtime != $time) return $this->failmsg($msg, "Timestamp mismatch");
998 if ($reqid != $id) return $this->failmsg($msg, "ID mismatch");
999 $reqmsg = $parser->get_parsemsg($req);
1000 if ($request == $t->TRANFEE) {
1001 if ($feemsg) {
1002 return $this->failmsg($msg, $t->TRANFEE . ' appeared multiple times');
1004 $tranasset = $reqargs[$t->ASSET];
1005 $tranamt = $reqargs[$t->AMOUNT];
1006 if ($tranasset != $tokenid || $tranamt != $tokens) {
1007 return $this->failmsg($msg, "Mismatched tranfee asset or amount ($tranasset <> $tokenid || $tranamt <> $tokens)");
1009 $feemsg = $this->bankmsg($t->ATTRANFEE, $reqmsg);
1010 } elseif ($request == $t->STORAGEFEE) {
1011 if ($storagemsg) {
1012 return $this->failmsg($msg, $t->STORAGEFEE . ' appeared multiple times');
1014 $storageasset = $reqargs[$t->ASSET];
1015 $storageamt = $reqargs[$t->AMOUNT];
1016 if ($storageasset != $assetid) {
1017 return $this->failmsg($msg, "Storage fee asset id doesn't match spend");
1019 $storagemsg = $this->bankmsg($t->ATSTORAGEFEE, $reqmsg);
1020 } elseif ($request == $t->FRACTION) {
1021 if ($fracmsg) {
1022 return $this->failmsg($msg, $t->FRACTION . ' appeared multiple times');
1024 $fracasset = $reqargs[$t->ASSET];
1025 $fracamt = $reqargs[$t->AMOUNT];
1026 if ($fracasset != $assetid) {
1027 return $this->failmsg($msg, "Fraction asset id doesn't match spend");
1029 $fracmsg = $this->bankmsg($t->ATFRACTION, $reqmsg);
1030 } elseif ($request == $t->BALANCE) {
1031 if ($time != $reqargs[$t->TIME]) {
1032 return $this->failmsg($msg, "Time mismatch in balance item");
1034 $errmsg = $this->handle_balance_msg($id, $reqmsg, $reqargs, $state);
1035 if ($errmsg) return $this->failmsg($msg, $errmsg);
1036 $newbals[] = $reqmsg;
1037 } elseif ($request == $t->OUTBOXHASH) {
1038 if ($outboxhashreq) {
1039 return $this->failmsg($msg, $t->OUTBOXHASH . " appeared multiple times");
1041 if ($time != $reqargs[$t->TIME]) {
1042 return $this->failmsg($msg, "Time mismatch in outboxhash");
1044 $outboxhashreq = $req;
1045 $outboxhashmsg = $reqmsg;
1046 $outboxhash = $reqargs[$t->HASH];
1047 $outboxhashcnt = $reqargs[$t->COUNT];
1048 } elseif ($request == $t->BALANCEHASH) {
1049 if ($balancehashreq) {
1050 return $this->failmsg($msg, $t->BALANCEHASH . " appeared multiple times");
1052 if ($time != $reqargs[$t->TIME]) {
1053 return $this->failmsg($msg, "Time mismatch in balancehash");
1055 $balancehashreq = $req;
1056 $balancehash = $reqargs[$t->HASH];
1057 $balancehashcnt = $reqargs[$t->COUNT];
1058 $balancehashmsg = $reqmsg;
1059 } else {
1060 return $this->failmsg($msg, "$request not valid for spend. Only " .
1061 $t->TRANFEE . ', ' . $t->BALANCE . ", and " .
1062 $t->OUTBOXHASH);
1066 $acctbals = $state['acctbals'];
1067 $bals = $state['bals'];
1068 $tokens = $state['tokens'];
1069 $accts = $state['accts'];
1070 $oldneg = $state['oldneg'];
1071 $newneg = $state['newneg'];
1072 $charges = $state['charges'];
1074 // Work the storage fee into the balances
1075 $storagefee = false;
1076 if ($charges) {
1077 $assetinfo = $charges[$assetid];
1078 if ($assetinfo) {
1079 $percent = $assetinfo['storagefee'];
1080 if ($percent) {
1081 $issuer = $assetinfo['issuer'];
1082 $storagefee = $assetinfo['storagefee'];
1083 $fraction = $assetinfo['fraction'];
1084 $digits = $assetinfo['digits'];
1085 $bal = $bals[$assetid];
1086 $bal = bcsub($bal, $storagefee, $digits);
1087 $u->normalize_balance($bal, $fraction, $digits);
1088 $bals[$assetid] = $bal;
1089 if (bccomp($fraction, $fracamt) != 0) {
1090 return $this->failmsg($msg, "Fraction amount was: $fractamt, sb: $fraction");
1092 if (bccomp($storagefee, $storageamt) != 0) {
1093 return $this->failmsg($msg, "Storage fee was: $storageamt, sb: $storagefee");
1098 if (!$storagefee && ($storagemsg || $fracmsg)) {
1099 return $this->failmsg($msg, "Storage or fraction included when no storage fee");
1102 // tranfee must be included if there's a transaction fee
1103 if ($tokens != 0 && !$feemsg && $id != $id2) {
1104 return $this->failmsg($msg, $t->TRANFEE . " missing");
1107 if (bccomp($amount, 0) < 0) {
1108 // Negative spend allowed only for switching issuer location
1109 if (!$oldneg[$assetid]) {
1110 return $this->failmsg($msg, "Negative spend on asset for which you are not the issuer");
1112 // Spending out the issuance.
1113 // Mark the new "acct" for the negative as being the spend itself.
1114 if (!$newneg[$assetid]) $newneg[$assetid] = $args;
1117 // Check that we have exactly as many negative balances after the transaction
1118 // as we had before.
1119 if (count($oldneg) != count($newneg)) {
1120 return $this->failmsg($msg, "Negative balance count not conserved");
1122 foreach ($oldneg as $asset => $acct) {
1123 if (!$newneg[$asset]) {
1124 return $this->failmsg($msg, "Negative balance assets not conserved");
1128 // Charge the transaction and new balance file tokens;
1129 $bals[$tokenid] = bcsub($bals[$tokenid], $tokens);
1131 $errmsg = "";
1132 $first = true;
1133 // Check that the balances in the spend message, match the current balance,
1134 // minus amount spent minus fees.
1135 foreach ($bals as $balasset => $balamount) {
1136 if (bccomp($balamount, 0) != 0) {
1137 $name = $this->lookup_asset_name($balasset);
1138 if (!$first) $errmsg .= ', ';
1139 $first = false;
1140 $errmsg .= "$name: $balamount";
1143 if ($errmsg != '') return $this->failmsg($msg, "Balance discrepancies: $errmsg");
1145 // Check outboxhash
1146 // outboxhash must be included, except on self spends
1147 $spendmsg = $parser->get_parsemsg($reqs[0]);
1148 if ($id != $id2 && $id != $bankid) {
1149 if (!$outboxhashreq) {
1150 return $this->failmsg($msg, $t->OUTBOXHASH . " missing");
1151 } else {
1152 $hasharray = $this->outboxhash($id, $spendmsg);
1153 $hash = $hasharray[$t->HASH];
1154 $hashcnt = $hasharray[$t->COUNT];
1155 if ($outboxhash != $hash || $outboxhashcnt != $hashcnt) {
1156 return $this->failmsg($msg, $t->OUTBOXHASH . ' mismatch');
1161 // balancehash must be included, except on bank spends
1162 if ($id != $bankid) {
1163 if (!$balancehashreq) {
1164 return $this->failmsg($msg, $t->BALANCEHASH . " missing");
1165 } else {
1166 $hasharray = $u->balancehash($db, $id, $this, $acctbals);
1167 $hash = $hasharray[$t->HASH];
1168 $hashcnt = $hasharray[$t->COUNT];
1169 if ($balancehash != $hash || $balancehashcnt != $hashcnt) {
1170 return $this->failmsg($msg, $t->BALANCEHASH . " mismatch, hash sb: $hash, was: $balancehash, count sb: $hashcnt, was: $balancehashcnt");
1175 // All's well with the world. Commit this puppy.
1176 // Eventually, the commit will be done as a second phase.
1177 $outbox_item = $this->bankmsg($t->ATSPEND, $spendmsg);
1178 if ($feemsg) {
1179 $outbox_item .= ".$feemsg";
1181 $res = $outbox_item;
1183 $newtime = false;
1184 if ($id2 != $t->COUPON) {
1185 if ($id != $id2) {
1186 $newtime = $this->gettime();
1187 $inbox_item = $this->bankmsg($t->INBOX, $newtime, $spendmsg);
1188 if ($feemsg) {
1189 $inbox_item .= ".$feemsg";
1192 } else {
1193 // If it's a coupon request, generate the coupon
1194 $ssl = $this->ssl;
1195 $random = $this->random;
1196 if (!$random) {
1197 require_once "LoomRandom.php";
1198 $random = new LoomRandom();
1199 $this->random = $random;
1201 $coupon_number = $random->random_id();
1202 $bankurl = $this->bankurl;
1203 if ($note) {
1204 $coupon = $this->bankmsg($t->COUPON, $bankurl, $coupon_number, $assetid, $amount, $note);
1205 } else {
1206 $coupon = $this->bankmsg($t->COUPON, $bankurl, $coupon_number, $assetid, $amount);
1208 $coupon_number_hash = sha1($coupon_number);
1209 $db->put($t->COUPON . "/$coupon_number_hash", "$outbox_item");
1210 $pubkey = $this->pubkeydb->get($id);
1211 $coupon = $ssl->pubkey_encrypt($coupon, $pubkey);
1212 $coupon = $this->bankmsg($t->COUPONENVELOPE, $id, $coupon);
1213 $res .= ".$coupon";
1214 $outbox_item .= ".$coupon";
1217 // I considered adding the transaction tokens to the bank
1218 // balances here, but am just leaving them in the outbox,
1219 // to be credited to this customer, if the spend is accepted,
1220 // or to the recipient, if he rejects it.
1221 // This means that auditing has to consider balances, outbox
1222 // fees, and inbox spend items.
1224 // Update balances
1225 $balancekey = $this->balancekey($id);
1226 foreach ($acctbals as $acct => $balances) {
1227 $acctdir = "$balancekey/$acct";
1228 foreach ($balances as $balasset => $balance) {
1229 $balance = $this->bankmsg($t->ATBALANCE, $balance);
1230 $res .= ".$balance";
1231 $db->put("$acctdir/$balasset", $balance);
1235 if ($fracmsg) {
1236 $key = $this->fractionbalancekey($id, $assetid);
1237 $db->put($key, $fracmsg);
1238 $res .= ".$fracmsg";
1240 if ($storagemsg) $res .= ".$storagemsg";
1242 if ($id != $id2 && $id != $bankid) {
1243 // Update outboxhash
1244 $outboxhash_item = $this->bankmsg($t->ATOUTBOXHASH, $outboxhashmsg);
1245 $res .= ".$outboxhash_item";
1246 $db->put($this->outboxhashkey($id), $outboxhash_item);
1248 // Append spend to outbox
1249 $db->put($this->outboxdir($id) . "/$time", $outbox_item);
1252 if ($id != $bankid) {
1253 // Update balancehash
1254 $balancehash_item = $this->bankmsg($t->ATBALANCEHASH, $balancehashmsg);
1255 $res .= ".$balancehash_item";
1256 $db->put($this->balancehashkey($id), $balancehash_item);
1259 // Append spend to recipient's inbox
1260 if ($newtime) {
1261 $db->put($this->inboxkey($id2) . "/$newtime", $inbox_item);
1264 // Force the user to do another getinbox, if anything appears
1265 // in his inbox since he last processed it.
1266 $db->put($this->acctlastkey($id), -1);
1268 // We're done
1269 $ok = true;
1270 return $res;
1273 // Credit storage fee to an asset issuer
1274 function post_storagefee($assetid, $issuer, $storagefee, $digits) {
1275 $db = $this->db;
1277 $lock = $db->lock($this->accttimekey($issuer));
1278 $res = $this->post_storagefee_internal($assetid, $issuer, $storagefee, $digits);
1279 $db->unlock($lock);
1280 return $res;
1283 function post_storagefee_internal($assetid, $issuer, $storagefee, $digits) {
1284 $db = $this->db;
1285 $t = $this->t;
1286 $u = $this->u;
1287 $parser = $this->parser;
1288 $bankid = $this->bankid;
1290 $key = $this->storagefeekey($issuer, $assetid);
1291 $storagemsg = $db->get($key);
1292 if ($storagemsg) {
1293 $reqs = $parser->parse($storagemsg);
1294 if (!$reqs) return $parser->errmsg;
1295 if (count($reqs) != 1) return "Bad storagefee msg: $storagemsg";
1296 $args = $u->match_pattern($reqs[0]);
1297 if (is_string($args)) return "While posting storagefee: $args";
1298 if ($args[$t->CUSTOMER] != $bankid ||
1299 $args[$t->REQUEST] != $t->STORAGEFEE ||
1300 $args[$t->BANKID] != $bankid ||
1301 $args[$t->ASSET] != $assetid) {
1302 return "Storage fee message malformed";
1304 $amount = $args[$t->AMOUNT];
1305 $storagefee = bcadd($storagefee, $amount, $digits);
1307 $time = $this->gettime();
1308 $storagemsg = $this->bankmsg($t->STORAGEFEE, $bankid, $time, $assetid, $storagefee);
1309 $db->put($key, $storagemsg);
1312 // Process a spend|reject
1313 function do_spendreject($args, $reqs, $msg) {
1314 $t = $this->t;
1315 $db = $this->db;
1316 $parser = $this->parser;
1318 $parser->verifysigs(false);
1320 $id = $args[$t->CUSTOMER];
1321 $lock = $db->lock($this->accttimekey($id));
1322 $res = $this->do_spendreject_internal($args, $msg, $id);
1323 $db->unlock($lock);
1325 $parser->verifysigs(true);
1327 return $res;
1330 function do_spendreject_internal($args, $msg, $id) {
1331 $t = $this->t;
1332 $u = $this->u;
1333 $db = $this->db;
1334 $bankid = $this->bankid;
1336 $time = $args[$t->TIME];
1337 $key = $this->outboxkey($id);
1338 $item = $db->get("$key/$time");
1339 if (!$item) return $this->failmsg($msg, "No outbox entry for time: $time");
1340 $args = $this->unpack_bankmsg($item, $t->ATSPEND, $t->SPEND);
1341 if (is_string($args)) return $this->failmsg($msg, $args);
1342 if ($time != $args[$t->TIME]) {
1343 return $this->failmsg($msg, "Time mismatch in outbox item");
1345 if ($args[$t->ID] == $t->COUPON) {
1346 return $this->failmsg($msg, "Coupons must be redeemed, not cancelled");
1348 $recipient = $args[$t->ID];
1349 $key = $this->inboxkey($recipient);
1350 $inbox = $db->contents($key);
1351 foreach ($inbox as $intime) {
1352 $item = $db->get("$key/$intime");
1353 // Unlikely, but possible
1354 if (!$item) return $this->failmsg($msg, "Spend has already been processed");
1355 $args = $this->unpack_bankmsg($item, $t->INBOX, $t->SPEND);
1356 if (is_string($args)) return $this->failmsg($msg, $args);
1357 if ($args[$t->TIME] == $time) {
1358 // Calculate the fee, if there is one
1359 $feeamt = '';
1360 $reqs = $args[$this->unpack_reqs_key];
1361 $req = $reqs[1];
1362 if ($req) {
1363 $args = $u->match_pattern($req);
1364 if (is_string($args)) return $this->failmsg($msg, $args);
1365 if ($args[$t->CUSTOMER] != $bankid) {
1366 return $this->failmsg($msg, "Fee message not from bank");
1368 if ($args[$t->REQUEST] == $t->ATTRANFEE) {
1369 $req = $args[$t->MSG];
1370 $args = $u->match_pattern($req);
1371 if (is_string($args)) return $this->failmsg($msg, $args);
1372 if ($args[$t->REQUEST] != $t->TRANFEE) {
1373 return $this->failmsg($msg, "Fee wrapper doesn't wrap fee message");
1375 $feeasset = $args[$t->ASSET];
1376 $feeamt = $args[$t->AMOUNT];
1379 // Found the inbox item corresponding to the outbox item
1380 // Make sure it's still there
1381 $lock = $db->lock("$key/$intime");
1382 $item2 = $db->get("$key/$intime");
1383 $db->put("$key/$intime", '');
1384 $db->unlock($lock);
1385 if ($item2 == '') {
1386 return $this->failmsg($msg, "Spend has already been processed");
1388 if ($feeamt) {
1389 $this->add_to_bank_balance($feeasset, $feeamt);
1391 $newtime = $this->gettime();
1392 $item = $this->bankmsg($t->INBOX, $newtime, $msg);
1393 $key = $this->inboxkey($id);
1394 $db->put("$key/$newtime", $item);
1395 return $item;
1398 return $this->failmsg($msg, "Spend has already been processed");
1402 // Redeem coupon by moving it from coupon/<coupon> to the customer inbox.
1403 // This isn't the right way to do this.
1404 // It really wants to be like processinbox, with new balances.
1405 // You have to do it this way for a new registration, though.
1406 function do_couponenvelope($args, $reqs, $msg) {
1407 $t = $this->t;
1408 $db = $this->db;
1410 $id = $args[$t->CUSTOMER];
1411 $lock = $db->lock($this->accttimekey($id));
1412 $res = $this->do_couponenvelope_internal($args, $msg, $id);
1413 $db->unlock($lock);
1414 return $res;
1417 function do_couponenvelope_internal($args, $msg, $id) {
1418 $t = $this->t;
1420 $res = $this->do_couponenvelope_raw($args, $id);
1421 if (is_string($res)) return $this->failmsg($msg, $res);
1422 return $this->bankmsg($t->ATCOUPONENVELOPE, $msg);
1425 // Called by do_register to process coupons there
1426 // Returns an error string or false
1427 function do_couponenvelope_raw($args, $id) {
1428 $t = $this->t;
1429 $db = $this->db;
1430 $bankid = $this->bankid;
1431 $parser = $this->parser;
1432 $u = $this->u;
1434 $encryptedto = $args[$t->ID];
1435 if ($encryptedto != $bankid) {
1436 return "Coupon not encrypted to bank";
1438 $coupon = $args[$t->ENCRYPTEDCOUPON];
1439 $coupon = $this->ssl->privkey_decrypt($coupon, $this->privkey);
1440 if ($u->is_coupon_number($coupon)) {
1441 $coupon_number = $coupon;
1442 } else {
1443 $args = $this->unpack_bankmsg($coupon, $t->COUPON);
1444 if (is_string($args)) return "Error parsing coupon: $args";
1445 if ($bankid != $args[$t->CUSTOMER]) {
1446 return "Coupon not signed by bank";
1448 $coupon_number = $args[$t->COUPON];
1451 $coupon_number_hash = sha1($coupon_number);
1453 $key = $t->COUPON . "/$coupon_number_hash";
1454 $lock = $db->lock($key);
1455 $outbox_item = $db->get($key);
1456 if ($outbox_item) $db->put($key, '');
1457 $db->unlock($lock);
1459 if (!$outbox_item) {
1460 $key = $this->inboxkey($id);
1461 $inbox = $db->contents($key);
1462 foreach ($inbox as $time) {
1463 $item = $db->get("$key/$time");
1464 if (strstr($item, ',' . $t->COUPON . ',')) {
1465 $reqs = $parser->parse($item);
1466 if ($reqs && count($reqs) > 1) {
1467 $req = $reqs[1];
1468 $msg = $u->match_pattern($req);
1469 if (!is_string($msg)) {
1470 if ($coupon_number_hash == $msg[$t->COUPON]) {
1471 // Customer already redeemded this coupon.
1472 // Success if he tries to do it again.
1473 return false;
1479 return "Coupon already redeemed";
1482 $args = $this->unpack_bankmsg($outbox_item, $t->ATSPEND);
1483 if (is_string($args)) {
1484 // Make sure the spender can cancel the coupon
1485 $db->put($key, $outbox_item);
1486 return "While unpacking coupon spend: $args";
1488 $reqs = $args[$this->unpack_reqs_key];
1489 $spendreq = $args[$t->MSG];
1490 $spendmsg = $parser->get_parsemsg($spendreq);
1491 $feemsg = '';
1492 if (count($reqs) > 1) {
1493 $feereq = $reqs[1];
1494 $feemsg = $parser->get_parsemsg($feereq);
1496 $newtime = $this->gettime();
1497 $inbox_item = $this->bankmsg($t->INBOX, $newtime, $spendmsg);
1498 $cnhmsg = $this->bankmsg($t->COUPONNUMBERHASH, $coupon_number_hash);
1499 $inbox_item .= $cnhmsg;
1500 if ($feemsg) $inbox_item .= ".$feemsg";
1502 $key = $this->inboxkey($id) . "/$newtime";
1503 $db->put($key, $inbox_item);
1504 return false;
1507 // Query inbox
1508 function do_getinbox($args, $reqs, $msg) {
1509 $t = $this->t;
1510 $db = $this->db;
1512 $id = $args[$t->CUSTOMER];
1513 $lock = $db->lock($this->accttimekey($id));
1514 $res = $this->do_getinbox_internal($args, $msg, $id);
1515 $db->unlock($lock);
1516 return $res;
1519 function do_getinbox_internal($args, $msg, $id) {
1520 $t = $this->t;
1521 $u = $this->u;
1522 $db = $this->db;
1524 $err = $this->checkreq($args, $msg);
1525 if ($err) return $err;
1527 $inbox = $this->scaninbox($id);
1528 $res = $this->bankmsg($t->ATGETINBOX, $msg);
1529 $last = 1;
1530 foreach ($inbox as $inmsg) {
1531 $res .= '.' . $inmsg;
1532 $args = $u->match_message($inmsg);
1533 if ($args && !is_string($args)) {
1534 if ($args[$t->REQUEST] == $t->INBOX) {
1535 $time = $args[$t->TIME];
1536 if (bccomp($time, $last) > 0) $last = $time;
1538 $args = $u->match_pattern($args[$t->MSG]);
1540 $err = false;
1541 if (!$args) $err = 'Inbox parse error';
1542 elseif (is_string($args)) $err = "Inbox match error: $args";
1543 elseif ($args[$t->ID] != $id && $args[$t->ID] != $t->COUPON) {
1544 $err = "Inbox entry for wrong ID: " . $args[$t->ID];
1546 if ($err) return $this->failmsg($msg, $err);
1549 $key = $this->storagefeekey($id);
1550 $assetids = $db->contents($key);
1551 foreach ($assetids as $assetid) {
1552 $storagefee = $db->get("$key/$assetid");
1553 $res .= ".$storagefee";
1556 // Update last time
1557 $db->put($this->acctlastkey($id), $last);
1559 /* Not pre-allocating timestamps any more
1560 * // Append the timestamps, if there are any inbox entries
1561 * if (count($inbox) > 0) {
1562 * // Avoid bumping the global timestamp if the customer already
1563 * // has two timestamps > the highest inbox timestamp.
1564 * $time = $args[$t->TIME];
1565 * $key = $this->accttimekey($id);
1566 * $times = explode(',', $db->get($key));
1567 * if (!(count($times) >= 2 &&
1568 * bccomp($times[0], $time) > 0 &&
1569 * bccomp($times[1], $time) > 0)) {
1570 * $times = array($this->gettime(), $this->gettime());
1571 * $db->put($key, implode(',', $times));
1573 * $res .= '.' . $this->bankmsg($t->TIME, $id, $times[0]);
1574 * $res .= '.' . $this->bankmsg($t->TIME, $id, $times[1]);
1577 return $res;
1580 function do_processinbox($args, $reqs, $msg) {
1581 $t = $this->t;
1582 $db = $this->db;
1583 $parser = $this->parser;
1585 $parser->verifysigs(false);
1587 $id = $args[$t->CUSTOMER];
1588 $lock = $db->lock($this->accttimekey($id));
1589 $res = $this->do_processinbox_internal($args, $reqs, $msg, $ok, $charges);
1590 $db->unlock($lock);
1591 // This is outside the customer lock to avoid deadlock with the issuer account.
1592 if ($ok && $charges) {
1593 foreach ($charges as $assetid => $assetinfo) {
1594 $issuer = $assetinfo['issuer'];
1595 $storagefee = $assetinfo['storagefee'];
1596 $digits = $assetinfo['digits'];
1597 $err = $this->post_storagefee($assetid, $issuer, $storagefee, $digits);
1598 if ($err) {
1599 $this->debugmsg("post_storagefee failed: $err\n");
1604 $parser->verifysigs(true);
1606 return $res;
1609 function do_processinbox_internal($args, $reqs, $msg, &$ok, &$charges) {
1610 $t = $this->t;
1611 $u = $this->u;
1612 $db = $this->db;
1613 $bankid = $this->bankid;
1614 $parser = $this->parser;
1616 $ok = false;
1618 // $t->PROCESSINBOX => array($t->BANKID,$t->TIME,$t->TIMELIST),
1619 $id = $args[$t->CUSTOMER];
1620 $time = $args[$t->TIME];
1621 $timelist = $args[$t->TIMELIST];
1622 $inboxtimes = explode('|', $timelist);
1624 // Burn the transaction, even if balances don't match.
1625 $err = $this->deq_time($id, $time);
1626 if ($err) return $this->failmsg($msg, $err);
1628 $spends = array();
1629 $fees = array();
1630 $accepts = array();
1631 $rejects = array();
1632 $storagemsgs = array();
1633 $fracmsgs = array();
1635 $inboxkey = $this->inboxkey($id);
1636 foreach ($inboxtimes as $inboxtime) {
1637 $item = $db->get("$inboxkey/$inboxtime");
1638 if (!$item) return $this->failmsg($msg, "Inbox entry not found: $inboxtime");
1639 $itemargs = $this->unpack_bankmsg($item, $t->INBOX, true);
1640 if ($itemargs[$t->ID] != $id && $itemargs[$t->ID] != $t->COUPON) {
1641 return $this->failmsg($msg, "Inbox corrupt. Item found for other customer");
1643 $request = $itemargs[$t->REQUEST];
1644 if ($request == $t->SPEND) {
1645 $itemtime = $itemargs[$t->TIME];
1646 $spends[$itemtime] = array($inboxtime, $itemargs);
1647 $itemreqs = $itemargs[$this->unpack_reqs_key];
1648 $itemcnt = count($itemreqs);
1649 $feereq = ($itemcnt > 1) ? $itemreqs[$itemcnt-1] : false;
1650 if ($feereq) {
1651 $feeargs = $u->match_pattern($feereq);
1652 if ($feeargs && $feeargs[$t->REQUEST] == $t->ATTRANFEE) {
1653 $feeargs = $u->match_pattern($feeargs[$t->MSG]);
1655 if (!$feeargs || $feeargs[$t->REQUEST] != $t->TRANFEE) {
1656 return $this->failmsg($msg, "Inbox corrupt. Fee not properly encoded");
1658 $fees[$itemtime] = $feeargs;
1661 elseif ($request == $t->SPENDACCEPT) $accepts[$inboxtime] = $itemargs;
1662 elseif ($request == $t->SPENDREJECT) $rejects[$inboxtime] = $itemargs;
1663 else return $this->failmsg($msg, "Inbox corrupted. Found '$request' item");
1666 $bals = array();
1667 $outboxtimes = array();
1669 // Refund the transaction fees for accepted spends
1670 foreach ($accepts as $itemargs) {
1671 $outboxtime = $itemargs[$t->TIME];
1672 $outboxtimes[] = $outboxtime;
1673 $spendfeeargs = $this->get_outbox_args($id, $outboxtime);
1674 if (is_string($spendfeeargs)) {
1675 return $this->failmsg($msg, $spendfeeargs);
1677 $feeargs = $spendfeeargs[1];
1678 if ($feeargs) {
1679 $asset = $feeargs[$t->ASSET];
1680 $amt = $feeargs[$t->AMOUNT];
1681 $bals[$asset] = bcadd($bals[$asset], $amt);
1685 $oldneg = array();
1686 $newneg = array();
1687 $tobecharged = array(); // amount/time pairs for accepted spends
1689 // Credit the spend amounts for rejected spends, but do NOT
1690 // refund the transaction fees
1691 foreach ($rejects as $itemargs) {
1692 $outboxtime = $itemargs[$t->TIME];
1693 $outboxtimes[] = $outboxtime;
1694 $spendfeeargs = $this->get_outbox_args($id, $outboxtime);
1695 if (is_string($spendfeeargs)) {
1696 return $this->failmsg($msg, $spendfeeargs);
1698 $spendargs = $spendfeeargs[0];
1699 $asset = $spendargs[$t->ASSET];
1700 $amt = $spendargs[$t->AMOUNT];
1701 $spendtime = $spendargs[$t->TIME];
1702 if (bccomp($amt, 0) < 0) {
1703 $oldneg[$asset] = $spendargs;
1705 $bals[$asset] = bcadd($bals[$asset], $amt);
1706 $tobecharged[] = array($t->AMOUNT => $amt,
1707 $t->TIME => $spendtime,
1708 $t->ASSET => $asset);
1711 $inboxmsgs = array();
1712 $acctbals = array();
1713 $accts = array();
1714 $res = $this->bankmsg($t->ATPROCESSINBOX, $parser->get_parsemsg($reqs[0]));
1715 $tokens = 0;
1717 $state = array('acctbals' => $acctbals,
1718 'bals' => $bals,
1719 'tokens' => $tokens,
1720 'accts' => $accts,
1721 'oldneg' => $oldneg,
1722 'newneg' => $newneg,
1723 'time' => $time);
1725 $outboxhashreq = false;
1726 $balancehashreq = false;
1727 $fracamts = array();
1728 $storageamts = array();
1730 // Go through the rest of the processinbox items, collecting
1731 // accept and reject instructions and balances.
1732 for ($i=1; $i<count($reqs); $i++) {
1733 $req = $reqs[$i];
1734 $reqmsg = $parser->get_parsemsg($req);
1735 $args = $u->match_pattern($req);
1736 if ($args[$t->CUSTOMER] != $id) {
1737 return $this->failmsg
1738 ($msg, "Item not from same customer as " . $t->PROCESSINBOX);
1740 $request = $args[$t->REQUEST];
1741 if ($request == $t->SPENDACCEPT ||
1742 $request == $t->SPENDREJECT) {
1743 // $t->SPENDACCEPT => array($t->BANKID,$t->TIME,$t->id,$t->NOTE=>1),
1744 // $t->SPENDREJECT => array($t->BANKID,$t->TIME,$t->id,$t->NOTE=>1),
1745 $itemtime = $args[$t->TIME];
1746 $otherid = $args[$t->ID];
1747 $inboxpair = $spends[$itemtime];
1748 if (!$inboxpair || count($inboxpair) != 2) {
1749 return $this->failmsg($msg, "'$request' not matched in '" .
1750 $t->PROCESSINBOX . "' item, itemtime: $itemtime");
1752 $itemargs = $inboxpair[1];
1753 if ($request == $t->SPENDACCEPT) {
1754 // Accepting the payment. Credit it.
1755 $itemasset = $itemargs[$t->ASSET];
1756 $itemamt = $itemargs[$t->AMOUNT];
1757 $itemtime = $itemargs[$t->TIME];
1758 if (bccomp($itemamt, 0) < 0 && $itemargs[$t->CUSTOMER] != $bankid) {
1759 $state['oldneg'][$itemasset] = $itemargs;
1761 $state['bals'][$itemasset] = bcadd($state['bals'][$itemasset], $itemamt);
1762 $tobecharged[] = array($t->AMOUNT => $itemamt,
1763 $t->TIME => $itemtime,
1764 $t->ASSET => $itemasset);
1765 $res .= '.' . $this->bankmsg($t->ATSPENDACCEPT, $reqmsg);
1766 } else {
1767 // Rejecting the payment. Credit the fee.
1768 $feeargs = $fees[$itemtime];
1769 if ($feeargs) {
1770 $feeasset = $feeargs[$t->ASSET];
1771 $feeamt = $feeargs[$t->AMOUNT];
1772 $state['bals'][$feeasset] = bcadd($state['bals'][$feeasset], $feeamt);
1774 $res .= '.' . $this->bankmsg($t->ATSPENDREJECT, $reqmsg);
1776 if ($otherid == $bankid) {
1777 if ($request == $t->SPENDREJECT &&
1778 $itemargs[$t->AMOUNT] < 0) {
1779 return $this->failmsg($msg, "You may not reject a bank charge");
1781 $inboxtime = $request;
1782 $inboxmsg = $itemargs;
1783 } else {
1784 $inboxtime = $this->gettime();
1785 $inboxmsg = $this->bankmsg($t->INBOX, $inboxtime, $reqmsg);
1787 if ($inboxtime) $inboxmsgs[] = array($otherid, $inboxtime, $inboxmsg);
1788 } elseif ($request == $t->STORAGEFEE) {
1789 if ($time != $args[$t->TIME]) {
1790 $argstime = $args[$t->TIME];
1791 return $this->failmsg($msg, "Time mismatch in storagefee item, was: $argstime, sb: $time");
1793 $storageasset = $args[$t->ASSET];
1794 $storageamt = $args[$t->AMOUNT];
1795 if ($storagemsgs[$storageasset]) {
1796 return $this->failmsg($msg, "Duplicate storage fee for asset: $storageasset");
1798 $storageamts[$storageasset] = $storageamt;
1799 $storagemsg = $this->bankmsg($t->ATSTORAGEFEE, $reqmsg);
1800 $storagemsgs[$storageasset] = $storagemsg;
1801 } elseif ($request == $t->FRACTION) {
1802 if ($time != $args[$t->TIME]) {
1803 $argstime = $args[$t->TIME];
1804 return $this->failmsg($msg, "Time mismatch in fraction item, was: $argstime, sb: $time");
1806 $fracasset = $args[$t->ASSET];
1807 $fracamt = $args[$t->AMOUNT];
1808 if ($fracmsgs[$fracasset]) {
1809 return $this->failmsg($msg, "Duplicate fraction balance for asset: $fracasset");
1811 $fracamts[$fracasset] = $fracamt;
1812 $fracmsg = $this->bankmsg($t->ATFRACTION, $reqmsg);
1813 $fracmsgs[$fracasset] = $fracmsg;
1814 } elseif ($request == $t->OUTBOXHASH) {
1815 if ($outboxhashreq) {
1816 return $this->failmsg($msg, $t->OUTBOXHASH . " appeared multiple times");
1818 if ($time != $args[$t->TIME]) {
1819 return $this->failmsg($msg, "Time mismatch in outboxhash");
1821 $outboxhashreq = $req;
1822 $outboxhashmsg = $parser->get_parsemsg($req);
1823 $outboxhash = $args[$t->HASH];
1824 $outboxcnt = $args[$t->COUNT];
1825 } elseif ($request == $t->BALANCE) {
1826 if ($time != $args[$t->TIME]) {
1827 $argstime = $args[$t->TIME];
1828 return $this->failmsg($msg, "Time mismatch in balance item, was: $argstime, sb: $time");
1830 $errmsg = $this->handle_balance_msg($id, $reqmsg, $args, $state);
1831 if ($errmsg) return $this->failmsg($msg, $errmsg);
1832 $newbals[] = $reqmsg;
1833 } elseif ($request == $t->BALANCEHASH) {
1834 if ($balancehashreq) {
1835 return $this->failmsg($msg, $t->BALANCEHASH . " appeared multiple times");
1837 if ($time != $args[$t->TIME]) {
1838 return $this->failmsg($msg, "Time mismatch in balancehash");
1840 $balancehashreq = $req;
1841 $balancehash = $args[$t->HASH];
1842 $balancehashcnt = $args[$t->COUNT];
1843 $balancehashmsg = $parser->get_parsemsg($req);
1844 } else {
1845 return $this->failmsg($msg, "$request not valid for " . $t->PROCESSINBOX .
1846 ". Only " . $t->SPENDACCEPT . ", " . $t->SPENDREJECT .
1847 ", " . $t->OUTBOXHASH . ", " .
1848 $t->BALANCE . ", &" . $t->BALANCEHASH);
1852 $acctbals = $state['acctbals'];
1853 $bals = $state['bals'];
1854 $tokens = $state['tokens'];
1855 $accts = $state['accts'];
1856 $oldneg = $state['oldneg'];
1857 $newneg = $state['newneg'];
1858 $charges = $state['charges'];
1860 // Check that we have exactly as many negative balances after the transaction
1861 // as we had before.
1862 if (count($oldneg) != count($newneg)) {
1863 return $this->failmsg($msg, "Negative balance count not conserved");
1865 foreach ($oldneg as $asset => $acct) {
1866 if (!$newneg[$asset]) {
1867 return $this->failmsg($msg, "Negative balance assets not conserved");
1871 // Charge the new balance file tokens
1872 $tokenid = $this->tokenid;
1873 $bals[$tokenid] = bcsub($bals[$tokenid], $tokens);
1875 // Work the storage fees into the balances
1876 $storagefees = array();
1877 $fractions = array();
1878 if ($charges) {
1879 // Add storage fees for accepted spends and affirmed rejects
1880 foreach ($tobecharged as $item) {
1881 $itemamt = $item[$t->AMOUNT];
1882 $itemtime = $item[$t->TIME];
1883 $itemasset = $item[$t->ASSET];
1884 $assetinfo = $charges[$itemasset];
1885 if ($assetinfo) {
1886 $percent = $assetinfo['percent'];
1887 if ($percent) {
1888 $digits = $assetinfo['digits'];
1889 $itemfee = $u->storagefee($itemamt, $itemtime, $time, $percent, $digits);
1890 $storagefee = bcadd($assetinfo['storagefee'], $itemfee, $digits);
1891 $assetinfo['storagefee'] = $storagefee;
1892 $charges[$itemasset] = $assetinfo;
1896 foreach ($charges as $itemasset => $assetinfo) {
1897 $percent = $assetinfo['digits'];
1898 if ($percent) {
1899 $digits = $assetinfo['digits'];
1900 $storagefee = $assetinfo['storagefee'];
1901 $bal = bcsub($bals[$itemasset], $storagefee, $digits);
1902 $fraction = bcadd($fractions[$itemasset], $assetinfo['fraction'], $digits);
1903 $u->normalize_balance($bal, $fraction, $digits);
1904 $bals[$itemasset] = $bal;
1905 $assetinfo['fraction'] = $fraction;
1906 $charges[$itemasset] = $assetinfo;
1907 $storagefees[$itemasset] = $storagefee;
1908 $fractions[$itemasset] = $fraction;
1913 foreach ($storageamts as $storageasset => $storageamt) {
1914 $storagefee = $storagefees[$storageasset];
1915 if (bccomp($storageamt, $storagefee) != 0) {
1916 return $this->failmsg($msg, "Storage fee mismatch, sb: $storageamt, was: $storagefee");
1918 unset($storagefees[$storageasset]);
1920 if (count($storagefees) > 0) {
1921 return $this->failmsg($msg, "Storage fees missing for some assets");
1924 foreach ($fracamts as $fracasset => $fracamt) {
1925 $fraction = $fractions[$fracasset];
1926 if (bccomp($fracamt, $fraction) != 0) {
1927 return $this->failmsg($msg, "Fraction mismatch, sb: $fracamt, was: $fraction");
1929 unset($fractions[$fracasset]);
1931 if (count($fractions) > 0) {
1932 return $this->failmsg($msg, "Fraction balances missing for some assets");
1935 $errmsg = "";
1936 $first = true;
1937 // Check that the balances in the spend message, match the current balance,
1938 // minus amount spent minus fees.
1939 foreach ($bals as $balasset => $balamount) {
1940 if ($balamount != 0) {
1941 $name = $this->lookup_asset_name($balasset);
1942 if (!$first) $errmsg .= ', ';
1943 $first = false;
1944 $errmsg .= "$name: $balamount";
1947 if ($errmsg != '') return $this->failmsg($msg, "Balance discrepancies: $errmsg");
1949 // No outbox hash maintained for the bank
1950 if ($id != $bankid) {
1951 // Make sure the outbox hash was included iff needed
1952 if ((count($outboxtimes) > 0 && !$outboxhashreq) ||
1953 (count($outboxtimes) == 0 && $outboxhashreq)) {
1954 return $this->failmsg($msg, $t->OUTBOXHASH .
1955 ($outboxhashreq ? " included when not needed" :
1956 " missing"));
1959 if ($outboxhashreq) {
1960 $hasharray = $this->outboxhash($id, false, $outboxtimes);
1961 $hash = $hasharray[$t->HASH];
1962 $hashcnt = $hasharray[$t->COUNT];
1963 if ($outboxhash != $hash || $outboxcnt != $hashcnt) {
1964 return $this->failmsg
1965 ($msg, $t->OUTBOXHASH . " mismatch");
1969 // Check balancehash
1970 if (!$balancehashreq) {
1971 return $this->failmsg($msg, $t->BALANCEHASH . " missing");
1972 } else {
1973 $hasharray = $u->balancehash($db, $id, $this, $acctbals);
1974 $hash = $hasharray[$t->HASH];
1975 $hashcnt = $hasharray[$t->COUNT];
1976 if ($balancehash != $hash || $balancehashcnt != $hashcnt) {
1977 return $this->failmsg($msg, $t->BALANCEHASH . ' mismatch');
1982 // All's well with the world. Commit this puppy.
1983 // Update balances
1984 $balancekey = $this->balancekey($id);
1985 foreach ($acctbals as $acct => $balances) {
1986 $acctkey = "$balancekey/$acct";
1987 foreach ($balances as $balasset => $balance) {
1988 $balance = $this->bankmsg($t->ATBALANCE, $balance);
1989 $res .= ".$balance";
1990 $db->put("$acctkey/$balasset", $balance);
1994 // Update accepted and rejected spenders' inboxes
1995 foreach ($inboxmsgs as $inboxmsg) {
1996 $otherid = $inboxmsg[0];
1997 if ($otherid == $bankid) {
1998 $request = $inboxmsg[1];
1999 $itemargs = $inboxmsg[2];
2000 if ($request == $t->SPENDREJECT) {
2001 // Return the funds to the bank's account
2002 $this->add_to_bank_balance($itemargs[$t->ASSET], $itemargs[$t->AMOUNT]);
2004 } else {
2005 $inboxtime = $inboxmsg[1];
2006 $inboxmsg = $inboxmsg[2];
2007 $inboxkey = $this->inboxkey($otherid);
2008 $db->put("$inboxkey/$inboxtime", $inboxmsg);
2012 // Remove no longer needed inbox and outbox entries
2013 // Probably should have a bank config parameter to archive these somewhere.
2014 $inboxkey = $this->inboxkey($id);
2015 foreach ($inboxtimes as $inboxtime) {
2016 $db->put("$inboxkey/$inboxtime", '');
2019 // Clear processed outbox entries
2020 $outboxkey = $this->outboxkey($id);
2021 foreach ($outboxtimes as $outboxtime) {
2022 $db->put("$outboxkey/$outboxtime", '');
2025 if ($id != $bankid) {
2026 // Update outboxhash
2027 if ($outboxhashreq) {
2028 $outboxhash_item = $this->bankmsg($t->ATOUTBOXHASH, $outboxhashmsg);
2029 $res .= ".$outboxhash_item";
2030 $db->put($this->outboxhashkey($id), $outboxhash_item);
2033 // Update balancehash
2034 $balancehash_item = $this->bankmsg($t->ATBALANCEHASH, $balancehashmsg);
2035 $res .= ".$balancehash_item";
2036 $db->put($this->balancehashkey($id), $balancehash_item);
2039 // Update fractions, and add fraction and storagefee messages to result
2040 foreach ($storagemsgs as $storagemsg) $res .= ".$storagemsg";
2041 foreach ($fracmsgs as $fracasset => $fracmsg) {
2042 $res .= ".$fracmsg";
2043 $key = $this->fractionbalancekey($id, $fracasset);
2044 $db->put($key, $fracmsg);
2047 $ok = true;
2048 return $res;
2051 // Process a storagefees request
2052 function do_storagefees($args, $reqs, $msg) {
2053 $db = $this->db;
2055 $lock = $db->lock($this->accttimekey($id));
2056 $res = $this->do_storagefees_internal($msg, $args);
2057 $db->unlock($lock);
2058 return $res;
2061 function do_storagefees_internal($msg, $args) {
2062 $t = $this->t;
2063 $db = $this->db;
2064 $u = $this->u;
2065 $bankid = $this->bankid;
2067 $err = $this->checkreq($args, $msg);
2068 if ($err) return $err;
2070 $id = $args[$t->CUSTOMER];
2071 $inboxkey = $this->inboxkey($id);
2072 $key = $this->storagefeekey($id);
2073 $assetids = $db->contents($key);
2074 foreach ($assetids as $assetid) {
2075 $storagefee = $db->get("$key/$assetid");
2076 $args = $this->unpack_bankmsg($storagefee, $t->STORAGEFEE);
2077 if (is_string($args)) {
2078 return $this->failmsg($msg, "storagefee parse error: $args");
2080 if ($assetid != $args[$t->ASSET]) {
2081 $feeasset = $args[$t->ASSET];
2082 return $this->failmsg($msg, "Asset mismatch, sb: $assetid, was: $feeasset");
2084 $amount = $args[$t->AMOUNT];
2085 $percent = $this->storageinfo($id, $asset, $issuer, $fraction, $fractime);
2086 $digits = $u->fraction_digits($percent);
2087 $u->normalize_balance($amount, $fraction, $digits);
2088 if (bccomp($amount, 0, 0) > 0) {
2089 $time = $this->gettime();
2090 $storagefee = $this->bankmsg($t->STORAGEFEE, $bankid, $time, $assetid, $fraction);
2091 $spend = $this->bankmsg($t->SPEND, $bankid, $time, $id, $assetid, $amount, "Storage fees");
2092 $inbox = $this->bankmsg($t->INBOX, $time, $spend);
2093 $db->put("$key/$assetid", $storagefee);
2094 $db->put("$inboxkey/$time", $inbox);
2098 return $this->bankmsg($t->ATSTORAGEFEES, $msg);
2101 function get_outbox_args($id, $spendtime) {
2102 $t = $this->t;
2103 $u = $this->u;
2104 $db = $this->db;
2105 $parser = $this->parser;
2106 $bankid = $this->bankid;
2108 $outboxkey = $this->outboxkey($id);
2109 $spendmsg = $db->get("$outboxkey/$spendtime");
2110 if (!$spendmsg) return "Can't find outbox item: $spendtime";
2111 $reqs = $parser->parse($spendmsg);
2112 if (!$reqs) return $parser->errmsg;
2113 $spendargs = $u->match_pattern($reqs[0]);
2114 $feeargs = false;
2115 if (count($reqs) > 1) $feeargs = $u->match_pattern($reqs[1]);
2116 if ($spendargs[$t->CUSTOMER] != $bankid ||
2117 $spendargs[$t->REQUEST] != $t->ATSPEND ||
2118 ($feeargs &&
2119 ($feeargs[$t->CUSTOMER] != $bankid ||
2120 $feeargs[$t->REQUEST] != $t->ATTRANFEE))) {
2121 return "Outbox corrupted";
2123 $spendargs = $u->match_pattern($spendargs[$t->MSG]);
2124 if ($feeargs) $feeargs = $u->match_pattern($feeargs[$t->MSG]);
2125 if ($spendargs[$t->CUSTOMER] != $id ||
2126 $spendargs[$t->REQUEST] != $t->SPEND ||
2127 ($feeargs &&
2128 ($feeargs[$t->CUSTOMER] != $id ||
2129 $feeargs[$t->REQUEST] != $t->TRANFEE))) {
2130 return "Outbox inner messages corrupted";
2132 return array($spendargs, $feeargs);
2135 function do_getasset($args, $reqs, $msg) {
2136 $t = $this->t;
2137 $db = $this->db;
2139 $err = $this->checkreq($args, $msg);
2140 if ($err) return $err;
2142 $assetid = $args[$t->ASSET];
2143 $asset = $db->get($t->ASSET . "/$assetid");
2144 if (!$asset) return $this->failmsg($msg, "Unknown asset: $assetid");
2145 return $asset;
2148 function do_asset($args, $reqs, $msg) {
2149 $t = $this->t;
2150 $db = $this->db;
2152 $id = $args[$t->CUSTOMER];
2153 $lock = $db->lock($this->accttimekey($id));
2154 $res = $this->do_asset_internal($args, $reqs, $msg);
2155 $db->unlock($lock);
2156 return $res;
2159 function do_asset_internal($args, $reqs, $msg) {
2160 $t = $this->t;
2161 $u = $this->u;
2162 $db = $this->db;
2163 $bankid = $this->bankid;
2164 $parser = $this->parser;
2166 if (count($reqs) < 2) {
2167 return $this->failmsg($msg, "No balance items");
2170 // $t->ASSET => array($t->BANKID,$t->ASSET,$t->SCALE,$t->PRECISION,$t->ASSETNAME),
2171 $id = $args[$t->CUSTOMER];
2172 $assetid = $args[$t->ASSET];
2173 $scale = $args[$t->SCALE];
2174 $precision = $args[$t->PRECISION];
2175 $assetname = $args[$t->ASSETNAME];
2176 $storage_msg = false;
2178 if (!(is_numeric($scale) && is_numeric($precision) &&
2179 $scale >= 0 && $precision >= 0)) {
2180 return $this->failmsg($msg, "Scale & precision must be integers >= 0");
2183 if (bccomp($scale, 10) > 0) {
2184 return $this->failmsg($msg, 'Maximum scale is 10');
2187 // Don't really need this restriction. Maybe widen it a bit?
2188 if (!$this->is_alphanumeric($assetname)) {
2189 return $this->failmsg($msg, "Asset name must contain only letters and digits");
2192 if ($assetid != $u->assetid($id, $scale, $precision, $assetname)) {
2193 return $this->failmsg
2194 ($msg, "Asset id is not sha1 hash of 'id,scale,precision,name'");
2197 $exists = ($this->is_asset($assetid));
2199 $tokens = 1; // costs 1 token for the /asset/<assetid> file
2200 if ($id == $bankid) $tokens = 0;
2202 $bals = array();
2203 if (!$exists) $bals[$assetid] = -1;
2204 $acctbals = array();
2205 $accts = array();
2206 $oldneg = array();
2207 $newneg = array();
2209 $tokenid = $this->tokenid;
2210 $bals[$tokenid] = 0;
2212 $state = array('acctbals' => $acctbals,
2213 'bals' => $bals,
2214 'tokens' => $tokens,
2215 'accts' => $accts,
2216 'oldneg' => $oldneg,
2217 'newneg' => $newneg
2218 // 'time' initialized below
2221 $balancehashreq = false;
2223 for ($i=1; $i<count($reqs); $i++) {
2224 $req = $reqs[$i];
2225 $args = $u->match_pattern($req);
2226 if (is_string($req_args)) return $this->failmsg($msg, $args); // match error
2227 $reqid = $args[$t->CUSTOMER];
2228 $request = $args[$t->REQUEST];
2229 $reqtime = $args[$t->TIME];
2230 if ($i == 1) {
2231 // Burn the transaction
2232 $time = $reqtime;
2233 $err = $this->deq_time($id, $time);
2234 if ($err) return $this->failmsg($msg, $err);
2235 $state['time'] = $time;
2237 if ($reqid != $id) return $this->failmsg($msg, "ID mismatch");
2238 elseif ($request == $t->STORAGE) {
2239 if ($storage_msg) return $this->failmsg($msg, "Duplicate storage fee");
2240 $storage_msg = $parser->get_parsemsg($req);
2242 elseif ($request == $t->BALANCE) {
2243 $reqmsg = $parser->get_parsemsg($req);
2244 $errmsg = $this->handle_balance_msg($id, $reqmsg, $args, $state, $assetid);
2245 if ($errmsg) return $this->failmsg($msg, $errmsg);
2246 $newbals[] = $reqmsg;
2247 } elseif ($request == $t->BALANCEHASH) {
2248 if ($balancehashreq) {
2249 return $this->failmsg($msg, $t->BALANCEHASH . " appeared multiple times");
2251 $balancehashreq = $req;
2252 $balancehash = $args[$t->HASH];
2253 $balancehashcnt = $args[$t->COUNT];
2254 $balancehashmsg = $parser->get_parsemsg($req);
2255 } else {
2256 return $this->failmsg($msg, "$request not valid for asset creation. Only " .
2257 $t->BALANCE . ' & ' . $t->BALANCEHASH);
2261 $acctbals = $state['acctbals'];
2262 $bals = $state['bals'];
2263 $accts = $state['accts'];
2264 $tokens = $state['tokens'];
2265 $oldneg = $state['oldneg'];
2266 $newneg = $state['newneg'];
2268 // Check that we have exactly as many negative balances after the transaction
2269 // as we had before, plus one for the new asset
2270 if (!$exists) $oldneg[$assetid] = $t->MAIN;
2271 if (count($oldneg) != count($newneg)) {
2272 return $this->failmsg($msg, "Negative balance count not conserved");
2274 foreach ($oldneg as $asset => $acct) {
2275 if (!$newneg[$asset]) {
2276 return $this->failmsg($msg, "Negative balance assets not conserved");
2280 // Charge the new file tokens
2281 $bals[$tokenid] = bcsub($bals[$tokenid], $tokens);
2283 $errmsg = "";
2284 // Check that the balances in the spend message, match the current balance,
2285 // minus amount spent minus fees.
2286 foreach ($bals as $balasset => $balamount) {
2287 if ($balamount != 0) {
2288 if ($balasset == $assetid) $name = $assetname;
2289 else $name = $this->lookup_asset_name($balasset);
2290 if ($errmsg) $errmsg .= ', ';
2291 $errmsg .= "$name: $balamount";
2294 if ($errmsg != '') return $this->failmsg($msg, "Balance discrepancies: $errmsg");
2296 // balancehash must be included
2297 if (!$balancehashreq) {
2298 return $this->failmsg($msg, $t->BALANCEHASH . " missing");
2299 } else {
2300 $hasharray = $u->balancehash($db, $id, $this, $acctbals);
2301 $hash = $hasharray[$t->HASH];
2302 $hashcnt = $hasharray[$t->COUNT];
2303 if ($balancehash != $hash || $balancehashcnt != $hashcnt) {
2304 return $this->failmsg($msg, $t->BALANCEHASH .
2305 " mismatch, hash: $balancehash, sb: $hash, count: $balancehashcnt, sb: $hashcnt");
2309 // All's well with the world. Commit this puppy.
2310 // Add asset
2311 $res = $this->bankmsg($t->ATASSET, $parser->get_parsemsg($reqs[0]));
2312 if ($storage_msg) {
2313 $res .= "." . $this->bankmsg($t->ATSTORAGE, $storage_msg);
2315 $db->put($t->ASSET . "/$assetid", $res);
2317 // Credit bank with tokens
2318 $this->add_to_bank_balance($tokenid, $tokens);
2320 // Update balances
2321 $balancekey = $this->balancekey($id);
2322 foreach ($acctbals as $acct => $balances) {
2323 $acctkey = "$balancekey/$acct";
2324 foreach ($balances as $balasset => $balance) {
2325 $balance = $this->bankmsg($t->ATBALANCE, $balance);
2326 $res .= ".$balance";
2327 $db->put("$acctkey/$balasset", $balance);
2331 // Update balancehash
2332 $balancehash_item = $this->bankmsg($t->ATBALANCEHASH, $balancehashmsg);
2333 $res .= ".$balancehash_item";
2334 $db->put($this->balancehashkey($id), $balancehash_item);
2336 return $res;
2339 function do_getbalance($args, $reqs, $msg) {
2340 $t = $this->t;
2341 $db = $this->db;
2343 $id = $args[$t->CUSTOMER];
2344 $lock = $db->lock($this->accttimekey($id));
2345 $res = $this->do_getbalance_internal($args, $reqs, $msg);
2346 $db->unlock($lock);
2347 return $res;
2350 function do_getbalance_internal($args, $reqs, $msg) {
2351 $t = $this->t;
2352 $db = $this->db;
2354 $err = $this->checkreq($args, $msg);
2355 if ($err) return $err;
2357 // $t->GETBALANCE => array($t->BANKID,$t->REQ,$t->ACCT=>1,$t->ASSET=>1));
2358 $id = $args[$t->CUSTOMER];
2359 $acct = $args[$t->ACCT];
2360 $assetid = $args[$t->ASSET];
2362 if ($acct) $acctkeys = array($this->acctbalancekey($id, $acct));
2363 else {
2364 $balancekey = $this->balancekey($id);
2365 $acctnames = $db->contents($balancekey);
2366 $acctkeys = array();
2367 foreach ($acctnames as $name) $acctkeys[] = "$balancekey/$name";
2370 $res = '';
2371 $assetnames = array();
2372 foreach ($acctkeys as $acctkey) {
2373 if ($assetid) $assetnames[] = $assetid;
2374 else $assetnames = $db->contents($acctkey);
2375 $assetkeys = array();
2376 foreach ($assetnames as $name) $assetkeys[] = "$acctkey/$name";
2378 foreach ($assetkeys as $assetkey) {
2379 $bal = $db->get($assetkey);
2380 if ($bal) {
2381 if ($res) $res .= '.';
2382 $res .= $bal;
2387 // Get the fractions
2388 $assetids = array();
2389 if ($assetid) $assetids[] = $assetid;
2390 else {
2391 foreach ($assetnames as $name) $assetids[] = $name;
2394 foreach($assetids as $assetid) {
2395 $fractionkey = $this->fractionbalancekey($id, $assetid);
2396 $fraction = $db->get($fractionkey);
2397 if ($fraction) $res .= ".$fraction";
2400 $balancehash = $db->get($this->balancehashkey($id));
2401 if ($balancehash) {
2402 if ($res) $res .= '.';
2403 $res .= $balancehash;
2406 return $res;
2409 function do_getoutbox($args, $reqs, $msg) {
2410 $t = $this->t;
2411 $db = $this->db;
2413 $id = $args[$t->CUSTOMER];
2414 $lock = $db->lock($this->accttimekey($id));
2415 $res = $this->do_getoutbox_internal($args, $reqs, $msg);
2416 $db->unlock($lock);
2417 return $res;
2420 function do_getoutbox_internal($args, $reqs, $msg) {
2421 $t = $this->t;
2422 $db = $this->db;
2424 $err = $this->checkreq($args, $msg);
2425 if ($err) return $err;
2427 // $t->GETOUTBOX => array($t->BANKID,$t->REQ),
2428 $id = $args[$t->CUSTOMER];
2430 $msg = $this->bankmsg($t->ATGETOUTBOX, $msg);
2431 $outboxkey = $this->outboxkey($id);
2432 $contents = $db->contents($outboxkey);
2433 foreach ($contents as $time) {
2434 $msg .= '.' . $db->get("$outboxkey/$time");
2436 $outboxhash = $db->get($this->outboxhashkey($id));
2437 if ($outboxhash) $msg .= '.' . $outboxhash;
2439 return $msg;
2443 /*** End request processing ***/
2445 function commands() {
2446 $t = $this->t;
2447 $u = $this->u;
2449 if (!$this->commands) {
2450 $patterns = $u->patterns();
2451 $names = array($t->BANKID => array($t->PUBKEY, $t->COUPON=>1),
2452 $t->ID => array($t->BANKID,$t->ID),
2453 $t->REGISTER => $patterns[$t->REGISTER],
2454 $t->GETREQ => array($t->BANKID),
2455 $t->GETTIME => array($t->BANKID,$t->REQ),
2456 $t->GETFEES => array($t->BANKID,$t->REQ,$t->OPERATION=>1),
2457 $t->SPEND => $patterns[$t->SPEND],
2458 $t->SPENDREJECT => $patterns[$t->SPENDREJECT],
2459 $t->COUPONENVELOPE => $patterns[$t->COUPONENVELOPE],
2460 $t->GETINBOX => $patterns[$t->GETINBOX],
2461 $t->PROCESSINBOX => $patterns[$t->PROCESSINBOX],
2462 $t->STORAGEFEES => $patterns[$t->STORAGEFEES],
2463 $t->GETASSET => array($t->BANKID,$t->REQ,$t->ASSET),
2464 $t->ASSET => array($t->BANKID,$t->ASSET,$t->SCALE,$t->PRECISION,$t->ASSETNAME),
2465 $t->GETOUTBOX => $patterns[$t->GETOUTBOX],
2466 $t->GETBALANCE => array($t->BANKID,$t->REQ,$t->ACCT=>1,$t->ASSET=>1));
2467 $commands = array();
2468 foreach($names as $name => $pattern) {
2469 $fun = str_replace('|', '', $name);
2470 $commands[$name] = array("do_$fun", $pattern);
2472 $this->commands = $commands;
2474 return $this->commands;
2477 // Process a message and return the response
2478 // This is usually all you'll call from outside
2479 function process($msg) {
2480 $parser = $this->parser;
2481 $t = $this->t;
2482 $parses = $parser->parse($msg);
2483 if (!$parses) {
2484 return $this->failmsg($msg, $parser->errmsg);
2486 $req = $parses[0][1];
2487 $commands = $this->commands();
2488 $method_pattern = $commands[$req];
2489 if (!$method_pattern) {
2490 return $this->failmsg($msg, "Unknown request: $req");
2492 $method = $method_pattern[0];
2493 $pattern = array_merge(array($t->CUSTOMER,$t->REQUEST), $method_pattern[1]);
2494 $args = $this->parser->matchargs($parses[0], $pattern);
2495 if (!$args) {
2496 return $this->failmsg($msg,
2497 "Request doesn't match pattern: " .
2498 $parser->formatpattern($pattern));
2500 $argsbankid = $args[$t->BANKID];
2501 if (array_key_exists($t->BANKID, $args) && $argsbankid != $this->bankid) {
2502 return $this->failmsg($msg, "bankid mismatch");
2504 if (strlen($args[$t->NOTE]) > 4096) {
2505 return $this->failmsg($msg, "Note too long. Max: 4096 chars");
2507 return $this->$method($args, $parses, $msg);
2512 // Copyright 2008-2009 Bill St. Clair
2514 // Licensed under the Apache License, Version 2.0 (the "License");
2515 // you may not use this file except in compliance with the License.
2516 // You may obtain a copy of the License at
2518 // http://www.apache.org/licenses/LICENSE-2.0
2520 // Unless required by applicable law or agreed to in writing, software
2521 // distributed under the License is distributed on an "AS IS" BASIS,
2522 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2523 // See the License for the specific language governing permissions
2524 // and limitations under the License.