curve384(); $generator = EccFactory::getNistCurves()->generator384(); $useDerandomizedSignatures = true; $algorithm = 'sha384'; $math = new GmpMath(); function sec1parse ($in) { switch ($in[0]) { case "\x02": $isOdd = false; break; case "\x03": $isOdd = true; break; default: return null; } global $math; global $curve; $x = $math->stringToInt(substr($in, 1, 48)); $y = $curve->recoverYfromX($isOdd, $x); global $adapter; global $generator; return new PublicKey($adapter, $generator, new Point($adapter, $curve, $x, $y)); } class Transaction { public $sender; public $recipient; public $amount; public $comment; public $nonce; public $r; public $s; public function parse ($in) { $this->sender = substr($in, 0, 49); $this->recipient = substr($in, 49, 49); $amount = substr($in, 49*2, 4); $this->amount = unpack("N", $amount)[1]; $this->comment = substr($in, 49*2+4, 256); $this->nonce = substr($in, 49*2+4+256, 32); $this->r = substr($in, 49*2+4+256+32, 48); $this->s = substr($in, 49*2+4+256+32+48, 48); } public function serialize ($without_signature = false) { return str_pad($this->sender, 49, "\0") . str_pad($this->recipient, 49, "\0") . pack("N", $this->amount) . str_pad($this->comment, 256, "\0") . str_pad($this->nonce, 32, "\0") . ($without_signature ? "" : (str_pad($this->r, 48, "\0") . str_pad($this->s, 48, "\0"))); } public function verify () { global $adapter; global $generator; global $algorithm; global $math; $signer = new Signer($adapter); $publickey = sec1parse($this->sender); $hasher = new SignHasher($algorithm, $adapter); $hash = $hasher->makeHash($this->serialize(true), $generator); return $signer->verify($publickey, new \Mdanter\Ecc\Crypto\Signature\Signature($math->stringToInt($this->r), $math->stringToInt($this->s)), $hash); } public function hash () { return hash("sha256", $this->serialize(), true); } } function tx_from_row($row) { $tx = new Transaction(); $tx->sender = $row["sender"]; $tx->recipient = $row["recipient"]; $tx->amount = $row["amount"]; $tx->comment = $row["comment"]; $tx->nonce = $row["nonce"]; $tx->r = $row["r"]; $tx->s = $row["s"]; return $tx; } function last_tx ($db) { foreach ($db->query("select * from transactions order by id desc limit 1") as $row); if ($row) return tx_from_row($row); return; } if (!empty($_REQUEST["src"])) { header("Content-Type: text/plain"); die(file_get_contents($_SERVER["SCRIPT_FILENAME"])); } if ($_SERVER["REQUEST_METHOD"] == "OPTIONS") { http_response_code(204); header("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Methods: *"); header("Access-Control-Allow-Headers: *"); header("Access-Control-Max-Age: 86400"); die(); } define("TEXT", "text/plain"); function response ($code, $body="", $type="application/octet-stream") { http_response_code($code); header("Content-Type: " . $type); header("Access-Control-Allow-Origin: *"); header("Access-Control-Allow-Methods: *"); header("Access-Control-Allow-Headers: *"); header("Access-Control-Max-Age: 86400"); echo $body; } if (($ret = @file_get_contents("error_status.txt")) !== false) { response(500, $ret, TEXT); die(); } function computers_post_handler ($in, $db, $forcepost=false) { $numcomp = sizeof($db->query("select url from computers")); if (strlen($in) % 256) { return [413, "content length should've been divisible by 256", TEXT]; } $in = str_split($in, 256); $stmt = $db->prepare("insert or ignore into computers (url) values (:url)"); foreach ($in as $url) { $stmt->bindParam(":url", $url, PDO::PARAM_LOB); $stmt->execute(); } $stmt = null; $computers = []; foreach ($db->query("select url from computers") as $url) $computers[] = $url; if ($numcomp != sizeof($computers) || $forcepost) { foreach ($computers as $url) // this would be better with curl parallel/multi file_get_contents(explode("\0", $url)[0] . "computers", false, stream_context_create(["http" => ["method" => "POST", "content" => implode("", $computers), "timeout" => 1]])); return [201]; } else { return [202]; } } function transactions_post_handler ($in, $db) { $tx = new Transaction(); $txlen = strlen($tx->serialize()); if (strlen($in) % $txlen) { return [469, "body length should've been divisible by $txlen", TEXT]; } $in = str_split($in, $txlen); foreach ($in as $txstr) { $tx->parse($txstr); if (!$tx->verify()) continue; $stmt = $db->prepare("select * from transactions where hash=:hash"); $txhash = $tx->hash(); $stmt->bindParam(":hash", $txhash, PDO::PARAM_LOB); $stmt->execute(); if ($stmt->rowCount()) continue; $stmt = null; $stmt = $db->prepare("insert or ignore into transactions (sender, recipient, amount, comment, nonce, r, s, hash) values (:sender, :recipient, :amount, :comment, :nonce, :r, :s, :hash)"); $stmt->bindParam(":sender", $tx->sender, PDO::PARAM_LOB); $stmt->bindParam(":recipient", $tx->recipient, PDO::PARAM_LOB); $stmt->bindParam(":amount", $tx->amount, PDO::PARAM_LOB); $stmt->bindParam(":comment", $tx->comment, PDO::PARAM_LOB); $stmt->bindParam(":nonce", $tx->nonce, PDO::PARAM_LOB); $stmt->bindParam(":r", $tx->r, PDO::PARAM_LOB); $stmt->bindParam(":s", $tx->s, PDO::PARAM_LOB); $stmt->bindParam(":hash", $txhash, PDO::PARAM_LOB); $stmt->execute(); $stmt = null; $computers = []; foreach ($db->query("select url from computers") as $url) $computers[] = $url; foreach ($computers as $url) file_get_contents(explode("\0", $url)[0] . "transaction", false, stream_context_create(["http" => ["method" => "POST", "content" => $in, "timeout" => 1]])); } return [200]; } function transactions_get_handler ($db, $after) { $response = ""; $ret = $db->query("select * from transactions order by id"); $hash = null; $stmt = $db->prepare("select * from transactions where hash=:hash"); $stmt->bindParam(":hash", $after, PDO::PARAM_LOB); $stmt->execute(); if ($stmt->fetch()) $hash = $after; $stmt = null; foreach ($ret as $row) if ($hash) { if ($hash == tx_from_row($row)->hash()) $hash = null; } else $response .= tx_from_row($row)->serialize(); if ($response == "") return [204]; return [200, $response]; } function sync_checkpoint_computer ($db, $url) { $stmt = $db->prepare("select last_hash from computers where url=:url"); $stmt->bindParam(":url", $url, PDO::PARAM_LOB); $stmt->execute(); return $stmt->fetchColumn(0); } # create table computers (url TEXT NOT NULL UNIQUE CHECK(length(url) == 256), last_hash TEXT NOT NULL UNIQUE CHECK(length(last_hash) == 32), date default CURRENT_TIMESTAMP); # create table transactions (id integer primary key autoincrement, sender TEXT NOT NULL CHECK(length(sender) == 49), recipient TEXT NOT NULL CHECK(length(recipient) == 49), amount INTEGER NOT NULL CHECK(amount >= 0), comment TEXT NOT NULL CHECK(length(comment) == 256), nonce TEXT NOT NULL CHECK(length(nonce) == 32), r TEXT NOT NULL CHECK(length(r) == 48), s TEXT NOT NULL CHECK(length(s) == 48), hash TEXT NOT NULL UNIQUE CHECK(length(hash) == 32), date default CURRENT_TIMESTAMP); $db = new PDO("sqlite:db", null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); if (!$db) response(503, "db: " . $e->getMessage(), TEXT); switch ($_REQUEST["e"] . "-" . $_SERVER["REQUEST_METHOD"]) { case "sec1decompress-GET": $x = $math->intToString(sec1parse(hex2bin($_REQUEST["s"]))->getPoint()->getX()); $y = $math->intToString(sec1parse(hex2bin($_REQUEST["s"]))->getPoint()->getY()); response(200, "\x04$x$y"); break; case "sec1decompress-POST": $in = file_get_contents("php://input"); global $math; $x = $math->intToString(sec1parse($in)->getPoint()->getX()); $y = $math->intToString(sec1parse($in)->getPoint()->getY()); response(200, "\x04$x$y"); break; case "push-POST": $in = file_get_contents("php://input"); for ($i = 0; $i < 60; $i++) { $resp = transactions_get_handler($db, $in); if ($resp[0] == 200) { response(...$resp); break; } usleep(250000); } if ($resp[0] != 200) response(204); break; case "jutro-GET": $computers = []; foreach ($db->query("select url from computers") as $url) $computers[] = $url; $send = ""; foreach ($computers as $url) { $recvd = file_get_contents(explode("\0", $url)[0] . "computers"); if (strlen($recvd) % 256) { error_log("server $url returned non mod256 computers get response length", 3, "log"); continue; } $send .= $recvd; } computers_post_handler($send, $db, true); $computers = []; foreach ($db->query("select url from computers") as $url) $computers[] = $url; foreach ($computers as $url) { $transactions = file_get_contents(explode("\0", $url[0])[0] . "transactions", false, stream_context_create(["http" => ["header" => "After: " . bin2hex(sync_checkpoint_computer($db, $url)) . "\r\n", "timeout" => 1]])); $tx = new Transaction(); if (strlen($transactions) % strlen($tx->serialize())) { error_log("server $url returned not correct mod for transactions response length", 3, "log"); continue; } foreach (str_split($transactions, strlen($tx->serialize())) as $transaction) { $tx->parse($transaction); $txhash = $tx->hash; $stmt = $db->prepare("update computers set last_hash=:last_hash where url=:url"); $stmt->bindParam(":last_hash", $txhash, PDO::PARAM_LOB); $stmt->bindParam(":url", $url, PDO::PARAM_LOB); $stmt->execute(); transactions_post_handler($transaction); } } break; case "computers-GET": $ret = $db->query("select url from computers"); response(200); foreach ($ret as $row) echo $row[0]; break; case "computers-POST": $in = file_get_contents("php://input"); response(...computers_post_handler($in, $db)); break; case "transactions-POST": $in = file_get_contents("php://input"); response(...transactions_post_handler($in, $db)); break; case "transactions-GET": response(...transactions_get_handler($db, hex2bin($_SERVER["HTTP_AFTER"]))); break; case "state-GET": $ret = $db->query("select * from transactions order by id"); $out = ""; $balances = []; foreach ($ret as $row) { $tx = tx_from_row($row); if (!$tx->verify()) { $message = "transaction with internal id {$row["id"]} has an invalid signature."; file_put_contents("error_status.txt", $message); response(500, $message); break 2; } @$balances[$tx->sender] -= $tx->amount; @$balances[$tx->recipient] += $tx->amount; } response(200); foreach ($balances as $key => $value) { // do not trust balances provided by this API, since they $packed = pack("q", $value); // are cast to machine dependent int by php if (pack("Q", 123) === pack("P", 123)) // machine is little endian $packed = strrev($packed); echo $key . $packed; } break; default: response(400, "unknown endpoint or method not allowed", TEXT); break; }