From 2d1f3e8e057961dc62577eaa744d6deed55f0443 Mon Sep 17 00:00:00 2001 From: Arionum Date: Mon, 8 Jan 2018 14:50:15 +0200 Subject: [PATCH] Initial commit --- api.php | 202 ++++++++++++++++++ include/account.inc.php | 138 ++++++++++++ include/block.inc.php | 405 ++++++++++++++++++++++++++++++++++++ include/config.inc.php | 17 ++ include/db.inc.php | 121 +++++++++++ include/functions.inc.php | 212 +++++++++++++++++++ include/init.inc.php | 71 +++++++ include/schema.inc.php | 142 +++++++++++++ include/transaction.inc.php | 205 ++++++++++++++++++ index.php | 37 ++++ mine.php | 68 ++++++ peer.php | 164 +++++++++++++++ propagate.php | 106 ++++++++++ sanity.php | 354 +++++++++++++++++++++++++++++++ tmp/db-update | 0 util.php | 113 ++++++++++ 16 files changed, 2355 insertions(+) create mode 100755 api.php create mode 100755 include/account.inc.php create mode 100755 include/block.inc.php create mode 100755 include/config.inc.php create mode 100755 include/db.inc.php create mode 100755 include/functions.inc.php create mode 100755 include/init.inc.php create mode 100755 include/schema.inc.php create mode 100755 include/transaction.inc.php create mode 100755 index.php create mode 100755 mine.php create mode 100755 peer.php create mode 100755 propagate.php create mode 100755 sanity.php create mode 100644 tmp/db-update create mode 100755 util.php diff --git a/api.php b/api.php new file mode 100755 index 0000000..94d4f67 --- /dev/null +++ b/api.php @@ -0,0 +1,202 @@ +get_address($public_key)); +} +elseif($q=="base58"){ + api_echo(base58_encode($data['data'])); +} +elseif($q=="getBalance"){ + $public_key=$data['public_key']; + $account=$data['account']; + if(!empty($public_key)&&strlen($public_key)<32) api_err("Invalid public key"); + if(!empty($public_key)) $account=$acc->get_address($public_key); + if(empty($account)) api_err("Invalid account id"); + $account=san($account); + api_echo($acc->balance($account)); +} +elseif($q=="getPendingBalance"){ + + $account=$data['account']; + if(empty($account)) api_err("Invalid account id"); + $account=san($account); + api_echo($acc->pending_balance($account)); +} +elseif($q=="getTransactions"){ + $account=san($data['account']); + $transactions=$acc->get_mempool_transactions($account); + $transactions=array_merge($transactions, $acc->get_transactions($account)); + api_echo($transactions); + +} +elseif($q=="getPublicKey"){ + $account=san($data['account']); + if(empty($account)) api_err("Invalid account id"); + $public_key=$acc->public_key($account); + if($public_key===false) api_err("No public key found for this account"); + else api_echo($public_key); + +} elseif($q=="getTransaction"){ + + $id=san($data['transaction']); + $res=$trx->get_transaction($id); + if($res===false) { + $res=$trx->get_mempool_transaction($id); + if($res===false) api_err("invalid transaction"); + } + api_Echo($res); + +} +elseif($q=="currentBlock"){ + $current=$block->current(); + api_echo($current); +} elseif($q=="version"){ + api_echo(VERSION); + +} elseif($q=="send"){ + + $acc = new Account; + $block = new Block; + + $trx = new Transaction; + + $dst=san($data['dst']); + + if(!$acc->valid($dst)) api_err("Invalid destination address"); + + + $public_key=san($data['public_key']); + if(!$acc->valid_key($public_key)) api_err("Invalid public key"); + $private_key=san($data['private_key']); + if(!$acc->valid_key($private_key)) api_err("Invalid private key"); + $signature=san($data['signature']); + if(!$acc->valid_key($signature)) api_err("Invalid signature"); + $date=$data['date']+0; + + if($date==0) $date=time(); + if($datetime()+86400) api_err("Invalid Date"); + $version=intval($data['version']); + $message=$data['message']; + if(strlen($message)>128) api_err("The message must be less than 128 chars"); + $val=$data['val']+0; + $fee=$val*0.0025; + if($fee<0.00000001) $fee=0.00000001; + + if($val<0.00000001) api_err("Invalid value"); + + + if($version<1) api_err("Invalid version"); + + $val=number_format($val,8,'.',''); + $fee=number_format($fee,8,'.',''); + + + if(empty($public_key)&&empty($private_key)) api_err("Either the private key or the public key must be sent"); + + + + if(empty($private_key)&&empty($signature)) api_err("Either the private_key or the signature must be sent"); + if(empty($public_key)) + { + + $pk=coin2pem($private_key,true); + $pkey=openssl_pkey_get_private($pk); + $pub = openssl_pkey_get_details($pkey); + $public_key= pem2coin($pub['key']); + + } + $transaction=array("val"=>$val, "fee"=>$fee, "dst"=>$dst, "public_key"=>$public_key,"date"=>$date, "version"=>$version,"message"=>$message, "signature"=>$signature); + + if(!empty($private_key)){ + + $signature=$trx->sign($transaction, $private_key); + $transaction['signature']=$signature; + + } + + + $hash=$trx->hash($transaction); + $transaction['id']=$hash; + + + + if(!$trx->check($transaction)) api_err("Transaction signature failed"); + + + + + $res=$db->single("SELECT COUNT(1) FROM mempool WHERE id=:id",array(":id"=>$hash)); + if($res!=0) api_err("The transaction is already in mempool"); + + $res=$db->single("SELECT COUNT(1) FROM transactions WHERE id=:id",array(":id"=>$hash)); + if($res!=0) api_err("The transaction is already in a block"); + + + + $src=$acc->get_address($public_key); + $transaction['src']=$src; + $balance=$db->single("SELECT balance FROM accounts WHERE id=:id",array(":id"=>$src)); + if($balance<$val+$fee) api_err("Not enough funds"); + + + $memspent=$db->single("SELECT SUM(val+fee) FROM mempool WHERE src=:src",array(":src"=>$src)); + if($balance-$memspent<$val+$fee) api_err("Not enough funds (mempool)"); + + + + $trx->add_mempool($transaction, "local"); + system("php propagate.php transaction $hash &>/dev/null &"); + api_echo($hash); +} elseif($q=="mempoolSize"){ + $res=$db->single("SELECT COUNT(1) FROM mempool"); + api_echo($res); + +} else { + api_err("Invalid request"); + } +?> \ No newline at end of file diff --git a/include/account.inc.php b/include/account.inc.php new file mode 100755 index 0000000..27db1a4 --- /dev/null +++ b/include/account.inc.php @@ -0,0 +1,138 @@ +get_address($public_key); + $bind=array(":id"=>$id, ":public_key"=>$public_key, ":block"=>$block,":public_key2"=>$public_key ); + + $db->run("INSERT INTO accounts SET id=:id, public_key=:public_key, block=:block, balance=0 ON DUPLICATE KEY UPDATE public_key=if(public_key='',:public_key2,public_key)",$bind); + } + public function add_id($id, $block){ + global $db; + $bind=array(":id"=>$id, ":block"=>$block); + $db->run("INSERT ignore INTO accounts SET id=:id, public_key='', block=:block, balance=0",$bind); + } + + public function get_address($hash){ + for($i=0;$i<9;$i++) $hash=hash('sha512',$hash, true); + + + + + return base58_encode($hash); + + + } + + public function check_signature($data, $signature, $public_key){ + + return ec_verify($data ,$signature, $public_key); + } + + + public function generate_account(){ + + $args = array( + "curve_name" => "secp256k1", + "private_key_type" => OPENSSL_KEYTYPE_EC, + ); + + + $key1 = openssl_pkey_new($args); + + openssl_pkey_export($key1, $pvkey); + + $private_key= pem2coin($pvkey); + + $pub = openssl_pkey_get_details($key1); + + $public_key= pem2coin($pub['key']); + + $address=$this->get_address($public_key); + return array("address"=>$address, "public_key"=>$public_key,"private_key"=>$private_key); + + + } + public function valid_key($id){ + $chars = str_split("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"); + for($i=0;$i128) return false; + $chars = str_split("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"); + for($i=0;$isingle("SELECT balance FROM accounts WHERE id=:id",array(":id"=>$id)); + if($res===false) $res="0.00000000"; + return $res; + } + public function pending_balance($id){ + global $db; + $res=$db->single("SELECT balance FROM accounts WHERE id=:id",array(":id"=>$id)); + if($res===false) $res="0.00000000"; + if($res=="0.00000000") return $res; + $mem=$db->single("SELECT SUM(val+fee) FROM mempool WHERE src=:id",array(":id"=>$id)); + $rez=$res-$mem; + return $rez; + + } + public function get_transactions($id){ + global $db; + $block=new Block; + $current=$block->current(); + $public_key=$this->public_key($id); + $res=$db->run("SELECT * FROM transactions WHERE dst=:dst or public_key=:src ORDER by height DESC LIMIT 100",array(":src"=>$public_key, ":dst"=>$id)); + + $transactions=array(); + foreach($res as $x){ + $trans=array("block"=>$x['block'],"height"=>$x['height'], "id"=>$x['id'],"dst"=>$x['dst'],"val"=>$x['val'],"fee"=>$x['fee'],"signature"=>$x['signature'], "message"=>$x['message'],"version"=>$x['version'],"date"=>$x['date'], "public_key"=>$x['public_key']); + $trans['src']=$this->get_address($x['public_key']); + $trans['confirmations']=$current['height']-$x['height']; + + if($x['version']==0) $trans['type']="mining"; + elseif($x['version']==1){ + if($x['dst']==$id) $trans['type']="credit"; + else $trans['type']="debit"; + } else { + $trans['type']="other"; + } + ksort($trans); + $transactions[]=$trans; + } + return $transactions; + } + public function get_mempool_transactions($id){ + global $db; + $transactions=array(); + $res=$db->run("SELECT * FROM mempool WHERE src=:src ORDER by height DESC LIMIT 100",array(":src"=>$id, ":dst"=>$id)); + foreach($res as $x){ + $trans=array("block"=>$x['block'],"height"=>$x['height'], "id"=>$x['id'],"src"=>$x['src'],"dst"=>$x['dst'],"val"=>$x['val'],"fee"=>$x['fee'],"signature"=>$x['signature'], "message"=>$x['message'],"version"=>$x['version'],"date"=>$x['date'], "public_key"=>$x['public_key']); + $trans['type']="mempool"; + $trans['confirmations']=-1; + ksort($trans); + $transactions[]=$trans; + } + return $transactions; + } + public function public_key($id){ + global $db; + $res=$db->single("SELECT public_key FROM accounts WHERE id=:id",array(":id"=>$id)); + return $res; + } +} + + + +?> diff --git a/include/block.inc.php b/include/block.inc.php new file mode 100755 index 0000000..35fd9ef --- /dev/null +++ b/include/block.inc.php @@ -0,0 +1,405 @@ +get_address($public_key); + + ksort($data); + + $hash=$this->hash($generator, $height, $date, $nonce, $data, $signature, $difficulty, $argon); + + + $json=json_encode($data); + + $info="{$generator}-{$height}-{$date}-{$nonce}-{$json}-{$difficulty}-{$argon}"; + + if(!$acc->check_signature($info,$signature,$public_key)) return false; + + + if(!$this->parse_block($hash,$height,$data, true)) return false; + + $db->exec("LOCK TABLES blocks WRITE, accounts WRITE, transactions WRITE, mempool WRITE"); + + $reward=$this->reward($height,$data); + + $msg=''; + + + $transaction=array("src"=>$generator, "dst"=>$generator, "val"=>$reward, "version"=>0, "date"=>$date, "message"=>$msg, "fee"=>"0.00000000","public_key"=>$public_key); + $transaction['signature']=$reward_signature; + $transaction['id']=$trx->hash($transaction); + $info=$transaction['val']."-".$transaction['fee']."-".$transaction['dst']."-".$transaction['message']."-".$transaction['version']."-".$transaction['public_key']."-".$transaction['date']; + + if(!$acc->check_signature($info,$reward_signature,$public_key)) return false; + + $db->beginTransaction(); + $total=count($data); + $bind=array(":id"=>$hash,":generator"=>$generator, ":signature"=>$signature, ":height"=>$height, ":date"=>$date, ":nonce"=>$nonce, ":difficulty"=>$difficulty,":argon"=>$argon, ":transactions"=>$total); + $res=$db->run("INSERT into blocks SET id=:id, generator=:generator, height=:height,`date`=:date,nonce=:nonce, signature=:signature, difficulty=:difficulty, argon=:argon, transactions=:transactions",$bind); + if($res!=1) { + $db->rollback(); + $db->exec("UNLOCK TABLES"); + return false; + } + + + $trx->add($hash, $height,$transaction); + + + $res=$this->parse_block($hash,$height,$data, false); + if($res==false) $db->rollback(); + else $db->commit(); + $db->exec("UNLOCK TABLES"); + return true; +} + +public function current(){ + global $db; + $current=$db->row("SELECT * FROM blocks ORDER by height DESC LIMIT 1"); + if(!$current){ + $this->genesis(); + return $this->current(true); + } + return $current; + +} + +public function prev(){ + global $db; + $current=$db->row("SELECT * FROM blocks ORDER by height DESC LIMIT 1,1"); + + return $current; + +} + +public function difficulty($height=0){ + global $db; + if($height==0){ + $current=$this->current(); + } else{ + $current=$this->get($height); + } + + + $height=$current['height']; + + $limit=20; + if($height<20) + $limit=$height-1; + + if($height<10) return $current['difficulty']; + + + $first=$db->row("SELECT `date` FROM blocks ORDER by height DESC LIMIT $limit,1"); + $time=$current['date']-$first['date']; + $result=ceil($time/$limit); + if($result>220){ + $dif= bcmul($current['difficulty'], 1.05); + } elseif($result<260){ + $dif= bcmul($current['difficulty'], 0.95); + } else { + $dif=$current['difficulty']; + } + if($dif<1000) $dif=1000; + if($dif>9223372036854775800) $dif=9223372036854775800; + + return $dif; +} + +public function max_transactions(){ + global $db; + $current=$this->current(); + $limit=$current['height']-100; + $avg=$db->single("SELECT AVG(transactions) FROM blocks WHERE height>:limit",array(":limit"=>$limit)); + if($avg<100) return 100; + return ceil($avg*1.1); +} + +public function reward($id,$data=array()){ + + + $reward=1000; + + $factor=floor($id/10800)/100; + $reward-=$reward*$factor; + if($reward<0) $reward=0; + + $fees=0; + if(count($data)>0){ + + foreach($data as $x){ + $fees+=$x['fee']; + } + } + return number_format($reward+$fees,8,'.',''); +} + + +public function check($data){ + if(strlen($data['argon'])<20) return false; + $acc=new Account; + if(!$acc->valid_key($data['public_key'])) return false; + if($data['difficulty']!=$this->difficulty()) return false; + if(!$this->mine($data['public_key'],$data['nonce'], $data['argon'])) return false; + + return true; + +} + + +public function forge($nonce, $argon, $public_key, $private_key){ + + if(!$this->mine($public_key,$nonce, $argon)) return false; + + $current=$this->current(); + $height=$current['height']+=1; + $date=time(); + if($date<=$current['date']) return 0; + + $txn=new Transaction; + $data=$txn->mempool($this->max_transactions()); + + + $difficulty=$this->difficulty(); + $acc=new Account; + $generator=$acc->get_address($public_key); + ksort($data); + $signature=$this->sign($generator, $height, $date, $nonce, $data, $private_key, $difficulty, $argon); + + // reward signature + $reward=$this->reward($height,$data); + $msg=''; + $transaction=array("src"=>$generator, "dst"=>$generator, "val"=>$reward, "version"=>0, "date"=>$date, "message"=>$msg, "fee"=>"0.00000000","public_key"=>$public_key); + ksort($transaction); + $reward_signature=$txn->sign($transaction, $private_key); + + + $res=$this->add($height, $public_key, $nonce, $data, $date, $signature, $difficulty, $reward_signature, $argon); + if(!$res) return false; + return true; +} + + +public function mine($public_key, $nonce, $argon, $difficulty=0, $current_id=0){ + + if($current_id===0){ + $current=$this->current(); + $current_id=$current['id']; + } + if($difficulty===0) $difficulty=$this->difficulty(); + + + $argon='$argon2i$v=19$m=16384,t=4,p=4'.$argon; + $base="$public_key-$nonce-".$current_id."-$difficulty"; + + + + + if(!password_verify($base,$argon)) { return false; } + + $hash=$base.$argon; + + for($i=0;$i<5;$i++) $hash=hash("sha512",$hash,true); + $hash=hash("sha512",$hash); + + $m=str_split($hash,2); + + $duration=hexdec($m[10]).hexdec($m[15]).hexdec($m[20]).hexdec($m[23]).hexdec($m[31]).hexdec($m[40]).hexdec($m[45]).hexdec($m[55]); + $duration=ltrim($duration, '0'); + $result=gmp_div($duration, $difficulty); + + if($result>0&&$result<=240) return true; + return false; + +} + + + + +public function parse_block($block, $height, $data, $test=true){ + global $db; + + if($data===false) return false; + $acc=new Account; + $trx=new Transaction; + if(count($data)==0) return true; + + $max=$this->max_transactions(); + + if(count($data)>$max) return false; + $balance=array(); + foreach($data as &$x){ + if(empty($x['src'])) $x['src']=$acc->get_address($x['public_key']); + + if(!$trx->check($x)) return false; + + $balance[$x['src']]+=$x['val']+$x['fee']; + + if($db->single("SELECT COUNT(1) FROM transactions WHERE id=:id",array(":id"=>$x['id']))>0) return false; //duplicate transaction + + } + + foreach($balance as $id=>$bal){ + $res=$db->single("SELECT COUNT(1) FROM accounts WHERE id=:id AND balance>=:balance",array(":id"=>$id, ":balance"=>$bal)); + if($res==0) return false; // not enough balance for the transactions + } + + if($test==false){ + + foreach($data as $d){ + $res=$trx->add($block, $height, $d); + if($res==false) return false; + } + } + + return true; +} + + +private function genesis(){ + global $db; + $signature='AN1rKvtLTWvZorbiiNk5TBYXLgxiLakra2byFef9qoz1bmRzhQheRtiWivfGSwP6r8qHJGrf8uBeKjNZP1GZvsdKUVVN2XQoL'; + $generator='2P67zUANj7NRKTruQ8nJRHNdKMroY6gLw4NjptTVmYk6Hh1QPYzzfEa9z4gv8qJhuhCNM8p9GDAEDqGUU1awaLW6'; + $public_key='PZ8Tyr4Nx8MHsRAGMpZmZ6TWY63dXWSCyjGMdVDanywM3CbqvswVqysqU8XS87FcjpqNijtpRSSQ36WexRDv3rJL5X8qpGvzvznuErSRMfb2G6aNoiaT3aEJ'; + $reward_signature='381yXZ3yq2AXHHdXfEm8TDHS4xJ6nkV4suXtUUvLjtvuyi17jCujtwcwXuYALM1F3Wiae2A4yJ6pXL1kTHJxZbrJNgtsKEsb'; + $argon='$M1ZpVzYzSUxYVFp6cXEwWA$CA6p39MVX7bvdXdIIRMnJuelqequanFfvcxzQjlmiik'; + + $difficulty="5555555555"; + $height=1; + $data=array(); + $date='1515324995'; + $nonce='4QRKTSJ+i9Gf9ubPo487eSi+eWOnIBt9w4Y+5J+qbh8='; + + + $res=$this->add($height, $public_key, $nonce, $data, $date, $signature, $difficulty, $reward_signature,$argon); + if(!$res) api_err("Could not add the genesis block."); +} +// delete last X blocks +public function pop($no=1){ + $current=$this->current(); + $this->delete($current['height']-$no+1); +} + +public function delete($height){ + if($height<2) $height=2; + global $db; + $trx=new Transaction; + + $r=$db->run("SELECT * FROM blocks WHERE height>=:height ORDER by height DESC",array(":height"=>$height)); + + if(count($r)==0) return; + $db->beginTransaction(); + $db->exec("LOCK TABLES blocks WRITE, accounts WRITE, transactions WRITE, mempool WRITE"); + foreach($r as $x){ + $res=$trx->reverse($x['id']); + if($res===false) { + $db->rollback(); + $db->exec("UNLOCK TABLES"); + return false; + } + $res=$db->run("DELETE FROM blocks WHERE id=:id",array(":id"=>$x['id'])); + if($res!=1){ + $db->rollback(); + $db->exec("UNLOCK TABLES"); + return false; + } + } + + $db->commit(); + $db->exec("UNLOCK TABLES"); + return true; +} + +public function delete_id($id){ + + global $db; + $trx=new Transaction; + + $x=$db->row("SELECT * FROM blocks WHERE id=:id",array(":id"=>$id)); + + if($x===false) return false; + $db->beginTransaction(); + $db->exec("LOCK TABLES blocks WRITE, accounts WRITE, transactions WRITE, mempool WRITE"); + + $res=$trx->reverse($x['id']); + if($res===false) { + $db->rollback(); + $db->exec("UNLOCK TABLES"); + return false; + } + $res=$db->run("DELETE FROM blocks WHERE id=:id",array(":id"=>$x['id'])); + if($res!=1){ + $db->rollback(); + $db->exec("UNLOCK TABLES"); + return false; + } + + $db->commit(); + $db->exec("UNLOCK TABLES"); + return true; +} + +public function sign($generator, $height, $date, $nonce, $data, $key, $difficulty, $argon){ + + $json=json_encode($data); + $info="{$generator}-{$height}-{$date}-{$nonce}-{$json}-{$difficulty}-{$argon}"; + + $signature=ec_sign($info,$key); + return $signature; + +} + +public function hash($public_key, $height, $date, $nonce, $data, $signature, $difficulty, $argon){ + $json=json_encode($data); + $hash= hash("sha512", "{$public_key}-{$height}-{$date}-{$nonce}-{$json}-{$signature}-{$difficulty}-{$argon}"); + return hex2coin($hash); +} + + +public function export($id="",$height=""){ + if(empty($id)&&empty($height)) return false; + + global $db; + $trx=new Transaction; + if(!empty($height)) $block=$db->row("SELECT * FROM blocks WHERE height=:height",array(":height"=>$height)); + else $block=$db->row("SELECT * FROM blocks WHERE id=:id",array(":id"=>$id)); + + if(!$block) return false; + $r=$db->run("SELECT * FROM transactions WHERE version>0 AND block=:block",array(":block"=>$block['id'])); + $transactions=array(); + foreach($r as $x){ + $trans=array("id"=>$x['id'],"dst"=>$x['dst'],"val"=>$x['val'],"fee"=>$x['fee'],"signature"=>$x['signature'], "message"=>$x['message'],"version"=>$x['version'],"date"=>$x['date'], "public_key"=>$x['public_key']); + ksort($trans); + $transactions[$x['id']]=$trans; + } + ksort($transactions); + $block['data']=$transactions; + + $gen=$db->row("SELECT public_key, signature FROM transactions WHERE version=0 AND block=:block",array(":block"=>$block['id'])); + $block['public_key']=$gen['public_key']; + $block['reward_signature']=$gen['signature']; + return $block; + +} + +public function get($height){ + global $db; + if(empty($height)) return false; + $block=$db->row("SELECT * FROM blocks WHERE height=:height",array(":height"=>$height)); + return $block; +} + +} +?> diff --git a/include/config.inc.php b/include/config.inc.php new file mode 100755 index 0000000..73e77f5 --- /dev/null +++ b/include/config.inc.php @@ -0,0 +1,17 @@ + diff --git a/include/db.inc.php b/include/db.inc.php new file mode 100755 index 0000000..0bbcb66 --- /dev/null +++ b/include/db.inc.php @@ -0,0 +1,121 @@ + true, + PDO::ATTR_EMULATE_PREPARES => false, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ); + $this->debugger=$debug_level; + try { + parent::__construct($dsn, $user, $passwd, $options); + } catch (PDOException $e) { + $this->error = $e->getMessage(); + die("Could not connect to the DB"); + } + } + + private function debug() { + if(!$this->debugger) return; + $error = array("Error" => $this->error); + if(!empty($this->sql)) + $error["SQL Statement"] = $this->sql; + if(!empty($this->bind)) + $error["Bind Parameters"] = trim(print_r($this->bind, true)); + + $backtrace = debug_backtrace(); + if(!empty($backtrace)) { + foreach($backtrace as $info) { + if($info["file"] != __FILE__) + $error["Backtrace"] = $info["file"] . " at line " . $info["line"]; + } + } + $msg = ""; + $msg .= "SQL Error\n" . str_repeat("-", 50); + foreach($error as $key => $val) + $msg .= "\n\n$key:\n$val"; + + if($this->debugger){ + + echo nl2br($msg); + + } + } + + private function cleanup($bind,$sql="") { + if(!is_array($bind)) { + if(!empty($bind)) + $bind = array($bind); + else + $bind = array(); + } + + foreach($bind as $key=>$val){ + if(str_replace($key,"",$sql)==$sql) unset($bind[$key]); + } + return $bind; + } + + + + + public function single($sql,$bind="") { + $this->sql = trim($sql); + $this->bind = $this->cleanup($bind,$sql); + $this->error = ""; + try { + $pdostmt = $this->prepare($this->sql); + if($pdostmt->execute($this->bind) !== false) { + return $pdostmt->fetchColumn(); + } + } catch (PDOException $e) { + $this->error = $e->getMessage(); + $this->debug(); + return false; + } + } + + + public function run($sql, $bind="") { + $this->sql = trim($sql); + $this->bind = $this->cleanup($bind,$sql); + $this->error = ""; + + try { + $pdostmt = $this->prepare($this->sql); + if($pdostmt->execute($this->bind) !== false) { + if(preg_match("/^(" . implode("|", array("select", "describe", "pragma")) . ") /i", $this->sql)) + return $pdostmt->fetchAll(PDO::FETCH_ASSOC); + elseif(preg_match("/^(" . implode("|", array("delete", "insert", "update")) . ") /i", $this->sql)) + return $pdostmt->rowCount(); + } + } catch (PDOException $e) { + $this->error = $e->getMessage(); + $this->debug(); + return false; + } + } + + public function row($sql,$bind=""){ + $query=$this->run($sql,$bind); + if(count($query)==0) return false; + if(count($query)>1) return $query; + if(count($query)==1){ + foreach($query as $row) $result=$row; + return $result; + } + } + + + +} + +?> diff --git a/include/functions.inc.php b/include/functions.inc.php new file mode 100755 index 0000000..bed18af --- /dev/null +++ b/include/functions.inc.php @@ -0,0 +1,212 @@ +"error","data"=>$data, "coin"=>$_config['coin'])); + exit; +} +function api_echo($data){ + global $_config; + echo json_encode(array("status"=>"ok","data"=>$data, "coin"=>$_config['coin'])); + exit; +} + +function _log($data){ + $date=date("[Y-m-d H:s:]"); + $trace=debug_backtrace(); + $location=$trace[1]['class'].'->'.$trace[1]['function'].'()'; + //echo "$date [$location] $data\n"; +} + +function pem2hex ($data) { + $data=str_replace("-----BEGIN PUBLIC KEY-----","",$data); + $data=str_replace("-----END PUBLIC KEY-----","",$data); + $data=str_replace("-----BEGIN EC PRIVATE KEY-----","",$data); + $data=str_replace("-----END EC PRIVATE KEY-----","",$data); + $data=str_replace("\n","",$data); + $data=base64_decode($data); + $data=bin2hex($data); + return $data; +} + +function hex2pem ($data, $is_private_key=false) { + $data=hex2bin($data); + $data=base64_encode($data); + if($is_private_key) return "-----BEGIN EC PRIVATE KEY-----\n".$data."\n-----END EC PRIVATE KEY-----"; + return "-----BEGIN PUBLIC KEY-----\n".$data."\n-----END PUBLIC KEY-----"; +} + + + + //all credits for this base58 functions should go to tuupola / https://github.com/tuupola/base58/ + function baseConvert(array $source, $source_base, $target_base) + { + $result = []; + while ($count = count($source)) { + $quotient = []; + $remainder = 0; + for ($i = 0; $i !== $count; $i++) { + $accumulator = $source[$i] + $remainder * $source_base; + $digit = (integer) ($accumulator / $target_base); + $remainder = $accumulator % $target_base; + if (count($quotient) || $digit) { + array_push($quotient, $digit); + }; + } + array_unshift($result, $remainder); + $source = $quotient; + } + return $result; + } + function base58_encode($data) + { + if (is_integer($data)) { + $data = [$data]; + } else { + $data = str_split($data); + $data = array_map(function ($character) { + return ord($character); + }, $data); + } + + + $converted = baseConvert($data, 256, 58); + + return implode("", array_map(function ($index) { + $chars="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + return $chars[$index]; + }, $converted)); + } + function base58_decode($data, $integer = false) + { + $data = str_split($data); + $data = array_map(function ($character) { + $chars="123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + return strpos($chars, $character); + }, $data); + /* Return as integer when requested. */ + if ($integer) { + $converted = baseConvert($data, 58, 10); + return (integer) implode("", $converted); + } + $converted = baseConvert($data, 58, 256); + return implode("", array_map(function ($ascii) { + return chr($ascii); + }, $converted)); + } + + + + +function pem2coin ($data) { + $data=str_replace("-----BEGIN PUBLIC KEY-----","",$data); + $data=str_replace("-----END PUBLIC KEY-----","",$data); + $data=str_replace("-----BEGIN EC PRIVATE KEY-----","",$data); + $data=str_replace("-----END EC PRIVATE KEY-----","",$data); + $data=str_replace("\n","",$data); + $data=base64_decode($data); + + + return base58_encode($data); + +} + +function coin2pem ($data, $is_private_key=false) { + + + + $data=base58_decode($data); + $data=base64_encode($data); + + $dat=str_split($data,64); + $data=implode("\n",$dat); + + if($is_private_key) return "-----BEGIN EC PRIVATE KEY-----\n".$data."\n-----END EC PRIVATE KEY-----\n"; + return "-----BEGIN PUBLIC KEY-----\n".$data."\n-----END PUBLIC KEY-----\n"; +} + + +function ec_sign($data, $key){ + + $private_key=coin2pem($key,true); + + + $pkey=openssl_pkey_get_private($private_key); + + $k=openssl_pkey_get_details($pkey); + + + openssl_sign($data,$signature,$pkey,OPENSSL_ALGO_SHA256); + + + + return base58_encode($signature); + +} + + +function ec_verify($data, $signature, $key){ + + + + $public_key=coin2pem($key); + + $signature=base58_decode($signature); + + $pkey=openssl_pkey_get_public($public_key); + + $res=openssl_verify($data,$signature,$pkey,OPENSSL_ALGO_SHA256); + + + if($res===1) return true; + return false; +} + + +function peer_post($url, $data=array()){ + global $_config; + $postdata = http_build_query( + array( + 'data' => json_encode($data), + "coin"=>$_config['coin'] + ) + ); + + $opts = array('http' => + array( + 'timeout' => "60", + 'method' => 'POST', + 'header' => 'Content-type: application/x-www-form-urlencoded', + 'content' => $postdata + ) + ); + + $context = stream_context_create($opts); + + $result = file_get_contents($url, false, $context); + + $res=json_decode($result,true); + if($res['status']!="ok"||$res['coin']!=$_config['coin']) return false; + return $res['data']; +} + + +function hex2coin($hex){ + + $data=hex2bin($hex); + return base58_encode($data); +} +function coin2hex($data){ + + $bin= base58_decode($data); + return bin2hex($bin); +} +?> \ No newline at end of file diff --git a/include/init.inc.php b/include/init.inc.php new file mode 100755 index 0000000..5b66b6f --- /dev/null +++ b/include/init.inc.php @@ -0,0 +1,71 @@ +run("SELECT cfg, val FROM config"); +foreach($query as $res){ + $_config[$res['cfg']]=trim($res['val']); +} + + + + + +if($_config['maintenance']==1) api_err("under-maintenance"); + + +if(file_exists("tmp/db-update")){ + + $res=unlink("tmp/db-update"); + if($res){ + require_once("include/schema.inc.php"); + exit; + } +} + +if($_config['dbversion']<2) exit; + +if($_config['testnet']==true) $_config['coin'].="-testnet"; + +$hostname=(!empty($_SERVER['HTTPS'])?'https':'http')."://".$_SERVER['HTTP_HOST']; +if($_SERVER['SERVER_PORT']!=80&&$_SERVER['SERVER_PORT']!=443) $hostname.=":".$_SERVER['SERVER_PORT']; + +if($hostname!=$_config['hostname']&&$_SERVER['HTTP_HOST']!="localhost"&&$_SERVER['HTTP_HOST']!="127.0.0.1"&&$_SERVER['hostname']!='::1'&&php_sapi_name() !== 'cli'){ + $db->run("UPDATE config SET val=:hostname WHERE cfg='hostname' LIMIT 1",array(":hostname"=>$hostname)); + $_config['hostname']=$hostname; +} +if(empty($_config['hostname'])||$_config['hostname']=="http://"||$_config['hostname']=="https://") api_err("Invalid hostname"); + + + $t=time(); + if($t-$_config['sanity_last']>$_config['sanity_interval']&& php_sapi_name() !== 'cli') system("php sanity.php &>>/dev/null &"); + + +?> diff --git a/include/schema.inc.php b/include/schema.inc.php new file mode 100755 index 0000000..2b534e0 --- /dev/null +++ b/include/schema.inc.php @@ -0,0 +1,142 @@ +beginTransaction(); +if($dbversion==0){ + $db->run(" + CREATE TABLE `accounts` ( + `id` varbinary(128) NOT NULL, + `public_key` varbinary(1024) NOT NULL, + `block` varbinary(128) NOT NULL, + `balance` decimal(20,8) NOT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;"); + + $db->run("CREATE TABLE `blocks` ( + `id` varbinary(128) NOT NULL, + `generator` varbinary(128) NOT NULL, + `height` int(11) NOT NULL, + `date` int(11) NOT NULL, + `nonce` varbinary(128) NOT NULL, + `signature` varbinary(256) NOT NULL, + `difficulty` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `argon` varbinary(128) NOT NULL, + `transactions` INT NOT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"); + + $db->run("CREATE TABLE `config` ( + `cfg` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `val` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"); + + + $db->run("INSERT INTO `config` (`cfg`, `val`) VALUES + ('hostname', '');"); + + $db->run("INSERT INTO `config` (`cfg`, `val`) VALUES + ('dbversion', '1');"); + + $db->run("CREATE TABLE `mempool` ( + `id` varbinary(128) NOT NULL, + `height` int(11) NOT NULL, + `src` varbinary(128) NOT NULL, + `dst` varbinary(128) NOT NULL, + `val` decimal(20,8) NOT NULL, + `fee` decimal(20,8) NOT NULL, + `signature` varbinary(256) NOT NULL, + `version` tinyint(4) NOT NULL, + `message` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT '', + `public_key` varbinary(1024) NOT NULL, + `date` bigint(20) NOT NULL, + `peer` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"); + + $db->run("CREATE TABLE `peers` ( + `id` int(11) NOT NULL, + `hostname` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `blacklisted` int(11) NOT NULL DEFAULT 0, + `ping` int(11) NOT NULL, + `reserve` tinyint(4) NOT NULL DEFAULT 1, + `ip` varchar(45) NOT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"); + + + $db->run("CREATE TABLE `transactions` ( + `id` varbinary(128) NOT NULL, + `block` varbinary(128) NOT NULL, + `height` int(11) NOT NULL, + `dst` varbinary(128) NOT NULL, + `val` decimal(20,8) NOT NULL, + `fee` decimal(20,8) NOT NULL, + `signature` varbinary(256) NOT NULL, + `version` tinyint(4) NOT NULL, + `message` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT '', + `date` int(11) NOT NULL, + `public_key` varbinary(1024) NOT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"); + + $db->run("ALTER TABLE `peers` + ADD PRIMARY KEY (`id`);"); + $db->run("ALTER TABLE `peers` + MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;"); + + $db->run("ALTER TABLE `accounts` + ADD PRIMARY KEY (`id`), + ADD KEY `accounts` (`block`);"); + + $db->run("ALTER TABLE `blocks` + ADD PRIMARY KEY (`id`), + ADD UNIQUE KEY `height` (`height`);"); + + $db->run("ALTER TABLE `config` ADD PRIMARY KEY (`cfg`);"); + + $db->run("ALTER TABLE `mempool` + ADD PRIMARY KEY (`id`), + ADD KEY `height` (`height`);"); + + $db->run("ALTER TABLE `peers` + ADD UNIQUE KEY `hostname` (`hostname`), + ADD UNIQUE KEY `ip` (`ip`), + ADD KEY `blacklisted` (`blacklisted`), + ADD KEY `ping` (`ping`), + ADD KEY `reserve` (`reserve`);"); + + $db->run("ALTER TABLE `transactions` + ADD PRIMARY KEY (`id`), + ADD KEY `block_id` (`block`);"); + + $db->run("ALTER TABLE `accounts` + ADD CONSTRAINT `accounts` FOREIGN KEY (`block`) REFERENCES `blocks` (`id`) ON DELETE CASCADE;"); + + $db->run("ALTER TABLE `transactions` + ADD CONSTRAINT `block_id` FOREIGN KEY (`block`) REFERENCES `blocks` (`id`) ON DELETE CASCADE;"); + + $dbversion++; +} +if($dbversion==1){ + $db->run("INSERT INTO `config` (`cfg`, `val`) VALUES ('sanity_last', '0');"); + $dbversion++; +} +if($dbversion==2){ + $db->run("INSERT INTO `config` (`cfg`, `val`) VALUES ('sanity_sync', '0');"); + $dbversion++; +} +if($dbversion==3){ + $dbversion++; +} + +if($dbversion==4){ + $db->run("ALTER TABLE `mempool` ADD INDEX(`src`);"); + $db->run("ALTER TABLE `mempool` ADD INDEX(`peer`); "); + $db->run("ALTER TABLE `mempool` ADD INDEX(`val`); "); + $dbversion++; +} +if($dbversion==5){ + $db->run("ALTER TABLE `peers` ADD `fails` TINYINT NOT NULL DEFAULT '0' AFTER `ip`; "); + $dbversion++; +} +if($dbversion!=$_config['dbversion']) $db->run("UPDATE config SET val=:val WHERE cfg='dbversion'",array(":val"=>$dbversion)); +$db->commit(); + + +?> \ No newline at end of file diff --git a/include/transaction.inc.php b/include/transaction.inc.php new file mode 100755 index 0000000..c5b3f79 --- /dev/null +++ b/include/transaction.inc.php @@ -0,0 +1,205 @@ +run("SELECT * FROM transactions WHERE block=:block",array(":block"=>$block)); + foreach($r as $x){ + if(empty($x['src'])) $x['src']=$acc->get_address($x['public_key']); + $db->run("UPDATE accounts SET balance=balance-:val WHERE id=:id",array(":id"=>$x['dst'], ":val"=>$x['val'])); + if($x['version']>0) $db->run("UPDATE accounts SET balance=balance+:val WHERE id=:id",array(":id"=>$x['src'], ":val"=>$x['val']+$x['fee'])); + + if($x['version']>0) $this->add_mempool($x); + $res= $db->run("DELETE FROM transactions WHERE id=:id",array(":id"=>$x['id'])); + if($res!=1) return false; + } + } + + public function clean_mempool(){ + global $db; + $block= new Block; + $current=$block->current(); + $height=$current['height']; + $limit=$height-1000; + $db->run("DELETE FROM mempool WHERE height<:limit",array(":limit"=>$limit)); + } + + public function mempool($max){ + global $db; + $block=new Block; + $current=$block->current(); + $height=$current['height']+1; + $r=$db->run("SELECT * FROM mempool WHERE height<=:height ORDER by val/fee DESC LIMIT :max",array(":height"=>$height, ":max"=>$max+50)); + $transactions=array(); + if(count($r)>0){ + $i=0; + $balance=array(); + foreach($r as $x){ + $trans=array("id"=>$x['id'],"dst"=>$x['dst'],"val"=>$x['val'],"fee"=>$x['fee'],"signature"=>$x['signature'], "message"=>$x['message'],"version"=>$x['version'],"date"=>$x['date'], "public_key"=>$x['public_key']); + + if($i>=$max) break; + + if(empty($x['public_key'])){ + _log("$x[id] - Transaction has empty public_key"); + continue; + } + if(empty($x['src'])){ + _log("$x[id] - Transaction has empty src"); + continue; + } + if(!$this->check($trans)){ + var_dump($trans); + _log("$x[id] - Transaction Check Failed"); + continue; + } + + $balance[$x['src']]+=$x['val']+$x['fee']; + if($db->single("SELECT COUNT(1) FROM transactions WHERE id=:id",array(":id"=>$x['id']))>0) { + _log("$x[id] - Duplicate transaction"); + continue; //duplicate transaction + } + + $res=$db->single("SELECT COUNT(1) FROM accounts WHERE id=:id AND balance>=:balance",array(":id"=>$x['src'], ":balance"=>$balance[$x['src']])); + + if($res==0) { + _log("$x[id] - Not enough funds in balance"); + continue; // not enough balance for the transactions + } + $i++; + ksort($trans); + $transactions[$x['id']]=$trans; + } + } + ksort($transactions); + + return $transactions; + } + + public function add_mempool($x, $peer=""){ + global $db; + $block= new Block; + $current=$block->current(); + $height=$current['height']; + $x['id']=san($x['id']); + $bind=array(":peer"=>$peer, ":id"=>$x['id'],"public_key"=>$x['public_key'], ":height"=>$height, ":src"=>$x['src'],":dst"=>$x['dst'],":val"=>$x['val'], ":fee"=>$x['fee'],":signature"=>$x['signature'], ":version"=>$x['version'],":date"=>$x['date'], ":message"=>$x['message']); + $db->run("INSERT into mempool SET peer=:peer, id=:id, public_key=:public_key, height=:height, src=:src, dst=:dst, val=:val, fee=:fee, signature=:signature, version=:version, message=:message, `date`=:date",$bind); + return true; + + + } + + public function add($block,$height, $x){ + global $db; + $acc= new Account; + $acc->add($x['public_key'], $block); + $acc->add_id($x['dst'],$block); + $x['id']=san($x['id']); + $bind=array(":id"=>$x['id'], ":public_key"=>$x['public_key'],":height"=>$height, ":block"=>$block, ":dst"=>$x['dst'],":val"=>$x['val'], ":fee"=>$x['fee'],":signature"=>$x['signature'], ":version"=>$x['version'],":date"=>$x['date'], ":message"=>$x['message']); + $res=$db->run("INSERT into transactions SET id=:id, public_key=:public_key, block=:block, height=:height, dst=:dst, val=:val, fee=:fee, signature=:signature, version=:version, message=:message, `date`=:date",$bind); + if($res!=1) return false; + $db->run("UPDATE accounts SET balance=balance+:val WHERE id=:id",array(":id"=>$x['dst'], ":val"=>$x['val'])); + if($x['version']>0) $db->run("UPDATE accounts SET balance=balance-:val WHERE id=:id",array(":id"=>$x['src'], ":val"=>$x['val']+$x['fee'])); + $db->run("DELETE FROM mempool WHERE id=:id",array(":id"=>$x['id'])); + return true; + + + } + + public function hash($x){ + $info=$x['val']."-".$x['fee']."-".$x['dst']."-".$x['message']."-".$x['version']."-".$x['public_key']."-".$x['date']."-".$x['signature']; + $hash= hash("sha512",$info); + return hex2coin($hash); + } + + + public function check($x){ + + $acc= new Account; + $info=$x['val']."-".$x['fee']."-".$x['dst']."-".$x['message']."-".$x['version']."-".$x['public_key']."-".$x['date']; + + if($x['val']<0){ _log("$x[id] - Value below 0"); return false; } + if($x['fee']<0) { _log("$x[id] - Fee below 0"); return false; } + + $fee=$x['val']*0.0025; + if($fee<0.00000001) $fee=0.00000001; + if($fee!=$x['fee']) { _log("$x[id] - Fee not 0.25%"); return false; } + + if(!$acc->valid($x['dst'])) { _log("$x[id] - Invalid destination address"); return false; } + + if($x['version']<1) { _log("$x[id] - Invalid version <1"); return false; } + //if($x['version']>1) { _log("$x[id] - Invalid version >1"); return false; } + + if(strlen($x['public_key'])<15) { _log("$x[id] - Invalid public key size"); return false; } + if($x['date']<1511725068) { _log("$x[id] - Date before genesis"); return false; } + if($x['date']>time()+86400) { _log("$x[id] - Date in the future"); return false; } + + $id=$this->hash($x); + if($x['id']!=$id) { _log("$x[id] - Invalid hash"); return false; } + + + if(!$acc->check_signature($info, $x['signature'], $x['public_key'])) { _log("$x[id] - Invalid signature"); return false; } + + return true; + } + public function sign($x, $private_key){ + $info=$x['val']."-".$x['fee']."-".$x['dst']."-".$x['message']."-".$x['version']."-".$x['public_key']."-".$x['date']; + $signature=ec_sign($info,$private_key); + + return $signature; + + } + + + public function export($id){ + global $db; + $r=$db->row("SELECT * FROM mempool WHERE id=:id",array(":id"=>$id)); + //unset($r['peer']); + return $r; + + } + public function get_transaction($id){ + global $db; + $block=new Block; + $current=$block->current(); + $acc=new Account; + $x=$db->row("SELECT * FROM transactions WHERE id=:id",array(":id"=>$id)); + + if(!$x) return false; + $trans=array("block"=>$x['block'],"height"=>$x['height'], "id"=>$x['id'],"dst"=>$x['dst'],"val"=>$x['val'],"fee"=>$x['fee'],"signature"=>$x['signature'], "message"=>$x['message'],"version"=>$x['version'],"date"=>$x['date'], "public_key"=>$x['public_key']); + $trans['src']=$acc->get_address($x['public_key']); + $trans['confirmations']=$current['height']-$x['height']; + + if($x['version']==0) $trans['type']="mining"; + elseif($x['version']==1){ + if($x['dst']==$id) $trans['type']="credit"; + else $trans['type']="debit"; + } else { + $trans['type']="other"; + } + ksort($trans); + return $trans; + + } + + public function get_mempool_transaction($id){ + global $db; + $x=$db->row("SELECT * FROM mempool WHERE id=:id",array(":id"=>$id)); + if(!$x) return false; + $trans=array("block"=>$x['block'],"height"=>$x['height'], "id"=>$x['id'],"dst"=>$x['dst'],"val"=>$x['val'],"fee"=>$x['fee'],"signature"=>$x['signature'], "message"=>$x['message'],"version"=>$x['version'],"date"=>$x['date'], "public_key"=>$x['public_key']); + $trans['src']=$x['src']; + + $trans['type']="mempool"; + $trans['confirmations']=-1; + ksort($trans); + return $trans; + + } + +} + + + +?> diff --git a/index.php b/index.php new file mode 100755 index 0000000..5d0f499 --- /dev/null +++ b/index.php @@ -0,0 +1,37 @@ +current(); + +echo "

Arionum Node

"; +echo "System check complete.

Current block: $current[height]"; + + + +?> diff --git a/mine.php b/mine.php new file mode 100755 index 0000000..7bcd1d8 --- /dev/null +++ b/mine.php @@ -0,0 +1,68 @@ +difficulty(); + $current=$block->current(); + api_echo(array("difficulty"=>$diff, "block"=>$current['id'])); + exit; +} elseif($q=="submitNonce"){ + if($_config['sanity_sync']==1) api_err("sanity-sync"); + $nonce = san($_POST['nonce']); + $argon=$_POST['argon']; + $public_key=san($_POST['public_key']); + $private_key=san($_POST['private_key']); + + $result=$block->mine($public_key, $nonce, $argon); + + if($result) { + + $res=$block->forge($nonce,$argon, $public_key, $private_key); + + + + + + if($res){ + $current=$block->current(); + system("php propagate.php block $current[id] &>/dev/null &"); + api_echo("accepted"); + } + } + api_err("rejected"); +} else { + api_err("invalid command"); +} + +?> diff --git a/peer.php b/peer.php new file mode 100755 index 0000000..8d68046 --- /dev/null +++ b/peer.php @@ -0,0 +1,164 @@ +single("SELECT COUNT(1) FROM peers WHERE hostname=:hostname AND ip=:ip",array(":hostname"=>$hostname,":ip"=>$ip)); + + if($res==1) api_echo("peer-ok-already"); + + $res=$db->single("SELECT COUNT(1) FROM peers WHERE blacklistedUNIX_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",array(":ip"=>$ip, ":hostname2"=>$hostname,":hostname"=>$hostname, ":reserve"=>$reserve)); + + $res=peer_post($hostname."/peer.php?q=peer",array("hostname"=>$_config['hostname'])); + if($res!==false) api_echo("re-peer-ok"); + else api_err("re-peer failed - $result"); +} +elseif($q=="ping"){ + api_echo("pong"); +} + + +elseif($q=="submitTransaction"){ + if($_config['sanity_sync']==1) api_err("sanity-sync"); + + $data['id']=san($data['id']); + + if(!$trx->check($data)) api_err("Invalid transaction"); + $hash=$data['id']; + $res=$db->single("SELECT COUNT(1) FROM mempool WHERE id=:id",array(":id"=>$hash)); + if($res!=0) api_err("The transaction is already in mempool"); + + $res=$db->single("SELECT COUNT(1) FROM mempool WHERE src=:src",array(":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",array(":peer"=>$_SERVER['REMOTE_ADDR'])); + if($res>$_config['peer_max_mempool']) api_error("Too many transactions broadcasted from this peer"); + + + + $res=$db->single("SELECT COUNT(1) FROM transactions WHERE id=:id",array(":id"=>$hash)); + if($res!=0) api_err("The transaction is already in a block"); + $acc=new Account; + $src=$acc->get_address($data['public_key']); + + $balance=$db->single("SELECT balance FROM accounts WHERE id=:id",array(":id"=>$src)); + if($balance<$val+$fee) api_err("Not enough funds"); + + + $memspent=$db->single("SELECT SUM(val+fee) FROM mempool WHERE src=:src",array(":src"=>$src)); + if($balance-$memspent<$val+$fee) api_err("Not enough funds (mempool)"); + $trx->add_mempool($data, $_SERVER['REMOTE_ADDR']); + + $res=$db->row("SELECT COUNT(1) as c, sum(val) as v FROM mempool ",array(":src"=>$data['src'])); + if($res['c']<$_config['max_mempool_rebroadcast']&&$res['v']/$res['c']<$data['val']) system("php propagate.php transaction $data[id] &>/dev/null &"); + api_echo("transaction-ok"); +} +elseif($q=="submitBlock"){ + if($_config['sanity_sync']==1) api_err("sanity-sync"); + $data['id']=san($data['id']); + $current=$block->current(); + if($current['id']==$data['id']) api_echo("block-ok"); + if($current['height']==$data['height']&&$current['id']!=$data['id']){ + $accept_new=false; + if($current['transactions']<$data['transactions']){ + $accept_new=true; + } elseif($current['transactions']==$data['transactions']) { + $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){ + system("php sanity.php microsanity $ip &>/dev/null &"); + api_echo("microsanity"); + } + } + + if($current['height']!=$data['height']-1) { + if($data['height']<$current['height']) api_err("block-too-old"); + if($data['height']-$current['height']>30) api_err("block-out-of-sync"); + api_echo(array("request"=>"microsync","height"=>$current['height'], "block"=>$current['id'])); + + } + if(!$block->check($data)) api_err("invalid-block"); + $b=$data; + $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) api_err("invalid-block-data"); + api_echo("block-ok"); + + system("php propagate.php block $data[id] &>/dev/null &"); + +} + +elseif($q=="currentBlock"){ + $current=$block->current(); + api_echo($current); +} +elseif($q=="getBlock"){ + $height=intval($data['height']); + + $export=$block->export("",$height); + if(!$export) api_err("invalid-block"); + api_echo($export); + } + elseif($q=="getBlocks"){ + $height=intval($data['height']); + + $r=$db->run("SELECT id,height FROM blocks WHERE height>=:height ORDER by height ASC LIMIT 100",array(":height"=>$height)); + foreach($r as $x){ + $blocks[$x['height']]=$block->export($x['id']); + } + api_echo($blocks); + + } + + elseif($q=="getPeers"){ + $peers=$db->run("SELECT ip,hostname FROM peers ORDER by RAND()"); + api_echo($peers); + } else { + api_err("Invalid request"); + } + +?> \ No newline at end of file diff --git a/propagate.php b/propagate.php new file mode 100755 index 0000000..515c3ed --- /dev/null +++ b/propagate.php @@ -0,0 +1,106 @@ +export($id); + + if($data===false||empty($data)) die("Could not export block"); + $data=json_encode($data); + // cache it to reduce the load + $res=file_put_contents("tmp/$id",$data); + if($res===false) die("Could not write the cachce file"); + $r=$db->run("SELECT * FROM peers WHERE blacklisted < UNIX_TIMESTAMP() AND reserve=0"); + foreach($r as $x) { + $host=base58_encode($x['hostname']); + system("php propagate.php $type $id $host &>/dev/null &"); + } + exit; +} + + + + +if($type=="block"){ + + + $data=file_get_contents("tmp/$id"); + if(empty($data)) { echo "Invalid Block data"; exit; } + $data=json_decode($data,true); + $hostname=base58_decode($peer); + + echo "Peer response - $hostname:\n"; + $response= peer_post($hostname."/peer.php?q=submitBlock",$data); + if($response=="block-ok") { echo "Block $i accepted. Exiting.\n"; exit;} + elseif($response['request']=="microsync"){ + echo "Microsync request\n"; + $height=intval($response['height']); + $bl=san($response['block']); + $current=$block->current(); + if($current['height']-$height>10) { echo "Height Differece too high\n"; exit; } + $last_block=$block->get($height); + + if ($last_block['id'] != $bl ) { echo "Last block does not match\n"; exit; } + echo "Sending the requested blocks\n"; + + for($i=$height+1;$i<=$current['height'];$i++){ + $data=$block->export("",$i); + $response = peer_post($hostname."/peer.php?q=submitBlock",$data); + + if($response!="block-ok") { echo "Block $i not accepted. Exiting.\n"; exit;} + echo "Block\t$i\t accepted\n"; + } + + } + else echo "Block not accepted!\n"; + +} +if($type=="transaction"){ + + $trx=new Transaction; + + $data=$trx->export($id); + + if(!$data){ echo "Invalid transaction id\n"; exit; } + + if($data['peer']=="local") $r=$db->run("SELECT hostname FROM peers WHERE blacklisted < UNIX_TIMESTAMP()"); + else $r=$db->run("SELECT hostname FROM peers WHERE blacklisted < UNIX_TIMESTAMP() AND reserve=0 ORDER by RAND() LIMIT ".$_config['transaction_propagation_peers']); + foreach($r as $x){ + $res= peer_post($x['hostname']."/peer.php?q=submitTransaction",$data); + if(!$res) echo "Transaction not accepted\n"; + else echo "Transaction accepted\n"; + } +} + +?> diff --git a/sanity.php b/sanity.php new file mode 100755 index 0000000..db5ac2b --- /dev/null +++ b/sanity.php @@ -0,0 +1,354 @@ +3600){ + @unlink("tmp/sanity-lock"); + } + die("Sanity lock in place"); +} +$lock = fopen("tmp/sanity-lock", "w"); +fclose($lock); +$arg=trim($argv[1]); +$arg2=trim($argv[2]); + +if($arg!="microsanity") sleep(10); + + +require_once("include/init.inc.php"); + + +if($_config['dbversion']<2){ + die("DB schema not created"); + @unlink("tmp/sanity-lock"); + exit; +} + +$block=new Block(); +$acc=new Account(); +$current=$block->current(); + + +$microsanity=false; +if($arg=="microsanity"&&!empty($arg2)){ + +do { + $x=$db->row("SELECT id,hostname FROM peers WHERE reserve=0 AND blacklisted$arg2)); + + if(!$x){ echo "Invalid node - $arg2\n"; break; } + $url=$x['hostname']."/peer.php?q="; + $data=peer_post($url."getBlock",array("height"=>$current['height'])); + + if(!$data) {echo "Invalid getBlock result\n"; break; } + if($data['id']==$current['id']) {echo "Same block\n"; break;} + + if($current['transactions']>$data['transactions']){ + echo "Block has less transactions\n"; + break; + } elseif($current['transactions']==$data['transactions']) { + $no1=hexdec(substr(coin2hex($current['id']),0,12)); + $no2=hexdec(substr(coin2hex($data['id']),0,12)); + + if(gmp_cmp($no1,$no2)!=-1){ + echo "Block hex larger than current\n"; + break; + } + } + + $prev = $block->get($current['height']-1); + + $public=$acc->public_key($data['generator']); + if(!$block->mine($public, $data['nonce'],$data['argon'],$block->difficulty($current['height']-1),$prev['id'])) { echo "Invalid prev-block\n"; break;} + $block->pop(1); + if(!$block->check($data)) break; + + + echo "Starting to sync last block from $x[hostname]\n"; + $b=$data; + $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("Block add: could not add block - $b[id] - $b[height]"); + + break; + } + + _log("Synced block from $host - $b[height] $b[difficulty]"); + + + + +} while(0); + + @unlink("tmp/sanity-lock"); +exit; +} + + +$t=time(); +//if($t-$_config['sanity_last']<300) {@unlink("tmp/sanity-lock"); die("The sanity cron was already run recently"); } +$db->run("UPDATE config SET val=:time WHERE cfg='sanity_last'",array(":time"=>$t)); +$block_peers=array(); +$longest_size=0; +$longest=0; +$blocks=array(); +$blocks_count=array(); +$most_common=""; +$most_common_size=0; + + +// checking peers + +$db->run("DELETE from peers WHERE fails>50"); + +$r=$db->run("SELECT id,hostname FROM peers WHERE reserve=0 AND blacklisted$_config['hostname'])); + if($res!==false) {$i++; echo "Peering OK - $peer\n"; } + else echo "Peering FAIL - $peer\n"; + if($i>$_config['max_peers']) break; + } + $r=$db->run("SELECT id,hostname FROM peers WHERE reserve=0 AND blacklistedrun("UPDATE peers SET fails=fails+1, blacklisted=UNIX_TIMESTAMP()+((fails+1)*3600) WHERE id=:id",array(":id"=>$x['id'])); + continue; + } + + $i=0; + foreach($data as $peer){ + if($peer['hostname']==$_config['hostname']) continue; + if (!filter_var($peer['hostname'], FILTER_VALIDATE_URL)) continue; + + if(!$db->single("SELECT COUNT(1) FROM peers WHERE ip=:ip or hostname=:hostname",array(":ip"=>$peer['ip'],":hostname"=>$peer['hostname']))){ + $i++; + if($i>$_config['max_test_peers']) break; + $test=peer_post($peer['hostname']."/peer.php?q=peer",array("hostname"=>$_config['hostname'])); + if($test!==false){ + $total_peers++; + echo "Peered with: $peer[hostname]\n"; + } + } + } + + + + + + + $data=peer_post($url."currentBlock"); + if($data===false) continue; + $db->run("UPDATE peers SET fails=0 WHERE id=:id",array(":id"=>$x['id'])); + + + $block_peers[$data['id']][]=$x['hostname']; + $blocks_count[$data['id']]++; + $blocks[$data['id']]=$data; + if($blocks_count[$data['id']]>$most_common_size){ + $most_common=$data['id']; + $most_common_size=$blocks_count[$data['id']]; + } + if($data['height']>$largest_height){ + $largest_height=$data['height']; + $largest_height_block=$data['id']; + } elseif($data['height']==$largestblock&&$data['id']!=$largest_height_block){ + if($data['difficulty']==$blocks[$largest_height_block]['difficulty']){ + if($most_common==$data['id']){ + $largest_height=$data['height']; + $largest_height_block=$data['id']; + } else { + if($blocks[$largest_height_block]['transactions']<$data['transactions']){ + $largest_height=$data['height']; + $largest_height_block=$data['id']; + } elseif($blocks[$largest_height_block]['transactions']==$data['transactions']) { + $no1=hexdec(substr(coin2hex($largest_height_block),0,12)); + $no2=hexdec(substr(coin2hex($data['id']),0,12)); + if(gmp_cmp($no1,$no2)==1){ + $largest_height=$data['height']; + $largest_height_block=$data['id']; + } + } + } + } elseif($data['difficulty']<$blocks[$largest_height_block]['difficulty']){ + $largest_height=$data['height']; + $largest_height_block=$data['id']; + } + + } + + + +} +echo "Most common: $most_common\n"; +echo "Most common block: $most_common_size\n"; +echo "Max height: $largest_height\n"; +echo "Current block: $current[height]\n"; +if($current['height']<$largest_height&&$largest_height>1){ + $db->run("UPDATE config SET val=1 WHERE cfg='sanity_sync'"); + sleep(10); + _log("Longest chain rule triggered - $largest_height - $largest_height_block"); + $peers=$block_peers[$largest_height_block]; + shuffle($peers); + foreach($peers as $host){ + _log("Starting to sync from $host"); + $url=$host."/peer.php?q="; + $data=peer_post($url."getBlock",array("height"=>$current['height'])); + + if($data===false){ _log("Could not get block from $host - $current[height]"); continue; } + + while($data['id']!=$current['id']){ + $block->delete($current['height']-10); + $current=$block->current(); + $data=peer_post($url."getBlock",array("height"=>$current['height'])); + + if($data===false){_log("Could not get block from $host - $current[height]"); break; } + } + if($data['id']!=$current['id']) continue; + while($current['height']<$largest_height){ + $data=peer_post($url."getBlocks",array("height"=>$current['height']+1)); + + if($data===false){_log("Could not get blocks from $host - height: $current[height]"); break; } + $good_peer=true; + foreach($data as $b){ + if(!$block->check($b)){ + _log("Block check: could not add block - $b[id] - $b[height]"); + $good_peer=false; + break; + } + $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("Block add: could not add block - $b[id] - $b[height]"); + $good_peer=false; + break; + } + + _log("Synced block from $host - $b[height] $b[difficulty]"); + + } + if(!$good_peer) break; + $current=$block->current(); + + } + if($good_peer) break; + + } + $db->run("UPDATE config SET val=0 WHERE cfg='sanity_sync'",array(":time"=>$t)); +} + + + +//rebroadcasting transactions +$forgotten=$current['height']-$_config['sanity_rebroadcast_height']; +$r=$db->run("SELECT id FROM mempool WHERE height<:forgotten ORDER by val DESC LIMIT 10",array(":forgotten"=>$forgotten)); +foreach($r as $x){ + system("php propagate.php transaction $x[id] &>/dev/null &"); + $db->run("UPDATE mempool SET height=:current WHERE id=:id",array(":id"=>$x['id'], ":current"=>$current['height'])); +} + + +//add new peers if there aren't enough active +if($total_peers<$_config['max_peers']*0.7){ + $res=$_config['max_peers']-$total_peers; + $db->run("UPDATE peers SET reserve=0 WHERE reserve=1 AND blacklistedrun("SELECT * FROM peers WHERE blacklistedrun("UPDATE peers SET fails=fails+1, blacklisted=UNIX_TIMESTAMP()+((fails+1)*3600) WHERE id=:id",array(":id"=>$x['id'])); + else $db->run("UPDATE peers SET fails=0 WHERE id=:id",array(":id"=>$x['id'])); +} + +//clean tmp files +$f=scandir("tmp/"); +$time=time(); +foreach($f as $x){ + if(strlen($x)<5&&substr($x,0,1)==".") continue; + $pid_time=filemtime("tmp/$x"); + if($time-$pid_time>7200) @unlink("tmp/$x"); +} + + +//recheck blocks +if($_config['sanity_recheck_blocks']>0){ + $blocks=array(); + $all_blocks_ok=true; + $start=$current['height']-$_config['sanity_recheck_blocks']; + if($start<2) $start=2; + $r=$db->run("SELECT * FROM blocks WHERE height>=:height ORDER by height ASC",array(":height"=>$start)); + foreach($r as $x){ + $blocks[$x['height']]=$x; + $max_height=$x['height']; + } + + for($i=$start+1;$i<=$max_height;$i++){ + $data=$blocks[$i]; + + $key=$db->single("SELECT public_key FROM accounts WHERE id=:id",array(":id"=>$data['generator'])); + + if(!$block->mine($key,$data['nonce'], $data['argon'], $data['difficulty'], $blocks[$i-1]['id'])) { + $db->run("UPDATE config SET val=1 WHERE cfg='sanity_sync'"); + _log("Invalid block detected. Deleting everything after $data[height] - $data[id]"); + sleep(10); + $all_blocks_ok=false; + $block->delete($i); + + $db->run("UPDATE config SET val=0 WHERE cfg='sanity_sync'"); + break; + } + } + if($all_blocks_ok) echo "All checked blocks are ok\n"; +} + + +@unlink("tmp/sanity-lock"); +?> \ No newline at end of file diff --git a/tmp/db-update b/tmp/db-update new file mode 100644 index 0000000..e69de29 diff --git a/util.php b/util.php new file mode 100755 index 0000000..a76c1a8 --- /dev/null +++ b/util.php @@ -0,0 +1,113 @@ +run("DELETE FROM {$table}"); + +echo "\n The database has been cleared\n"; + +} + + +elseif($cmd=='pop'){ + $no=intval($argv[2]); + $block=new Block; + $block->pop($no); +} + + +elseif($cmd=='block-time'){ + $t=time(); + $r=$db->run("SELECT * FROM blocks ORDER by height DESC LIMIT 100"); + $start=0; + foreach($r as $x){ + if($start==0) $start=$x['date']; + $time=$t-$x['date']; + $t=$x['date']; + echo "$x[height] -> $time\n"; + $end=$x['date']; + } +echo "Average block time: ".ceil(($start-$end)/100)." seconds\n"; + + +} + +elseif($cmd=="peer"){ + $res=peer_post($argv[2]."/peer.php?q=peer",array("hostname"=>$_config['hostname'])); + if($res!==false) echo "Peering OK\n"; + else echo "Peering FAIL\n"; +} +elseif ($cmd=="current") { + $block=new Block; + var_dump($block->current()); +} elseif($cmd=="blocks"){ + $height=intval($argv[2]); + $limit=intval($argv[3]); + if($limit<1) $limit=100; + $r=$db->run("SELECT * FROM blocks WHERE height>:height ORDER by height ASC LIMIT $limit",array(":height"=>$height)); + foreach($r as $x){ + echo "$x[height]\t$x[id]\n"; + } +} + +elseif($cmd=="recheck-blocks"){ + $blocks=array(); + $block=new Block(); + $r=$db->run("SELECT * FROM blocks ORDER by height ASC"); + foreach($r as $x){ + $blocks[$x['height']]=$x; + $max_height=$x['height']; + } + for($i=2;$i<=$max_height;$i++){ + $data=$blocks[$i]; + + $key=$db->single("SELECT public_key FROM accounts WHERE id=:id",array(":id"=>$data['generator'])); + + if(!$block->mine($key,$data['nonce'], $data['argon'], $data['difficulty'], $blocks[$i-1]['id'])) { + _log("Invalid block detected. We should delete everything after $data[height] - $data[id]"); + break; + } + } +} elseif($cmd=="peers"){ + $r=$db->run("SELECT * FROM peers ORDER by reserve ASC LIMIT 100"); + foreach($r as $x){ + echo "$x[hostname]\t$x[reserve]\n"; + } + +} elseif($cmd=="mempool"){ +$res=$db->single("SELECT COUNT(1) from mempool"); +echo "Mempool size: $res\n"; +} else { + echo "Invalid command\n"; +} + + +?>