<?php
require_once "vendor/autoload.php";
use Mdanter\Ecc\Crypto\Signature\SignHasher;
use Mdanter\Ecc\Crypto\Key\PublicKey;
use Mdanter\Ecc\Primitives\Point;
use Mdanter\Ecc\EccFactory;
use Mdanter\Ecc\Crypto\Signature\Signer;
use Mdanter\Ecc\Serializer\PrivateKey\DerPrivateKeySerializer;
use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer;
use Mdanter\Ecc\Math;
use Mdanter\Ecc\Primitives\CurveFp;
use Mdanter\Ecc\Crypto\Signature;
use Mdanter\Ecc\Math\GmpMath;
$adapter = EccFactory::getAdapter();
$curve = EccFactory::getNistCurves()->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;
}