<?php
/*
The MIT License (MIT)
Copyright (c) 2018 AroDev

www.arionum.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.
*/
const SANITY_LOCK_PATH = __DIR__.'/tmp/sanity-lock';
require_once __DIR__.'/include/init.inc.php';
header('Content-Type: application/json');

$trx = new Transaction();
$block = new Block();
$q = $_GET['q'];
// the data is sent as json, in $_POST['data']
if (!empty($_POST['data'])) {
    $data = json_decode(trim($_POST['data']), true);
}

// make sure it's the same coin and not testnet
if ($_POST['coin'] != $_config['coin']) {
    api_err("Invalid coin");
}
$ip = san_ip($_SERVER['REMOTE_ADDR']);
$ip = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);

// there should not be any local peering attempts
if($_SERVER['REMOTE_ADDR']==$_SERVER['SERVER_ADDR']){
        api_err("invalid-ip");
}


// peer with the current node
if ($q == "peer") {
    // sanitize the hostname
    $hostname = filter_var($data['hostname'], FILTER_SANITIZE_URL);

    $bad_peers = ["127.", "localhost", "10.", "192.168.","172.16.","172.17.","172.18.","172.19.","172.20.","172.21.","172.22.","172.23.","172.24.","172.25.","172.26.","172.27.","172.28.","172.29.","172.30.","172.31."];
    $tpeer=str_replace(["https://","http://","//"], "", $hostname);
    foreach ($bad_peers as $bp) {
        if (strpos($tpeer, $bp)===0) {
            api_err("invalid-hostname");
        }
    }

    if (!filter_var($hostname, FILTER_VALIDATE_URL)) {
        api_err("invalid-hostname");
    }
    $hostname = san_host($hostname);
    // if it's already peered, only repeer on request
    $res = $db->single(
        "SELECT COUNT(1) FROM peers WHERE hostname=:hostname AND ip=:ip",
        [":hostname" => $hostname, ":ip" => $ip]
    );
    if ($res == 1) {
        if (isset($data['repeer']) && $data['repeer'] == 1) {
            $res = peer_post($hostname."/peer.php?q=peer", ["hostname" => $_config['hostname']]);
            if ($res !== false) {
                api_echo("re-peer-ok");
            } else {
                api_err("re-peer failed - $result");
            }
        }
        api_echo("peer-ok-already");
    }
    // if we have enough peers, add it to DB as reserve
    $res = $db->single("SELECT COUNT(1) FROM peers WHERE blacklisted<UNIX_TIMESTAMP() AND ping >UNIX_TIMESTAMP()-86400 AND reserve=0");
    $reserve = 1;
    if ($res < $_config['max_peers']) {
        $reserve = 0;
    }
    $db->run(
        "INSERT ignore INTO peers SET hostname=:hostname, reserve=:reserve, ping=UNIX_TIMESTAMP(), ip=:ip ON DUPLICATE KEY UPDATE hostname=:hostname2",
        [":ip" => $ip, ":hostname2" => $hostname, ":hostname" => $hostname, ":reserve" => $reserve]
    );
    // re-peer to make sure the peer is valid
    $res = peer_post($hostname."/peer.php?q=peer", ["hostname" => $_config['hostname']]);
    if ($res !== false) {
        api_echo("re-peer-ok");
    } else {
        $db->run("DELETE FROM peers WHERE ip=:ip", [":ip" => $ip]);
        api_err("re-peer failed - $result");
    }
} elseif ($q == "ping") {
    // confirm peer is active
    api_echo("pong");
} elseif ($q == "submitTransaction") {
    // receive a new transaction from a peer
    $current = $block->current();


    // no transactions accepted if the sanity is syncing
    if ($_config['sanity_sync'] == 1) {
        api_err("sanity-sync");
    }

    $data['id'] = san($data['id']);
    // validate transaction data
    if (!$trx->check($data)) {
        api_err("Invalid transaction");
    }
    $hash = $data['id'];
    // make sure it's not already in mempool
    $res = $db->single("SELECT COUNT(1) FROM mempool WHERE id=:id", [":id" => $hash]);
    if ($res != 0) {
        api_err("The transaction is already in mempool");
    }
    // make sure the peer is not flooding us with transactions
    $res = $db->single("SELECT COUNT(1) FROM mempool WHERE src=:src", [":src" => $data['src']]);
    if ($res > 25) {
        api_err("Too many transactions from this address in mempool. Please rebroadcast later.");
    }
    $res = $db->single("SELECT COUNT(1) FROM mempool WHERE peer=:peer", [":peer" => $ip]);
    if ($res > $_config['peer_max_mempool']) {
        api_error("Too many transactions broadcasted from this peer");
    }


    // make sure the transaction is not already on the blockchain
    $res = $db->single("SELECT COUNT(1) FROM transactions WHERE id=:id", [":id" => $hash]);
    if ($res != 0) {
        api_err("The transaction is already in a block");
    }
    $acc = new Account();
    $src = $acc->get_address($data['public_key']);
    $val = $data['val'] ?? 0;
    $fee = $data['fee'] ?? 0;
    // make sure the sender has enough balance
    $balance = $db->single("SELECT balance FROM accounts WHERE id=:id", [":id" => $src]);
    if ($balance < $val + $fee) {
        api_err("Not enough funds");
    }

    // make sure the sender has enough pending balance
    $memspent = $db->single("SELECT SUM(val+fee) FROM mempool WHERE src=:src", [":src" => $src]);
    if ($balance - $memspent < $val + $fee) {
        api_err("Not enough funds (mempool)");
    }
  
    // add to mempool
    $trx->add_mempool($data, $ip);

    // rebroadcast the transaction to some peers unless the transaction is smaller than the average size of transactions in mempool - protect against garbage data flooding
    $res = $db->row("SELECT COUNT(1) as c, sum(val) as v FROM  mempool ", [":src" => $data['src']]);
    if ($res['c'] < $_config['max_mempool_rebroadcast'] && $res['v'] / $res['c'] < $data['val']) {
        $data['id']=escapeshellarg(san($data['id']));
        system("php propagate.php transaction '$data[id]'  > /dev/null 2>&1  &");
    }
    api_echo("transaction-ok");
} elseif ($q == "submitBlock") {
    // receive a  new block from a peer

    // if sanity sync, refuse all
    if ($_config['sanity_sync'] == 1) {
        _log('['.$ip."] Block rejected due to sanity sync");
        api_err("sanity-sync");
    }
    $sanity_lock=fopen(SANITY_LOCK_PATH,'w+');
    if(!flock($sanity_lock, LOCK_EX | LOCK_NB)){
        _log('['.$ip."] Block rejected due to sanity sync");
        api_err("sanity-sync");
    }
    // make sure sanity doesn't start while we add a new block and no other blocks are added at the same time
    flock($sanity_lock, LOCK_EX);

    $data['id'] = san($data['id']);
    $current = $block->current();
    // block already in the blockchain
    if ($current['id'] == $data['id']) {
        api_echo("block-ok");
    }
    if ($data['date'] > time() + 30) {
        api_err("block in the future");
    }

    if ($current['height'] == $data['height'] && $current['id'] != $data['id']) {
        // different forks, same height
        $accept_new = false;

            // convert the first 12 characters from hex to decimal and the block with the largest number wins
            $no1 = hexdec(substr(coin2hex($current['id']), 0, 12));
            $no2 = hexdec(substr(coin2hex($data['id']), 0, 12));
            if (gmp_cmp($no1, $no2) == 1) {
                $accept_new = true;
            }
        
        if ($accept_new) {
            // if the new block is accepted, run a microsanity to sync it
            _log('['.$ip."] Starting microsanity - $data[height]");
            $ip=escapeshellarg($ip);
            system("php sanity.php microsanity '$ip'  > /dev/null 2>&1  &");
            api_echo("microsanity");
        } else {
            _log('['.$ip."] suggesting reverse-microsanity - $data[height]");
            api_echo("reverse-microsanity"); // if it's not, suggest to the peer to get the block from us
        }
    }
    // if it's not the next block
    if ($current['height'] != $data['height'] - 1) {
        // if the height of the block submitted is lower than our current height, send them our current block
        if ($data['height'] < $current['height']) {
            $pr = $db->row("SELECT * FROM peers WHERE ip=:ip", [":ip" => $ip]);
            if (!$pr) {
                api_err("block-too-old");
            }
            $peer_host = escapeshellcmd(base58_encode($pr['hostname']));
            $pr['ip'] = escapeshellcmd(san_ip($pr['ip']));
            system("php propagate.php block current '$peer_host' '$pr[ip]'   > /dev/null 2>&1  &");
            _log('['.$ip."] block too old, sending our current block - $data[height]");

            api_err("block-too-old");
        }
        // if the block difference is bigger than 150, nothing should be done. They should sync via sanity
        if ($data['height'] - $current['height'] > 150) {
            _log('['.$ip."] block-out-of-sync - $data[height]");
            api_err("block-out-of-sync");
        }
        // request them to send us a microsync with the latest blocks
        _log('['.$ip."] requesting microsync - $current[height] - $data[height]");
        api_echo(["request" => "microsync", "height" => $current['height'], "block" => $current['id']]);
    }
    // check block data
    if (!$block->check($data)) {
        _log('['.$ip."] invalid block - $data[height]");
        api_err("invalid-block");
    }
    $b = $data;
    // add the block to the blockchain
    $res = $block->add(
        $b['height'],
        $b['public_key'],
        $b['nonce'],
        $b['data'],
        $b['date'],
        $b['signature'],
        $b['difficulty'],
        $b['reward_signature'],
        $b['argon']
    );

    if (!$res) {
        _log('['.$ip."] invalid block data - $data[height]");
        api_err("invalid-block-data");
    }

    _log('['.$ip."] block ok, repropagating - $data[height]");

    // send it to all our peers
    $data['id']=escapeshellcmd(san($data['id']));
    system("php propagate.php block '$data[id]' all all linear > /dev/null 2>&1  &");
    api_echo("block-ok");
} // return the current block, used in syncing
elseif ($q == "currentBlock") {
    $current = $block->current();
    api_echo($current);
} // return a specific block, used in syncing
elseif ($q == "getBlock") {
    $height = intval($data['height']);
    $export = $block->export("", $height);
    if (!$export) {
        api_err("invalid-block");
    }
    api_echo($export);
} elseif ($q == "getBlocks") {
// returns X block starting at height,  used in syncing

    $height = intval($data['height']);

    $r = $db->run(
        "SELECT id,height FROM blocks WHERE height>=:height ORDER by height ASC LIMIT 100",
        [":height" => $height]
    );
    foreach ($r as $x) {
        $blocks[$x['height']] = $block->export($x['id']);
    }
    api_echo($blocks);
} // returns a full list of unblacklisted peers in a random order
elseif ($q == "getPeers") {
    $peers = $db->run("SELECT ip,hostname FROM peers WHERE blacklisted<UNIX_TIMESTAMP() ORDER by RAND()");
    api_echo($peers);
} else {
    api_err("Invalid request");
}
