get_address($public_key); // the transactions are always sorted in the same way, on all nodes, as they are hashed as json ksort($data); // create the hash / block id $hash=$this->hash($generator, $height, $date, $nonce, $data, $signature, $difficulty, $argon); //fix for the broken base58 library used until block 16900, trimming the first 0 bytes. if($height<16900) $hash=ltrim($hash,'1'); $json=json_encode($data); // create the block data and check it against the signature $info="{$generator}-{$height}-{$date}-{$nonce}-{$json}-{$difficulty}-{$argon}"; if(!$acc->check_signature($info,$signature,$public_key)) { _log("Block signature check failed"); return false; } if(!$this->parse_block($hash,$height,$data, true)) { _log("Parse block failed"); return false; } // lock table to avoid race conditions on blocks $db->exec("LOCK TABLES blocks WRITE, accounts WRITE, transactions WRITE, mempool WRITE"); $reward=$this->reward($height,$data); $msg=''; // the reward transaction $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; // hash the transaction $transaction['id']=$trx->hash($transaction); // check the signature $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)) {_log("Reward signature failed"); return false; } // insert the block into the db $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) { // rollback and exit if it fails _log("Block DB insert failed"); $db->rollback(); $db->exec("UNLOCK TABLES"); return false; } // insert the reward transaction in the db $trx->add($hash, $height,$transaction); // parse the block's transactions and insert them to db $res=$this->parse_block($hash,$height,$data, false); // if any fails, rollback if($res==false) $db->rollback(); else $db->commit(); // relese the locking as everything is finished $db->exec("UNLOCK TABLES"); return true; } // returns the current block, without the transactions 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; } // returns the previous block public function prev(){ global $db; $current=$db->row("SELECT * FROM blocks ORDER by height DESC LIMIT 1,1"); return $current; } // calculates the difficulty / base target for a specific block. The higher the difficulty number, the easier it is to win a block. public function difficulty($height=0){ global $db; // if no block height is specified, use the current block. if($height==0){ $current=$this->current(); } else{ $current=$this->get($height); } $height=$current['height']; if($height==10801) return 5555555555; //hard fork 10900 resistance, force new difficulty // last 20 blocks used to check the block times $limit=20; if($height<20) $limit=$height-1; // for the first 10 blocks, use the genesis difficulty if($height<10) return $current['difficulty']; // elapsed time between the last 20 blocks $first=$db->row("SELECT `date` FROM blocks ORDER by height DESC LIMIT $limit,1"); $time=$current['date']-$first['date']; // avg block time $result=ceil($time/$limit); // if larger than 200 sec, increase by 5% if($result>220){ $dif= bcmul($current['difficulty'], 1.05); } elseif($result<260){ // if lower, decrease by 5% $dif= bcmul($current['difficulty'], 0.95); } else { // keep current difficulty $dif=$current['difficulty']; } if(strpos($dif,'.')!==false){ $dif=substr($dif,0,strpos($dif,'.')); } //minimum and maximum diff if($dif<1000) $dif=1000; if($dif>9223372036854775800) $dif=9223372036854775800; return $dif; } // calculates the maximum block size and increase by 10% the number of transactions if > 100 on the last 100 blocks 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); } // calculate the reward for each block public function reward($id,$data=array()){ // starting reward $reward=1000; // decrease by 1% each 10800 blocks (approx 1 month) $factor=floor($id/10800)/100; $reward-=$reward*$factor; if($reward<0) $reward=0; // calculate the transaction fees $fees=0; if(count($data)>0){ foreach($data as $x){ $fees+=$x['fee']; } } return number_format($reward+$fees,8,'.',''); } // checks the validity of a block public function check($data){ // argon must have at least 20 chars if(strlen($data['argon'])<20) { _log("Invalid block argon - $data[argon]"); return false; } $acc=new Account; // generator's public key must be valid if(!$acc->valid_key($data['public_key'])) { _log("Invalid public key - $data[public_key]"); return false; } //difficulty should be the same as our calculation if($data['difficulty']!=$this->difficulty()) { _log("Invalid difficulty - $data[difficulty] - ".$this->difficulty()); return false; } //check the argon hash and the nonce to produce a valid block if(!$this->mine($data['public_key'],$data['nonce'], $data['argon'])) { _log("Mine check failed"); return false; } return true; } // creates a new block on this node public function forge($nonce, $argon, $public_key, $private_key){ //check the argon hash and the nonce to produce a valid block if(!$this->mine($public_key,$nonce, $argon)) return false; // the block's date timestamp must be bigger than the last block $current=$this->current(); $height=$current['height']+=1; $date=time(); if($date<=$current['date']) return 0; // get the mempool transactions $txn=new Transaction; $data=$txn->mempool($this->max_transactions()); $difficulty=$this->difficulty(); $acc=new Account; $generator=$acc->get_address($public_key); // always sort the transactions in the same way ksort($data); // sign the block $signature=$this->sign($generator, $height, $date, $nonce, $data, $private_key, $difficulty, $argon); // reward transaction and 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); // add the block to the blockchain $res=$this->add($height, $public_key, $nonce, $data, $date, $signature, $difficulty, $reward_signature, $argon); if(!$res) return false; return true; } // check if the arguments are good for mining a specific block public function mine($public_key, $nonce, $argon, $difficulty=0, $current_id=0, $current_height=0){ global $_config; // if no id is specified, we use the current if($current_id===0){ $current=$this->current(); $current_id=$current['id']; $current_height=$current['height']; } // get the current difficulty if empty if($difficulty===0) $difficulty=$this->difficulty(); // the argon parameters are hardcoded to avoid any exploits if($current_height>10800) $argon='$argon2i$v=19$m=524288,t=1,p=1'.$argon; //10800 block hard fork - resistance against gpu else $argon='$argon2i$v=19$m=16384,t=4,p=4'.$argon; // the hash base for agon $base="$public_key-$nonce-".$current_id."-$difficulty"; // check argon's hash validity if(!password_verify($base,$argon)) { return false; } // all nonces are valid in testnet if($_config['testnet']==true) return true; // prepare the base for the hashing $hash=$base.$argon; // hash the base 6 times for($i=0;$i<5;$i++) $hash=hash("sha512",$hash,true); $hash=hash("sha512",$hash); // split it in 2 char substrings, to be used as hex $m=str_split($hash,2); // calculate a number based on 8 hex numbers - no specific reason, we just needed an algoritm to generate the number from the hash $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]); // the number must not start with 0 $duration=ltrim($duration, '0'); // divide the number by the difficulty and create the deadline $result=gmp_div($duration, $difficulty); // if the deadline >0 and <=240, the arguments are valid fora block win if($result>0&&$result<=240) return true; return false; } // parse the block transactions public function parse_block($block, $height, $data, $test=true){ global $db; // data must be array if($data===false) return false; $acc=new Account; $trx=new Transaction; // no transactions means all are valid if(count($data)==0) return true; // check if the number of transactions is not bigger than current block size $max=$this->max_transactions(); if(count($data)>$max) return false; $balance=array(); foreach($data as &$x){ // get the sender's account if empty if(empty($x['src'])) $x['src']=$acc->get_address($x['public_key']); //validate the transaction if(!$trx->check($x,$height)) return false; // prepare total balance $balance[$x['src']]+=$x['val']+$x['fee']; // check if the transaction is already on the blockchain if($db->single("SELECT COUNT(1) FROM transactions WHERE id=:id",array(":id"=>$x['id']))>0) return false; } // check if the account has enough balance to perform the 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 the test argument is false, add the transactions to the blockchain if($test==false){ foreach($data as $d){ $res=$trx->add($block, $height, $d); if($res==false) return false; } } return true; } // initialize the blockchain, add the genesis block 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); } // delete all blocks >= height 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; } // delete specific block 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; // avoid race conditions on blockchain manipulations $db->beginTransaction(); $db->exec("LOCK TABLES blocks WRITE, accounts WRITE, transactions WRITE, mempool WRITE"); // reverse all transactions of the block $res=$trx->reverse($x['id']); if($res===false) { // rollback if you can't reverse the transactions $db->rollback(); $db->exec("UNLOCK TABLES"); return false; } // remove the actual block $res=$db->run("DELETE FROM blocks WHERE id=:id",array(":id"=>$x['id'])); if($res!=1){ //rollback if you can't delete the block $db->rollback(); $db->exec("UNLOCK TABLES"); return false; } // commit and release if all good $db->commit(); $db->exec("UNLOCK TABLES"); return true; } // sign a new block, used when mining 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; } // generate the sha512 hash of the block data and converts it to base58 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); } // exports the block data, to be used when submitting to other peers 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; // the reward transaction always has version 0 $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; } //return a specific block as array 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; } } ?>