documentation + base58 fix

This commit is contained in:
Arionum
2018-02-22 19:22:28 +02:00
parent e1971dd741
commit 8a648dcc8a
18 changed files with 1749 additions and 226 deletions

View File

@@ -3,7 +3,7 @@
class Account {
// inserts the account in the DB and updates the public key if empty
public function add($public_key, $block){
global $db;
$id=$this->get_address($public_key);
@@ -11,52 +11,64 @@ class Account {
$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);
}
// inserts just the account without public key
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);
}
// generates Account's address from the public key
public function get_address($hash){
for($i=0;$i<9;$i++) $hash=hash('sha512',$hash, true);
return base58_encode($hash);
//broken base58 addresses, which are block winners, missing the first 0 bytes from the address.
if($hash=='PZ8Tyr4Nx8MHsRAGMpZmZ6TWY63dXWSCwCpspGFGQSaF9yVGLamBgymdf8M7FafghmP3oPzQb3W4PZsZApVa41uQrrHRVBH5p9bdoz7c6XeRQHK2TkzWR45e') return '22SoB29oyq2JhMxtBbesL7JioEYytyC6VeFmzvBH6fRQrueSvyZfEXR5oR7ajSQ9mLERn6JKU85EAbVDNChke32';
elseif($hash=='PZ8Tyr4Nx8MHsRAGMpZmZ6TWY63dXWSCzbRyyz5oDNDKhk5jyjg4caRjkbqegMZMrUkuBjVMuYcVfPyc3aKuLmPHS4QEDjCrNGks7Z5oPxwv4yXSv7WJnkbL') return 'AoFnv3SLujrJSa2J7FDTADGD7Eb9kv3KtNAp7YVYQEUPcLE6cC6nLvvhVqcVnRLYF5BFF38C1DyunUtmfJBhyU';
elseif($hash=='PZ8Tyr4Nx8MHsRAGMpZmZ6TWY63dXWSCyradtFFJoaYB4QdcXyBGSXjiASMMnofsT4f5ZNaxTnNDJt91ubemn3LzgKrfQh8CBpqaphkVNoRLub2ctdMnrzG1') return 'RncXQuc7S7aWkvTUJSHEFvYoV3ntAf7bfxEHjSiZNBvQV37MzZtg44L7GAV7szZ3uV8qWqikBewa3piZMqzBqm';
elseif($hash=='PZ8Tyr4Nx8MHsRAGMpZmZ6TWY63dXWSCyjKMBY4ihhJ2G25EVezg7KnoCBVbhdvWfqzNA4LC5R7wgu3VNfJgvqkCq9sKKZcCoCpX6Qr9cN882MoXsfGTvZoj') return 'Rq53oLzpCrb4BdJZ1jqQ2zsixV2ukxVdM4H9uvUhCGJCz1q2wagvuXV4hC6UVwK7HqAt1FenukzhVXgzyG1y32';
// hashes 9 times in sha512 (binary) and encodes in base58
for($i=0;$i<9;$i++) $hash=hash('sha512',$hash, true);
return base58_encode($hash);
}
// checks the ecdsa secp256k1 signature for a specific public key
public function check_signature($data, $signature, $public_key){
return ec_verify($data ,$signature, $public_key);
}
// generates a new account and a public/private key pair
public function generate_account(){
// using secp256k1 curve for ECDSA
$args = array(
"curve_name" => "secp256k1",
"private_key_type" => OPENSSL_KEYTYPE_EC,
);
// generates a new key pair
$key1 = openssl_pkey_new($args);
// exports the private key encoded as PEM
openssl_pkey_export($key1, $pvkey);
// converts the PEM to a base58 format
$private_key= pem2coin($pvkey);
// exports the private key encoded as PEM
$pub = openssl_pkey_get_details($key1);
// converts the PEM to a base58 format
$public_key= pem2coin($pub['key']);
// generates the account's address based on the public key
$address=$this->get_address($public_key);
return array("address"=>$address, "public_key"=>$public_key,"private_key"=>$private_key);
}
// check the validity of a base58 encoded key. At the moment, it checks only the characters to be base58.
public function valid_key($id){
$chars = str_split("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz");
for($i=0;$i<strlen($id);$i++) if(!in_array($id[$i],$chars)) return false;
@@ -64,6 +76,7 @@ class Account {
return true;
}
// check the validity of an address. At the moment, it checks only the characters to be base58 and the length to be >=70 and <=128.
public function valid($id){
if(strlen($id)<70||strlen($id)>128) return false;
$chars = str_split("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz");
@@ -72,26 +85,31 @@ class Account {
return true;
}
// returns the current account balance
public function balance($id){
global $db;
$res=$db->single("SELECT balance FROM accounts WHERE id=:id",array(":id"=>$id));
if($res===false) $res="0.00000000";
return number_format($res,8,".","");
}
// returns the account balance - any pending debits from the mempool
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 the original balance is 0, no mempool transactions are possible
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 number_format($rez,8,".","");
}
// returns all the transactions of a specific address
public function get_transactions($id,$limit=100){
global $db;
$block=new Block;
$current=$block->current();
$current=$block->current();
$public_key=$this->public_key($id);
$limit=intval($limit);
if($limit>100||$limit<1) $limit=100;
@@ -102,7 +120,8 @@ class Account {
$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'];
// version 0 -> reward transaction, version 1 -> normal transaction
if($x['version']==0) $trans['type']="mining";
elseif($x['version']==1){
if($x['dst']==$id) $trans['type']="credit";
@@ -115,6 +134,7 @@ class Account {
}
return $transactions;
}
// returns the transactions from the mempool
public function get_mempool_transactions($id){
global $db;
$transactions=array();
@@ -122,12 +142,14 @@ class Account {
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";
// they are unconfirmed, so they will have -1 confirmations.
$trans['confirmations']=-1;
ksort($trans);
$transactions[]=$trans;
}
return $transactions;
}
// returns the public key for a specific account
public function public_key($id){
global $db;
$res=$db->single("SELECT public_key FROM accounts WHERE id=:id",array(":id"=>$id));

View File

@@ -8,28 +8,27 @@ public function add($height, $public_key, $nonce, $data, $date, $signature, $dif
global $db;
$acc=new Account;
$trx=new Transaction;
//try {
// } catch (Exception $e){
// }
$generator=$acc->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 17000, trimming the first 0 bytes.
if($height<=17000) $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)) return false;
if(!$acc->check_signature($info,$signature,$public_key)) { _log("Block signature check failed"); return false; }
if(!$this->parse_block($hash,$height,$data, true)) 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);
@@ -37,34 +36,42 @@ public function add($height, $public_key, $nonce, $data, $date, $signature, $dif
$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);
$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;
// 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");
@@ -75,7 +82,7 @@ public function current(){
return $current;
}
// returns the previous block
public function prev(){
global $db;
$current=$db->row("SELECT * FROM blocks ORDER by height DESC LIMIT 1,1");
@@ -83,9 +90,11 @@ public function prev(){
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{
@@ -94,35 +103,46 @@ public function difficulty($height=0){
$height=$current['height'];
if($height==10801) return 5555555555; //hard fork 10900 resistance, force new difficulty
if($height==10801) return 5555555555; //hard fork 10900 resistance
// 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();
@@ -132,15 +152,19 @@ public function max_transactions(){
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){
@@ -151,28 +175,39 @@ public function reward($id,$data=array()){
return number_format($reward+$fees,8,'.','');
}
// checks the validity of a block
public function check($data){
if(strlen($data['argon'])<20) return false;
// argon must have at least 20 chars
if(strlen($data['argon'])<20) { _log("Invalid block argon - $data[argon]"); 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;
// 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());
@@ -180,55 +215,72 @@ public function forge($nonce, $argon, $public_key, $private_key){
$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 signature
// 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();
if($current_height>10800) $argon='$argon2i$v=19$m=524288,t=1,p=1'.$argon; //10800
// 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]);
$duration=ltrim($duration, '0');
$result=gmp_div($duration, $difficulty);
// 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;
@@ -236,35 +288,43 @@ public function mine($public_key, $nonce, $argon, $difficulty=0, $current_id=0,
// 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;
$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
// 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){
@@ -277,6 +337,7 @@ public function parse_block($block, $height, $data, $test=true){
}
// initialize the blockchain, add the genesis block
private function genesis(){
global $db;
$signature='AN1rKvtLTWvZorbiiNk5TBYXLgxiLakra2byFef9qoz1bmRzhQheRtiWivfGSwP6r8qHJGrf8uBeKjNZP1GZvsdKUVVN2XQoL';
@@ -301,6 +362,7 @@ public function pop($no=1){
$this->delete($current['height']-$no+1);
}
// delete all blocks >= height
public function delete($height){
if($height<2) $height=2;
global $db;
@@ -331,6 +393,8 @@ public function delete($height){
return true;
}
// delete specific block
public function delete_id($id){
global $db;
@@ -339,27 +403,33 @@ public function delete_id($id){
$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);
@@ -370,6 +440,7 @@ public function sign($generator, $height, $date, $nonce, $data, $key, $difficult
}
// 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}");
@@ -377,6 +448,7 @@ public function hash($public_key, $height, $date, $nonce, $data, $signature, $di
}
// exports the block data, to be used when submitting to other peers
public function export($id="",$height=""){
if(empty($id)&&empty($height)) return false;
@@ -396,13 +468,14 @@ public function export($id="",$height=""){
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;

View File

@@ -1,17 +1,34 @@
<?php
// Database connection
$_config['db_connect']="mysql:host=localhost;dbname=ENTER-DB-NAME";
$_config['db_user']="ENTER-DB-USER";
$_config['db_pass']="ENTER-DB-PASS";
// Maximum number of connected peers
$_config['max_peers']=30;
// Testnet, used for development
$_config['testnet']=false;
// To avoid any problems if other clones are made
$_config['coin']="arionum";
// maximum transactions accepted from a single peer
$_config['peer_max_mempool']=100;
// maximum mempool transactions to be rebroadcasted
$_config['max_mempool_rebroadcast']=5000;
// after how many blocks should the transactions be rebroadcasted
$_config['sanity_rebroadcast_height']=30;
// each new received transaction is sent to X peers
$_config['transaction_propagation_peers']=5;
// how many new peers to check from each peer.
$_config['max_test_peers']=5;
// recheck the last blocks on sanity
$_config['sanity_recheck_blocks']=10;
// allow others to connect to node api. If set to false, only allowed_hosts are allowed
$_config['public_api']=true;
// hosts allowed to mine on this node
$_config['allowed_hosts']=array("127.0.0.1");
// sanity is run every X seconds
$_config['sanity_interval']=900;
// accept the setting of new hostnames / should be used only if you want to change the hostname
$_config['allow_hostname_change']=false;
?>

View File

@@ -1,4 +1,5 @@
<?php
// a simple wrapper for pdo
class db extends PDO {

View File

@@ -1,24 +1,25 @@
<?php
// simple santization function to accept only alphanumeric characters
function san($a,$b=""){
$a = preg_replace("/[^a-zA-Z0-9".$b."]/", "", $a);
return $a;
}
// api error and exit
function api_err($data){
global $_config;
echo json_encode(array("status"=>"error","data"=>$data, "coin"=>$_config['coin']));
exit;
}
// api print ok and exit
function api_echo($data){
global $_config;
echo json_encode(array("status"=>"ok","data"=>$data, "coin"=>$_config['coin']));
exit;
}
// log function, shows only in cli atm
function _log($data){
$date=date("[Y-m-d H:s:]");
$trace=debug_backtrace();
@@ -26,6 +27,7 @@ function _log($data){
if(php_sapi_name() === 'cli') echo "$date [$location] $data\n";
}
// converts PEM key to hex
function pem2hex ($data) {
$data=str_replace("-----BEGIN PUBLIC KEY-----","",$data);
$data=str_replace("-----END PUBLIC KEY-----","",$data);
@@ -37,6 +39,7 @@ function pem2hex ($data) {
return $data;
}
// converts hex key to PEM
function hex2pem ($data, $is_private_key=false) {
$data=hex2bin($data);
$data=base64_encode($data);
@@ -46,66 +49,94 @@ function hex2pem ($data, $is_private_key=false) {
//all credits for this base58 functions should go to tuupola / https://github.com/tuupola/base58/
function baseConvert(array $source, $source_base, $target_base)
// Base58 encoding/decoding functions - all credits go to https://github.com/stephen-hill/base58php
function base58_encode($string)
{
$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);
};
$alphabet='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
$base=strlen($alphabet);
// Type validation
if (is_string($string) === false) {
return false;
}
// If the string is empty, then the encoded string is obviously empty
if (strlen($string) === 0) {
return '';
}
// Now we need to convert the byte array into an arbitrary-precision decimal
// We basically do this by performing a base256 to base10 conversion
$hex = unpack('H*', $string);
$hex = reset($hex);
$decimal = gmp_init($hex, 16);
// This loop now performs base 10 to base 58 conversion
// The remainder or modulo on each loop becomes a base 58 character
$output = '';
while (gmp_cmp($decimal, $base) >= 0) {
list($decimal, $mod) = gmp_div_qr($decimal, $base);
$output .= $alphabet[gmp_intval($mod)];
}
// If there's still a remainder, append it
if (gmp_cmp($decimal, 0) > 0) {
$output .= $alphabet[gmp_intval($decimal)];
}
// Now we need to reverse the encoded data
$output = strrev($output);
// Now we need to add leading zeros
$bytes = str_split($string);
foreach ($bytes as $byte) {
if ($byte === "\x00") {
$output = $alphabet[0] . $output;
continue;
}
array_unshift($result, $remainder);
$source = $quotient;
break;
}
return $result;
return (string) $output;
}
function base58_encode($data)
function base58_decode($base58)
{
if (is_integer($data)) {
$data = [$data];
} else {
$data = str_split($data);
$data = array_map(function ($character) {
return ord($character);
}, $data);
$alphabet='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
$base=strlen($alphabet);
// Type Validation
if (is_string($base58) === false) {
return false;
}
$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);
// If the string is empty, then the decoded string is obviously empty
if (strlen($base58) === 0) {
return '';
}
$converted = baseConvert($data, 58, 256);
return implode("", array_map(function ($ascii) {
return chr($ascii);
}, $converted));
$indexes = array_flip(str_split($alphabet));
$chars = str_split($base58);
// Check for invalid characters in the supplied base58 string
foreach ($chars as $char) {
if (isset($indexes[$char]) === false) {
return false;
}
}
// Convert from base58 to base10
$decimal = gmp_init($indexes[$chars[0]], 10);
for ($i = 1, $l = count($chars); $i < $l; $i++) {
$decimal = gmp_mul($decimal, $base);
$decimal = gmp_add($decimal, $indexes[$chars[$i]]);
}
// Convert from base10 to base256 (8-bit byte array)
$output = '';
while (gmp_cmp($decimal, 0) > 0) {
list($decimal, $byte) = gmp_div_qr($decimal, 256);
$output = pack('C', gmp_intval($byte)) . $output;
}
// Now we need to add leading zeros
foreach ($chars as $char) {
if ($indexes[$char] === 0) {
$output = "\x00" . $output;
continue;
}
break;
}
return $output;
}
// converts PEM key to the base58 version used by ARO
function pem2coin ($data) {
$data=str_replace("-----BEGIN PUBLIC KEY-----","",$data);
$data=str_replace("-----END PUBLIC KEY-----","",$data);
@@ -118,7 +149,7 @@ function pem2coin ($data) {
return base58_encode($data);
}
// converts the key in base58 to PEM
function coin2pem ($data, $is_private_key=false) {
@@ -133,9 +164,9 @@ function coin2pem ($data, $is_private_key=false) {
return "-----BEGIN PUBLIC KEY-----\n".$data."\n-----END PUBLIC KEY-----\n";
}
// sign data with private key
function ec_sign($data, $key){
// transform the base58 key format to PEM
$private_key=coin2pem($key,true);
@@ -145,9 +176,8 @@ function ec_sign($data, $key){
openssl_sign($data,$signature,$pkey,OPENSSL_ALGO_SHA256);
// the signature will be base58 encoded
return base58_encode($signature);
}
@@ -156,7 +186,7 @@ function ec_sign($data, $key){
function ec_verify($data, $signature, $key){
// transform the base58 key to PEM
$public_key=coin2pem($key);
$signature=base58_decode($signature);
@@ -170,7 +200,7 @@ function ec_verify($data, $signature, $key){
return false;
}
// POST data to an URL (usualy peer). The data is an array, json encoded with is sent as $_POST['data']
function peer_post($url, $data=array(),$timeout=60,$debug=false){
global $_config;
if($debug) echo "\nPeer post: $url\n";
@@ -195,16 +225,19 @@ function peer_post($url, $data=array(),$timeout=60,$debug=false){
$result = file_get_contents($url, false, $context);
if($debug) echo "\nPeer response: $result\n";
$res=json_decode($result,true);
// the function will return false if something goes wrong
if($res['status']!="ok"||$res['coin']!=$_config['coin']) return false;
return $res['data'];
}
// convers hex to base58
function hex2coin($hex){
$data=hex2bin($hex);
return base58_encode($data);
}
// converts base58 to hex
function coin2hex($data){
$bin= base58_decode($data);

View File

@@ -1,15 +1,16 @@
<?php
// ARO version
define("VERSION", "0.2b");
// Amsterdam timezone by default, should probably be moved to config
date_default_timezone_set("Europe/Amsterdam");
//error_reporting(E_ALL & ~E_NOTICE);
error_reporting(0);
ini_set('display_errors',"off");
// not accessible directly
if(php_sapi_name() !== 'cli'&&substr_count($_SERVER['PHP_SELF'],"/")>1){
die("This application should only be run in the main directory /");
}
@@ -26,6 +27,8 @@ if($_config['db_pass']=="ENTER-DB-PASS") die("Please update your config file and
// initial DB connection
$db=new DB($_config['db_connect'],$_config['db_user'],$_config['db_pass'],0);
if(!$db) die("Could not connect to the DB backend.");
// checks for php version and extensions
if (!extension_loaded("openssl") && !defined("OPENSSL_KEYTYPE_EC")) api_err("Openssl php extension missing");
if (!extension_loaded("gmp")) api_err("gmp php extension missing");
if (!extension_loaded('PDO')) api_err("pdo php extension missing");
@@ -47,10 +50,11 @@ foreach($query as $res){
// nothing is allowed while in maintenance
if($_config['maintenance']==1) api_err("under-maintenance");
// update the db schema, on every git pull or initial install
if(file_exists("tmp/db-update")){
$res=unlink("tmp/db-update");
@@ -62,19 +66,22 @@ if(file_exists("tmp/db-update")){
echo "Could not access the tmp/db-update file. Please give full permissions to this file\n";
}
// something went wront with the db schema
if($_config['dbversion']<2) exit;
// separate blockchain for testnet
if($_config['testnet']==true) $_config['coin'].="-testnet";
// current hostname
$hostname=(!empty($_SERVER['HTTPS'])?'https':'http')."://".$_SERVER['HTTP_HOST'];
if($hostname!=$_config['hostname']&&$_SERVER['HTTP_HOST']!="localhost"&&$_SERVER['HTTP_HOST']!="127.0.0.1"&&$_SERVER['hostname']!='::1'&&php_sapi_name() !== 'cli'){
// set the hostname to the current one
if($hostname!=$_config['hostname']&&$_SERVER['HTTP_HOST']!="localhost"&&$_SERVER['HTTP_HOST']!="127.0.0.1"&&$_SERVER['hostname']!='::1'&&php_sapi_name() !== 'cli' && ($_config['allow_hostname_change']!=false||empty($_config['hostname']))){
$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");
// run sanity
$t=time();
if($t-$_config['sanity_last']>$_config['sanity_interval']&& php_sapi_name() !== 'cli') system("php sanity.php > /dev/null 2>&1 &");

View File

@@ -1,5 +1,6 @@
<?php
// when db schema modifications are done, this function is run.
$dbversion=intval($_config['dbversion']);
$db->beginTransaction();
@@ -135,8 +136,9 @@ if($dbversion==5){
$db->run("ALTER TABLE `peers` ADD `fails` TINYINT NOT NULL DEFAULT '0' AFTER `ip`; ");
$dbversion++;
}
// update the db version to the latest one
if($dbversion!=$_config['dbversion']) $db->run("UPDATE config SET val=:val WHERE cfg='dbversion'",array(":val"=>$dbversion));
$db->commit();
?>
?>

View File

@@ -1,8 +1,7 @@
<?php
class Transaction {
// reverse and remove all transactions from a block
public function reverse($block){
global $db;
$acc=new Account;
@@ -10,14 +9,17 @@ class Transaction {
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']));
// on version 0 / reward transaction, don't credit anyone
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);
// add the transactions to mempool
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;
}
}
// clears the mempool
public function clean_mempool(){
global $db;
$block= new Block;
@@ -26,12 +28,14 @@ class Transaction {
$limit=$height-1000;
$db->run("DELETE FROM mempool WHERE height<:limit",array(":limit"=>$limit));
}
// returns X transactions from mempool
public function mempool($max){
global $db;
$block=new Block;
$current=$block->current();
$height=$current['height']+1;
// only get the transactions that are not locked with a future height
$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){
@@ -72,11 +76,13 @@ class Transaction {
$transactions[$x['id']]=$trans;
}
}
// always sort the array
ksort($transactions);
return $transactions;
}
// add a new transaction to mempool and lock it with the current height
public function add_mempool($x, $peer=""){
global $db;
$block= new Block;
@@ -90,6 +96,7 @@ class Transaction {
}
// add a new transaction to the blockchain
public function add($block,$height, $x){
global $db;
$acc= new Account;
@@ -100,22 +107,24 @@ class Transaction {
$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)-:fee WHERE id=:id",array(":id"=>$x['src'], ":val"=>$x['val'], ":fee"=>$x['fee']));
// no debit when the transaction is reward
if($x['version']>0) $db->run("UPDATE accounts SET balance=(balance-:val)-:fee WHERE id=:id",array(":id"=>$x['src'], ":val"=>$x['val'], ":fee"=>$x['fee']));
$db->run("DELETE FROM mempool WHERE id=:id",array(":id"=>$x['id']));
return true;
}
// hash the transaction's most important fields and create the transaction ID
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);
}
// check the transaction for validity
public function check($x, $height=0){
// if no specific block, use current
if($height===0){
$block=new Block;
$current=$block->current();
@@ -124,32 +133,54 @@ class Transaction {
$acc= new Account;
$info=$x['val']."-".$x['fee']."-".$x['dst']."-".$x['message']."-".$x['version']."-".$x['public_key']."-".$x['date'];
// the value must be >=0
if($x['val']<0){ _log("$x[id] - Value below 0"); return false; }
if($x['fee']<0) { _log("$x[id] - Fee below 0"); return false; }
// the fee must be >=0
if($x['fee']<0) { _log("$x[id] - Fee below 0"); return false; }
// the fee is 0.25%, hardcoded
$fee=$x['val']*0.0025;
$fee=number_format($fee,8,".","");
if($fee<0.00000001) $fee=0.00000001;
// max fee after block 10800 is 10
if($height>10800&&$fee>10) $fee=10; //10800
// added fee does not match
if($fee!=$x['fee']) { _log("$x[id] - Fee not 0.25%"); return false; }
// invalid destination address
if(!$acc->valid($x['dst'])) { _log("$x[id] - Invalid destination address"); return false; }
// reward transactions are not added via this function
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; }
// public key must be at least 15 chars / probably should be replaced with the validator function
if(strlen($x['public_key'])<15) { _log("$x[id] - Invalid public key size"); return false; }
// no transactions before the genesis
if($x['date']<1511725068) { _log("$x[id] - Date before genesis"); return false; }
// no future transactions
if($x['date']>time()+86400) { _log("$x[id] - Date in the future"); return false; }
// prevent the resending of broken base58 transactions
if($height>17000&&$x['date']<1519319340) return false;
$id=$this->hash($x);
if($x['id']!=$id) { _log("$x[id] - Invalid hash"); return false; }
// the hash does not match our regenerated hash
if($x['id']!=$id) {
// fix for broken base58 library which was used until block 17000, accepts hashes without the first 1 or 2 bytes
$xs=base58_decode($x['id']);
if(((strlen($xs)!=63||substr($id,1)!=$x['id'])&&(strlen($xs)!=62||substr($id,2)!=$x['id']))||$height>17000){
_log("$x[id] - $id - Invalid hash");
return false;
}
}
if(!$acc->check_signature($info, $x['signature'], $x['public_key'])) { _log("$x[id] - Invalid signature"); return false; }
//verify the ecdsa signature
if(!$acc->check_signature($info, $x['signature'], $x['public_key'])) { _log("$x[id] - Invalid signature"); return false; }
return true;
}
// sign a transaction
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);
@@ -158,14 +189,14 @@ class Transaction {
}
//export a mempool transaction
public function export($id){
global $db;
$r=$db->row("SELECT * FROM mempool WHERE id=:id",array(":id"=>$id));
//unset($r['peer']);
return $r;
}
// get the transaction data as array
public function get_transaction($id){
global $db;
$acc=new Account;
@@ -188,6 +219,7 @@ class Transaction {
}
// return the transactions for a specific block id or height
public function get_transactions($height="", $id=""){
global $db;
$acc=new Account;
@@ -216,7 +248,7 @@ class Transaction {
}
// get a specific mempool transaction as array
public function get_mempool_transaction($id){
global $db;
$x=$db->row("SELECT * FROM mempool WHERE id=:id",array(":id"=>$id));