documentation + base58 fix
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
?>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
// a simple wrapper for pdo
|
||||
class db extends PDO {
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 &");
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
?>
|
||||
?>
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user