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";
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
31 // True to always verify sigs. False to disable for DB access during
32 // expensive operations.
33 var $alwaysverifysigs = true;
35 // Debugging. See setdebugmsgs()
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) {
45 if (!$ssl) $ssl = new 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
66 $lock = $db->lock($t->TIME
);
67 $res = $db->get($t->TIME
);
68 $res = $this->timestamp
->next($res);
69 $db->put($t->TIME
, $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) {
120 $key = $this->assetbalancekey($id, $asset, $acct);
121 $msg = $db->get($key);
123 return $this->unpack_bankmsg($msg, $t->ATBALANCE
, $t->BALANCE
, $t->AMOUNT
);
126 // Get the values necessary to compute the storage fee.
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) {
138 $parser = $this->parser
;
141 $msg = $db->get($t->ASSET
. "/$assetid");
143 $reqs = $parser->parse($msg);
144 if (!$reqs) 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
];
162 $key = $this->fractionbalancekey($id, $assetid);
163 $msg = $db->get($key);
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
];
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";
193 function outboxdir($id) {
194 return $this->accountdir($id) . $this->t
->OUTBOX
;
197 function outboxhash($id, $newitem=false, $removed_items=false) {
201 return $u->dirhash($db, $this->outboxkey($id), $this, $newitem, $removed_items);
204 function outboxhashmsg($id) {
207 $array = $this->outboxhash($id);
208 $hash = $array[$t->HASH
];
209 $count = $array[$t->COUNT
];
210 return $this->bankmsg($this->t
->OUTBOXHASH
,
212 $this->getacctlast($id),
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) {
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];
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
];
242 function lookup_asset_name($assetid) {
243 $assetreq = $this->lookup_asset($assetid);
244 return $assetreq[$this->t
->ASSETNAME
];
247 function is_alphanumeric($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;
261 // Initialize the database, if it needs initializing
262 function setupDB($passphrase) {
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
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));
294 $db->put($t->REGFEE
, $this->bankmsg($t->REGFEE
, $bankid, 0, $tokenid, $this->regfee
));
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);
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.
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);
342 foreach ($args as $k => $v) {
344 if ($skip) $skip = false;
346 if ($msg != '(') $msg .= ',';
347 $msg .= $u->escape($v);
349 } elseif ($k == $t->MSG
) {
358 // Make a bank signed message from the args.
359 // Takes as many args as you care to pass
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) . "...";
374 // Takes as many args as you care to pass
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");
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
;
394 $reqs = $parser->parse($msg);
395 if (!$reqs) return $this->maybedie($parser->errmsg
, $fatal);
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");
409 return $this->maybedie($res, $fatal && !$res);
411 $args[$this->unpack_reqs_key
] = $reqs; // save parse results
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");
425 return $this->maybedie($res, $fatal && !$res);
427 $args[$this->unpack_reqs_key
] = $reqs; // save parse results
431 function scaninbox($id) {
433 $inboxkey = $this->inboxkey($id);
434 $times = $db->contents($inboxkey);
436 foreach ($times as $time) {
437 $item = $db->get("$inboxkey/$time");
438 if ($item) $res[] = $item;
443 function signed_balance($time, $asset, $amount, $acct=false) {
445 return $this->bankmsg($this->t
->BALANCE
, $time, $asset, $amount, $acct);
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);
456 return $this->bankmsg($this->t
->SPEND
, $bankid, $time, $id, $assetid, $amount, $note);
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) {
464 $time = $this->gettime();
465 $key = $this->accttimekey($id);
466 $lock = $db->lock($key);
475 function deq_time($id, $time) {
477 $key = $this->accttimekey($id);
478 $lock = $db->lock($key);
482 $times = explode(',', $q);
483 foreach ($times as $k => $v) {
487 $q = implode(',', $times);
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";
501 function match_bank_signed_message($inmsg) {
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
;
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);
535 function add_to_bank_balance_internal($key, $assetid, $amount) {
536 $bankid = $this->bankid
;
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";
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.";
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)));
567 // True return is an error string
568 function checkreq($args, $msg) {
572 $id = $args[$t->CUSTOMER
];
573 $req = $args[$t->REQ
];
574 $reqkey = $this->acctreqkey($id);
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);
581 if ($res) $res = $this->failmsg($msg, $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) {
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);
634 if ($id != $bankid) $state['tokens']++
;
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'] =
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];
667 $assetinfo = array();
668 $tokenid = $this->tokenid
;
669 if ($asset != $tokenid) {
670 $percent = $this->storageinfo($id, $asset, $issuer, $fraction, $fractime);
672 $digits = $u->fraction_digits($percent);
674 $time = $state['time'];
675 $fracfee = $u->storagefee($fraction, $fractime, $time, $percent, $digits);
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;
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) {
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);
729 // Validate a coupon number
730 $coupon_number_hash = sha1($coupon);
731 $key = $t->COUPON
. "/$coupon_number_hash";
732 $coupon = $db->get($key);
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);
746 $msg = $this->failmsg($inmsg, "Coupon invalid or already redeemed");
754 // Lookup a public key
755 function do_id($args, $reqs, $msg) {
758 // $t->ID => array($t->BANKID,$t->ID)
759 $customer = $args[$t->CUSTOMER
];
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) {
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++
) {
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
;
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) {
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();
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);
843 function do_getreq($args, $reqs, $msg) {
845 $id = $args[$t->CUSTOMER
];
846 return $this->bankmsg($t->REQ
,
848 $this->db
->get($this->acctreqkey($id)));
851 // Process a time request
852 function do_gettime($args, $reqs, $msg) {
855 $lock = $db->lock($this->accttimekey($id));
856 $res = $this->do_gettime_internal($msg, $args);
861 function do_gettime_internal($msg, $args) {
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) {
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";
887 function do_spend($args, $reqs, $msg) {
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);
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);
903 $this->debugmsg("post_storagefee failed: $err\n");
907 $parser->verifysigs(true);
912 function do_spend_internal($args, $reqs, $msg,
913 &$ok, &$assetid, &$issuer, &$storagefee, &$digits) {
917 $bankid = $this->bankid
;
918 $parser = $this->parser
;
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);
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");
961 $tokenid = $this->tokenid
;
965 if ($id != $id2 && $id != $bankid) {
966 // Spends to yourself are free, as are spends from the bank
967 $tokens = $this->tranfee
;
973 // No money changes hands on spends to yourself
974 $bals[$assetid] = bcsub(0, $amount);
981 $state = array('acctbals' => $acctbals,
988 $outboxhashreq = false;
989 $balancehashreq = false;
990 for ($i=1; $i<count($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
) {
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
) {
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
) {
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;
1060 return $this->failmsg($msg, "$request not valid for spend. Only " .
1061 $t->TRANFEE
. ', ' . $t->BALANCE
. ", and " .
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;
1077 $assetinfo = $charges[$assetid];
1079 $percent = $assetinfo['storagefee'];
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);
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 .= ', ';
1140 $errmsg .= "$name: $balamount";
1143 if ($errmsg != '') return $this->failmsg($msg, "Balance discrepancies: $errmsg");
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");
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");
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);
1179 $outbox_item .= ".$feemsg";
1181 $res = $outbox_item;
1184 if ($id2 != $t->COUPON
) {
1186 $newtime = $this->gettime();
1187 $inbox_item = $this->bankmsg($t->INBOX
, $newtime, $spendmsg);
1189 $inbox_item .= ".$feemsg";
1193 // If it's a coupon request, generate the coupon
1195 $random = $this->random
;
1197 require_once "LoomRandom.php";
1198 $random = new LoomRandom();
1199 $this->random
= $random;
1201 $coupon_number = $random->random_id();
1202 $bankurl = $this->bankurl
;
1204 $coupon = $this->bankmsg($t->COUPON
, $bankurl, $coupon_number, $assetid, $amount, $note);
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);
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.
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);
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
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);
1273 // Credit storage fee to an asset issuer
1274 function post_storagefee($assetid, $issuer, $storagefee, $digits) {
1277 $lock = $db->lock($this->accttimekey($issuer));
1278 $res = $this->post_storagefee_internal($assetid, $issuer, $storagefee, $digits);
1283 function post_storagefee_internal($assetid, $issuer, $storagefee, $digits) {
1287 $parser = $this->parser
;
1288 $bankid = $this->bankid
;
1290 $key = $this->storagefeekey($issuer, $assetid);
1291 $storagemsg = $db->get($key);
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) {
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);
1325 $parser->verifysigs(true);
1330 function do_spendreject_internal($args, $msg, $id) {
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
1360 $reqs = $args[$this->unpack_reqs_key
];
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", '');
1386 return $this->failmsg($msg, "Spend has already been processed");
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);
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) {
1410 $id = $args[$t->CUSTOMER
];
1411 $lock = $db->lock($this->accttimekey($id));
1412 $res = $this->do_couponenvelope_internal($args, $msg, $id);
1417 function do_couponenvelope_internal($args, $msg, $id) {
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) {
1430 $bankid = $this->bankid
;
1431 $parser = $this->parser
;
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;
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, '');
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) {
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.
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);
1492 if (count($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);
1508 function do_getinbox($args, $reqs, $msg) {
1512 $id = $args[$t->CUSTOMER
];
1513 $lock = $db->lock($this->accttimekey($id));
1514 $res = $this->do_getinbox_internal($args, $msg, $id);
1519 function do_getinbox_internal($args, $msg, $id) {
1524 $err = $this->checkreq($args, $msg);
1525 if ($err) return $err;
1527 $inbox = $this->scaninbox($id);
1528 $res = $this->bankmsg($t->ATGETINBOX
, $msg);
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
]);
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";
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]);
1580 function do_processinbox($args, $reqs, $msg) {
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);
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);
1599 $this->debugmsg("post_storagefee failed: $err\n");
1604 $parser->verifysigs(true);
1609 function do_processinbox_internal($args, $reqs, $msg, &$ok, &$charges) {
1613 $bankid = $this->bankid
;
1614 $parser = $this->parser
;
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);
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;
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");
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];
1679 $asset = $feeargs[$t->ASSET
];
1680 $amt = $feeargs[$t->AMOUNT
];
1681 $bals[$asset] = bcadd($bals[$asset], $amt);
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();
1714 $res = $this->bankmsg($t->ATPROCESSINBOX
, $parser->get_parsemsg($reqs[0]));
1717 $state = array('acctbals' => $acctbals,
1719 'tokens' => $tokens,
1721 'oldneg' => $oldneg,
1722 'newneg' => $newneg,
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++
) {
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);
1767 // Rejecting the payment. Credit the fee.
1768 $feeargs = $fees[$itemtime];
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;
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);
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();
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];
1886 $percent = $assetinfo['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'];
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");
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 .= ', ';
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" :
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");
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.
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
]);
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);
2051 // Process a storagefees request
2052 function do_storagefees($args, $reqs, $msg) {
2055 $lock = $db->lock($this->accttimekey($id));
2056 $res = $this->do_storagefees_internal($msg, $args);
2061 function do_storagefees_internal($msg, $args) {
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) {
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]);
2115 if (count($reqs) > 1) $feeargs = $u->match_pattern($reqs[1]);
2116 if ($spendargs[$t->CUSTOMER
] != $bankid ||
2117 $spendargs[$t->REQUEST
] != $t->ATSPEND ||
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 ||
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) {
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");
2148 function do_asset($args, $reqs, $msg) {
2152 $id = $args[$t->CUSTOMER
];
2153 $lock = $db->lock($this->accttimekey($id));
2154 $res = $this->do_asset_internal($args, $reqs, $msg);
2159 function do_asset_internal($args, $reqs, $msg) {
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;
2203 if (!$exists) $bals[$assetid] = -1;
2204 $acctbals = array();
2209 $tokenid = $this->tokenid
;
2210 $bals[$tokenid] = 0;
2212 $state = array('acctbals' => $acctbals,
2214 'tokens' => $tokens,
2216 'oldneg' => $oldneg,
2218 // 'time' initialized below
2221 $balancehashreq = false;
2223 for ($i=1; $i<count($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
];
2231 // Burn the transaction
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);
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);
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");
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.
2311 $res = $this->bankmsg($t->ATASSET
, $parser->get_parsemsg($reqs[0]));
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);
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);
2339 function do_getbalance($args, $reqs, $msg) {
2343 $id = $args[$t->CUSTOMER
];
2344 $lock = $db->lock($this->accttimekey($id));
2345 $res = $this->do_getbalance_internal($args, $reqs, $msg);
2350 function do_getbalance_internal($args, $reqs, $msg) {
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));
2364 $balancekey = $this->balancekey($id);
2365 $acctnames = $db->contents($balancekey);
2366 $acctkeys = array();
2367 foreach ($acctnames as $name) $acctkeys[] = "$balancekey/$name";
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);
2381 if ($res) $res .= '.';
2387 // Get the fractions
2388 $assetids = array();
2389 if ($assetid) $assetids[] = $assetid;
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));
2402 if ($res) $res .= '.';
2403 $res .= $balancehash;
2409 function do_getoutbox($args, $reqs, $msg) {
2413 $id = $args[$t->CUSTOMER
];
2414 $lock = $db->lock($this->accttimekey($id));
2415 $res = $this->do_getoutbox_internal($args, $reqs, $msg);
2420 function do_getoutbox_internal($args, $reqs, $msg) {
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;
2443 /*** End request processing ***/
2445 function commands() {
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
;
2482 $parses = $parser->parse($msg);
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);
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.