block-explorer/app/Console/Commands/BlockCommand.php
Ben van Hartingsveldt 3a6e10b467
Fix Redis instance
2025-05-25 19:51:23 +02:00

2253 lines
98 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\Address;
use App\Models\Block;
use App\Models\Claim;
use App\Models\ClaimStream;
use App\Models\Input;
use App\Models\Output;
use App\Models\Transaction;
use Carbon\Carbon;
use DateTime;
use DateTimeZone;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use InvalidArgumentException;
use Mdanter\Ecc\EccFactory;
use PDO;
use RedisException;
use Throwable;
class BlockCommand extends Command{
const mempooltxkey = 'lbc.mempooltx';
const pubKeyAddress = [0, 85];
const scriptAddress = [5, 122];
protected static $redis;
public static $rpcurl;
/**
* The console command description.
* @var string
*/
protected $description = 'LBRY Block Explorer - Block command';
/**
* The name and signature of the console command.
* @var string
*/
protected $signature = 'explorer:block {function?}';
public function __construct(){
parent::__construct();
self::$redis = Redis::connection()->client();
self::$rpcurl = config('lbry.rpc_url');
}
/**
* Execute the console command.
*/
public function handle(): void{
$function = $this->argument('function');
if($function){
$this->$function();
}else{
$this->warn('No arguments specified');
}
}
/**
* @return void
* @throws Throwable
*/
public function fixscripthashtx(): void{
$conn = DB::connection();
$otxs = Output::query()->select(['TransactionId'])->distinct(['TransactionId'])->where('Type','scripthash')->get();
foreach ($otxs as $otx) {
$txid = $otx->TransactionId;
$tx = Transaction::query()->select(['Hash'])->where('Id',$txid)->first();
$req = ['method' => 'getrawtransaction', 'params' => [$tx->Hash],'id'=>rand()];
$response = self::curl_json_post(self::$rpcurl, json_encode($req));
$json = json_decode($response);
$tx_result = $json->result;
$raw_tx = $tx_result;
$tx_data = self::decode_tx($raw_tx);
$all_tx_data = $this->txdb_data_from_decoded($tx_data);
foreach ($all_tx_data['outputs'] as $out) {
if ($out['Type'] != 'scripthash') {
continue;
}
// get the old address
$old_output = Output::query()->select(['Id', 'Addresses'])->where([
['TransactionId',$txid],
['Vout',$out['Vout']],
])->first();
if ($old_output) {
$old_addresses = json_decode($old_output->Addresses);
$old_address = $old_addresses[0];
$new_addresses = json_decode($out['Addresses']);
$new_address = $new_addresses[0];
// update the output with new addresses array
$conn->beginTransaction();
$conn->statement('UPDATE Outputs SET Addresses = ? WHERE Id = ?', [$out['Addresses'], $old_output->Id]);
// update the old address with the new one
$conn->statement('UPDATE Addresses SET Address = ? WHERE Address = ?', [$new_address, $old_address]);
$conn->commit();
echo "$old_address => $new_address\n";
}
}
}
}
public static function hex2str(string $hex): string{
$string = '';
for ($i = 0; $i < strlen($hex)-1; $i+=2){
$string .= chr(hexdec($hex[$i].$hex[$i+1]));
}
return $string;
}
public function updateclaimfees(): void{
self::lock('claimfees');
$conn = DB::connection();
try {
$stmt = $conn->getPdo()->query('SELECT CS.Id, CS.Stream FROM ClaimStreams CS JOIN Claims C ON C.Id = CS.Id WHERE C.Fee = 0 AND C.Id <= 11462 ORDER BY Id ASC');
$claims = $stmt->fetchAll(PDO::FETCH_OBJ);
foreach ($claims as $claim) {
$stream = json_decode($claim->Stream);
if (isset($stream->metadata->fee) && $stream->metadata->fee->amount > 0) {
$fee = $stream->metadata->fee->amount;
$currency = $stream->metadata->fee->currency;
$conn->statement('UPDATE Claims SET Fee = ?, FeeCurrency = ? WHERE Id = ?', [$fee, $currency, $claim->Id]);
echo "Updated fee for claim ID: $claim->Id. Fee: $fee, Currency: $currency\n";
}
}
} catch (Exception $e) {
print_r($e);
}
self::unlock('claimfees');
}
/**
* @return void
* @throws RedisException
* @throws Throwable
*/
public function buildclaimindex(): void{
self::lock('buildindex');
// start with all txs
$decoder_url = 'http://127.0.0.1:5000';
$conn = DB::connection();
$redis_key = 'claim.oid';
$last_claim_oid = self::$redis->exists($redis_key) ? self::$redis->get($redis_key) : 0;
try {
$stmt = $conn->getPdo()->query('SELECT COUNT(Id) AS RecordCount FROM Outputs WHERE Id > ?', [$last_claim_oid]);
$count = min(500000, $stmt->fetch(PDO::FETCH_OBJ)->RecordCount);
$idx = 0;
$stmt = $conn->getPdo()->query('SELECT O.Id, O.TransactionId, O.Vout, O.ScriptPubKeyAsm, T.Hash, IFNULL(T.TransactionTime, T.CreatedTime) AS TxTime FROM Outputs O ' .
'JOIN Transactions T ON T.Id = O.TransactionId WHERE O.Id > ? ORDER BY O.Id ASC LIMIT 500000',
[$last_claim_oid]);
while ($out = $stmt->fetch(PDO::FETCH_OBJ)) {
$idx++;
$idx_str = str_pad($idx, strlen($count), '0', STR_PAD_LEFT);
$txid = $out->TransactionId;
$vout = $out->Vout;
if (strpos($out->ScriptPubKeyAsm, 'OP_CLAIM_NAME') !== false) {
// check if the claim already exists in the claims table
$stmt2 = $conn->getPdo()->query('SELECT Id FROM Claims WHERE TransactionHash = ? AND Vout = ?', [$out->Hash, $out->Vout]);
$exist_claim = $stmt2->fetch(PDO::FETCH_OBJ);
if ($exist_claim) {
echo "[$idx_str/$count] claim already exists for [$out->Hash:$vout]. Skipping.\n";
continue;
}
$asm_parts = explode(' ', $out->ScriptPubKeyAsm, 4);
$name_hex = $asm_parts[1];
$claim_name = @pack('H*', $name_hex);
// decode claim
$url = sprintf("%s/claim_decode/%s", $decoder_url, $claim_name);
$json = null;
try {
$json = self::curl_json_get($url);
} catch (Exception $e) {
echo "[$idx_str/$count] claimdecode failed for [$out->Hash:$vout]. Skipping.\n";
continue;
}
$claim = json_decode($json);
if ($claim) {
$req = ['method' => 'getvalueforname', 'params' => [$claim_name],'id'=>rand()];
$json = null;
try {
$json = json_decode(self::curl_json_post(self::$rpcurl, json_encode($req)));
if (!$json) {
echo "[$idx_str/$count] getvalueforname failed for [$out->Hash:$vout]. Skipping.\n";
continue;
}
} catch (Exception $e) {
echo "[$idx_str/$count] getvalueforname failed for [$out->Hash:$vout]. Skipping.\n";
continue;
}
echo "[$idx_str/$count] claim found for [$out->Hash:$vout]. Processing claim... \n";
$claim_data = [];
$claim_id = $json->result->claimId;
$tx_dt = DateTime::createFromFormat('U', $out->TxTime);
$claim_stream_data = null;
if ($claim->claimType === 'streamType') {
// Build claim object to save
$claim_data = [
'ClaimId' => $claim_id,
'TransactionHash' => $out->Hash,
'Vout' => $out->Vout,
'Name' => $claim_name,
'Version' => $claim->version,
'ClaimType' => 2, // streamType
'ContentType' => isset($claim->stream->source->contentType) ? $claim->stream->source->contentType : null,
'Title' => isset($claim->stream->metadata->title) ? $claim->stream->metadata->title : null,
'Description' => isset($claim->stream->metadata->description) ? $claim->stream->metadata->description : null,
'Language' => isset($claim->stream->metadata->language) ? $claim->stream->metadata->language : null,
'Author' => isset($claim->stream->metadata->author) ? $claim->stream->metadata->author : null,
'ThumbnailUrl' => isset($claim->stream->metadata->thumbnail) ? $claim->stream->metadata->thumbnail : null,
'IsNSFW' => isset($claim->stream->metadata->nsfw) ? $claim->stream->metadata->nsfw : 0,
'Fee' => isset($claim->stream->metadata->fee) ? $claim->stream->metadata->fee->amount : 0,
'FeeCurrency' => isset($claim->stream->metadata->fee) ? $claim->stream->metadata->fee->currency : 0,
'Created' => $tx_dt->format('Y-m-d H:i:s'),
'Modified' => $tx_dt->format('Y-m-d H:i:s')
];
$claim_stream_data = [
'Stream' => json_encode($claim->stream)
];
if (isset($claim->publisherSignature)) {
$sig_claim = Claim::query()->select(['Id', 'ClaimId', 'Name'])->where('ClaimId',$claim->publisherSignature->certificateId)->first();
if ($sig_claim) {
$claim_data['PublisherId'] = $sig_claim->ClaimId;
$claim_data['PublisherName'] = $sig_claim->Name;
}
}
} else {
$claim_data = [
'ClaimId' => $claim_id,
'TransactionHash' => $out->Hash,
'Vout' => $out->Vout,
'Name' => $claim_name,
'Version' => $claim->version,
'ClaimType' => 1,
'Certificate' => json_encode($claim->certificate),
'Created' => $tx_dt->format('Y-m-d H:i:s'),
'Modified' => $tx_dt->format('Y-m-d H:i:s')
];
}
$conn->beginTransaction();
$data_error = false;
$claim_entity = new Claim($claim_data);
$res = $claim_entity->save();
if (!$res) {
$data_error = true;
echo "[$idx_str/$count] claim for [$out->Hash:$vout] FAILED to save.\n";
}
if (!$data_error) {
if ($claim_stream_data) {
$claim_stream_data['Id'] = $claim_entity->Id;
$claim_stream_entity = new ClaimStream($claim_stream_data);
$res = $claim_stream_entity->save();
if (!$res) {
$data_error = true;
}
}
}
if (!$data_error) {
$conn->commit();
echo "[$idx_str/$count] claim for [$out->Hash:$vout] indexed.\n";
} else {
$conn->rollback();
echo "[$idx_str/$count] claim for [$out->Hash:$vout] NOT indexed. Rolled back.\n";
}
} else {
echo "[$idx_str/$count] claim for [$out->Hash:$vout] could not be decoded. Skipping.\n";
}
} else {
echo "[$idx_str/$count] no claim found for [$out->Hash:$vout]. Skipping.\n";
}
self::$redis->set($redis_key, $out->Id);
}
} catch (Exception $e) {
// continue
print_r($e);
}
self::unlock('buildindex');
}
// TODO: Refactor for unique claim identification by claim_id instead of using the claim name.
protected function _getclaimfortxout($pubkeyasm, $tx_hash, $vout, $tx_time = null): array{
$claim_data = null;
$claim_stream_data = null;
$asm_parts = explode(' ', $pubkeyasm, 4);
$name_hex = $asm_parts[1];
$claim_name = @pack('H*', $name_hex);
// decode claim
$decoder_url = 'http://127.0.0.1:5000';
$url = sprintf("%s/claim_decode/%s", $decoder_url, $claim_name);
$json = null;
try {
$json = self::curl_json_get($url);
} catch (Exception $e) {
echo "***claimdecode failed for [$tx_hash:$vout]. Skipping.\n";
}
if ($json) {
$claim = json_decode($json);
if ($claim) {
if (strpos($claim_name, '#') !== false) {
$claim_name = substr($claim_name, 0, strpos($claim_name, '#'));
}
$req = ['method' => 'getvalueforname', 'params' => [$claim_name],'id'=>rand()];
$json = null;
try {
$json = json_decode(self::curl_json_post(self::$rpcurl, json_encode($req)));
if ($json) {
$claim_data = [];
$claim_id = $json->result->claimId;
$now = new DateTime('now', new DateTimeZone('UTC'));
$tx_dt = ($tx_time != null) ? $tx_time : $now;
if ($claim->claimType === 'streamType') {
// Build claim object to save
$claim_data = [
'ClaimId' => $claim_id,
'TransactionHash' => $tx_hash,
'Vout' => $vout,
'Name' => $claim_name,
'Version' => $claim->version,
'ClaimType' => 2, // streamType
'ContentType' => isset($claim->stream->source->contentType) ? $claim->stream->source->contentType : null,
'Title' => isset($claim->stream->metadata->title) ? $claim->stream->metadata->title : null,
'Description' => isset($claim->stream->metadata->description) ? $claim->stream->metadata->description : null,
'Language' => isset($claim->stream->metadata->language) ? $claim->stream->metadata->language : null,
'Author' => isset($claim->stream->metadata->author) ? $claim->stream->metadata->author : null,
'ThumbnailUrl' => isset($claim->stream->metadata->thumbnail) ? $claim->stream->metadata->thumbnail : null,
'IsNSFW' => isset($claim->stream->metadata->nsfw) ? $claim->stream->metadata->nsfw : 0,
'Fee' => isset($claim->stream->metadata->fee) ? $claim->stream->metadata->fee->amount : 0,
'FeeCurrency' => isset($claim->stream->metadata->fee) ? $claim->stream->metadata->fee->currency : 0,
'Created' => $tx_dt->format('Y-m-d H:i:s'),
'Modified' => $tx_dt->format('Y-m-d H:i:s')
];
$claim_stream_data = [
'Stream' => json_encode($claim->stream)
];
if (isset($claim->publisherSignature)) {
$sig_claim = Claim::query()->select(['Id', 'ClaimId', 'Name'])->where('ClaimId',$claim->publisherSignature->certificateId)->first();
if ($sig_claim) {
$claim_data['PublisherId'] = $sig_claim->ClaimId;
$claim_data['PublisherName'] = $sig_claim->Name;
}
}
} else {
$claim_data = [
'ClaimId' => $claim_id,
'TransactionHash' => $tx_hash,
'Vout' => $vout,
'Name' => $claim_name,
'Version' => $claim->version,
'ClaimType' => 1,
'Certificate' => json_encode($claim->certificate),
'Created' => $tx_dt->format('Y-m-d H:i:s'),
'Modified' => $tx_dt->format('Y-m-d H:i:s')
];
}
}
} catch (Exception $e) {
echo "***getvalueforname failed for [$out->Hash:$vout]. Skipping.\n";
}
}
}
return ['claim_data' => $claim_data, 'claim_stream_data' => $claim_stream_data];
}
public function fixzerooutputs(): void{
self::lock('zerooutputs');
$conn = DB::connection();
/** 2017-06-12 21:38:07 **/
//$last_fixed_txid = self::$redis->exists('fix.txid') ? self::$redis->get('fix.txid') : 0;
try {
$stmt = $conn->getPdo()->query('SELECT Id FROM Transactions WHERE Created >= ? AND Created <= ? LIMIT 1000000', ['2017-06-15 20:44:50', '2017-06-16 08:02:09']);
$txids = $stmt->fetchAll(PDO::FETCH_OBJ);
$count = count($txids);
$idx = 0;
foreach ($txids as $distincttx) {
$idx++;
$idx_str = str_pad($idx, strlen($count), '0', STR_PAD_LEFT);
$txid = $distincttx->Id;
echo "[$idx_str/$count] Processing txid: $txid... ";
$total_diff = 0;
// findtx
$start_ms = round(microtime(true) * 1000);
$tx = Transaction::query()->select(['Hash'])->where('Id',$txid)->first();
$diff_ms = (round(microtime(true) * 1000)) - $start_ms;
$total_diff += $diff_ms;
echo "findtx took {$diff_ms}ms. ";
// Get the inputs and outputs
// Get the raw transaction (Use getrawtx daemon instead (for speed!!!)
// getraw
$req = ['method' => 'getrawtransaction', 'params' => [$tx->Hash],'id'=>rand()];
$start_ms = round(microtime(true) * 1000);
$response = self::curl_json_post(self::$rpcurl, json_encode($req));
$diff_ms = (round(microtime(true) * 1000)) - $start_ms;
$total_diff += $diff_ms;
echo "getrawtx took {$diff_ms}ms. ";
$start_ms = round(microtime(true) * 1000);
$json = json_decode($response);
$tx_result = $json->result;
$raw_tx = $tx_result;
$tx_data = self::decode_tx($raw_tx);
$all_tx_data = $this->txdb_data_from_decoded($tx_data);
$inputs = $all_tx_data['inputs'];
$outputs = $all_tx_data['outputs'];
$addr_id_map = [];
$addr_id_drcr = []; // debits and credits grouped by address
$total_tx_value = 0;
$diff_ms = (round(microtime(true) * 1000)) - $start_ms;
$total_diff += $diff_ms;
echo "decodetx took {$diff_ms}ms. ";
// Create / update addresses
$addr_id_map = [];
$new_addr_map = [];
foreach($all_tx_data['addresses'] as $address => $addro) {
$prev_addr = Address::query()->select(['Id'])->where('Address',$address)->first();
if (!$prev_addr) {
$new_addr = [
'Address' => $address,
'FirstSeen' => $block_ts->format('Y-m-d H:i:s')
];
$entity = new Address($new_addr);
$res = $entity->save();
if (!$res) {
$data_error = true;
} else {
$addr_id_map[$address] = $entity->Id;
}
$new_addr_map[$address] = 1;
} else {
$addr_id_map[$address] = $prev_addr->Id;
}
}
$start_ms = round(microtime(true) * 1000);
$num_outputs = count($outputs);
foreach ($outputs as $out) {
$vout = $out['Vout'];
$total_tx_value = bcadd($total_tx_value, $out['Value'], 8);
// check if the output exists
$stmt = $conn->getPdo()->query('SELECT Id FROM Outputs WHERE TransactionId = ? AND Vout = ?', [$txid, $vout]);
$exist_output = $stmt->fetch(PDO::FETCH_OBJ);
if (!$exist_output) {
$out['TransactionId'] = $txid;
$out_entity = new Output($out);
//$stmt->execute('INSERT INTO Outputs')
$conn->statement('INSERT INTO Outputs (TransactionId, Vout, Value, Type, ScriptPubKeyAsm, ScriptPubKeyHex, RequiredSignatures, Hash160, Addresses, Created, Modified) '.
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())',
[$out['TransactionId'],
$out['Vout'],
$out['Value'],
$out['Type'],
$out['ScriptPubKeyAsm'],
$out['ScriptPubKeyHex'],
$out['RequiredSignatures'],
$out['Hash160'],
$out['Addresses']
]);
// get the last insert id
$stmt = $conn->getPdo()->query('SELECT LAST_INSERT_ID() AS outputId');
$linsert = $stmt->fetch(PDO::FETCH_OBJ);
$out_entity->Id = $linsert->outputId;
if ($out_entity->Id === 0) {
$data_error = true;
break;
}
$json_addr = json_decode($out['Addresses']);
$address = $json_addr[0];
// Get the address ID
$addr_id = -1;
if (isset($addr_id_map[$address])) {
$addr_id = $addr_id_map[$address];
} else {
$src_addr = Address::query()->select(['Id'])->where('Address',$address)->first();
if ($src_addr) {
$addr_id = $src_addr->Id;
$addr_id_map[$address] = $addr_id;
}
}
if ($addr_id > -1) {
$conn->statement('INSERT INTO OutputsAddresses (OutputId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE OutputId = OutputId', [$out_entity->Id, $addr_id]);
$conn->statement('INSERT INTO TransactionsAddresses (TransactionId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE TransactionId = TransactionId', [$txid, $addr_id]);
}
if ($addr_id > -1 && isset($new_addr_map[$address])) {
if (!isset($addr_id_drcr[$addr_id])) {
$addr_id_drcr[$addr_id] = ['debit' => 0, 'credit' => 0];
}
$addr_id_drcr[$addr_id]['credit'] = bcadd($addr_id_drcr[$addr_id]['credit'], $out['Value'], 8);
// Update the Received amount for the address based on the output
$conn->statement('UPDATE Addresses SET TotalReceived = TotalReceived + ? WHERE Id = ?', [$out['Value'], $addr_id]);
}
}
}
$diff_ms = (round(microtime(true) * 1000)) - $start_ms;
$total_diff += $diff_ms;
echo "$num_outputs output(s) took {$diff_ms}ms. ";
// Fix the input values
$start_ms = round(microtime(true) * 1000);
$num_inputs = count($inputs);
foreach ($inputs as $in) {
if (isset($in['PrevoutHash'])) {
$prevout_hash = $in['PrevoutHash'];
$in_prevout = $in['PrevoutN'];
$prevout_tx_id = -1;
$prevout_tx = Transaction::query()->select(['Id'])->where('Hash',$prevout_hash)->first();
if (!$prevout_tx) {
continue;
}
$prevout_tx_id = $prevout_tx->Id;
$stmt = $conn->getPdo()->query('SELECT Value, Addresses FROM Outputs WHERE TransactionId = ? AND Vout = ?', [$prevout_tx_id, $in_prevout]);
$src_output = $stmt->fetch(PDO::FETCH_OBJ);
if ($src_output) {
$in['Value'] = $src_output->Value;
//$conn->execute('UPDATE Inputs SET Value = ? WHERE TransactionId = ? AND PrevoutHash = ? AND PrevoutN = ?', [$in['Value'], $txid, $prevout_hash, $in_prevout]);
// Check if the input exists
$stmt = $conn->getPdo()->query('SELECT Id FROM Inputs WHERE TransactionId = ? AND PrevoutHash = ? AND PrevoutN = ?', [$txid, $prevout_hash, $in_prevout]);
$exist_input = $stmt->fetch(PDO::FETCH_OBJ);
if (!$exist_input) {
$json_addr = json_decode($src_output->Addresses);
$address = $json_addr[0];
// Get the address ID
$addr_id = -1;
if (isset($addr_id_map[$address])) {
$addr_id = $addr_id_map[$address];
} else {
$src_addr = Address::query()->select(['Id'])->where('Address',$address)->first();
if ($src_addr) {
$addr_id = $src_addr->Id;
$addr_id_map[$address] = $addr_id;
}
}
$in_entity = new Input($in);
$in['TransactionId'] = $txid;
if ($addr_id > -1) {
$in['AddressId'] = $addr_id;
}
$conn->statement('INSERT INTO Inputs (TransactionId, TransactionHash, AddressId, PrevoutHash, PrevoutN, Sequence, Value, ScriptSigAsm, ScriptSigHex, Created, Modified) ' .
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())',
[$in['TransactionId'],
$in['TransactionHash'],
isset($in['AddressId']) ? $in['AddressId'] : null,
$in['PrevoutHash'],
$in['PrevoutN'],
$in['Sequence'],
isset($in['Value']) ? $in['Value'] : 0,
$in['ScriptSigAsm'],
$in['ScriptSigHex']
]);
// get last insert id
$stmt = $conn->getPdo()->query('SELECT LAST_INSERT_ID() AS inputId');
$linsert = $stmt->fetch(PDO::FETCH_OBJ);
$in_entity->Id = $linsert->inputId;
if ($in_entity->Id === 0) {
$data_error = true;
break;
}
if ($addr_id > -1) {
$conn->statement('INSERT INTO InputsAddresses (InputId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE InputId = InputId', [$in_entity->Id, $addr_id]);
$conn->statement('INSERT INTO TransactionsAddresses (TransactionId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE TransactionId = TransactionId', [$txid, $addr_id]);
}
if ($addr_id > -1 && isset($new_addr_map[$address])) {
if (!isset($addr_id_drcr[$addr_id])) {
$addr_id_drcr[$addr_id] = ['debit' => 0, 'credit' => 0];
}
$addr_id_drcr[$addr_id]['debit'] = bcadd($addr_id_drcr[$addr_id]['debit'], $in['Value'], 8);
// Update total sent
$conn->statement('UPDATE Addresses SET TotalSent = TotalSent + ? WHERE Id = ?', [$in['Value'], $addr_id]);
}
}
}
}
}
$diff_ms = (round(microtime(true) * 1000)) - $start_ms;
$total_diff += $diff_ms;
echo "$num_inputs input(s) took {$diff_ms}ms. ";
// update tx time
$start_ms = round(microtime(true) * 1000);
$upd_addr_ids = [];
//$conn->execute('UPDATE Transactions SET Value = ? WHERE Id = ?', [$total_tx_value, $txid]);
foreach ($addr_id_drcr as $addr_id => $drcr) {
try {
$conn->statement('UPDATE TransactionsAddresses SET DebitAmount = ?, CreditAmount = ? WHERE TransactionId = ? AND AddressId = ?',
[$drcr['debit'], $drcr['credit'], $txid, $addr_id]);
} catch (Exception $e) {
print_r($e);
$data_error = true;
break;
}
}
//self::$redis->set('fix.txid', $txid);
$diff_ms = (round(microtime(true) * 1000)) - $start_ms;
$total_diff += $diff_ms;
echo "update took {$diff_ms}ms. Total {$total_diff} ms.\n";
}
} catch (Exception $e) {
print_r($e);
}
self::unlock('zerooutputs');
}
public function addrtxamounts(): void{
set_time_limit(0);
self::lock('addrtxamounts');
try {
$conn = DB::connection();
$stmt = $conn->getPdo()->query('SELECT TransactionId, AddressId FROM TransactionsAddresses WHERE DebitAmount = 0 AND CreditAmount = 0 LIMIT 1000000');
$txaddresses = $stmt->fetchAll(PDO::FETCH_OBJ);
$count = count($txaddresses);
$idx = 0;
echo "Processing $count tx address combos...\n";
foreach ($txaddresses as $txaddr) {
$idx++;
$idx_str = str_pad($idx, strlen($count), '0', STR_PAD_LEFT);
// Check the inputs
$stmt = $conn->getPdo()->query('SELECT SUM(I.Value) AS DebitAmount FROM Inputs I JOIN InputsAddresses IA ON IA.InputId = I.Id WHERE I.TransactionId = ? AND IA.AddressId = ?',
[$txaddr->TransactionId, $txaddr->AddressId]);
$res = $stmt->fetch(PDO::FETCH_OBJ);
$debitamount = $res->DebitAmount ? $res->DebitAmount : 0;
$stmt = $conn->getPdo()->query('SELECT SUM(O.Value) AS CreditAmount FROM Outputs O JOIN OutputsAddresses OA ON OA.OutputId = O.Id WHERE O.TransactionId = ? AND OA.AddressId = ?',
[$txaddr->TransactionId, $txaddr->AddressId]);
$res = $stmt->fetch(PDO::FETCH_OBJ);
$creditamount = $res->CreditAmount ? $res->CreditAmount : 0;
echo "[$idx_str/$count] Updating tx $txaddr->TransactionId, address id $txaddr->AddressId with debit amount: $debitamount, credit amount: $creditamount... ";
$conn->statement('UPDATE TransactionsAddresses SET DebitAmount = ?, CreditAmount = ? WHERE TransactionId = ? AND AddressId = ?',
[$debitamount, $creditamount, $txaddr->TransactionId, $txaddr->AddressId]);
echo "Done.\n";
}
} catch (Exception $e) {
// failed
print_r($e);
}
self::unlock('addrtxamounts');
}
/**
* @param $tx_hash
* @param $block_ts
* @param $block_data
* @param $data_error
* @return void
* @throws Exception
*/
private function processtx($tx_hash, $block_ts, $block_data, &$data_error): void{
// Get the raw transaction (Use getrawtx daemon instead (for speed!!!)
$req = ['method' => 'getrawtransaction', 'params' => [$tx_hash],'id'=>rand()];
$response = self::curl_json_post(self::$rpcurl, json_encode($req));
$json = json_decode($response);
$tx_result = $json->result;
$raw_tx = $tx_result;
$tx_data = self::decode_tx($raw_tx);
$all_tx_data = $this->txdb_data_from_decoded($tx_data);
$conn = DB::connection();
// Create / update addresses
$addr_id_map = [];
foreach($all_tx_data['addresses'] as $address => $addrss) {
$prev_addr = Address::query()->select(['Id'])->where('Address',$address)->first();
if (!$prev_addr) {
$new_addr = [
'Address' => $address,
'FirstSeen' => $block_ts->format('Y-m-d H:i:s'),
];
$entity = new Address($new_addr);
$res = $entity->save();
if (!$res) {
$data_error = true;
} else {
$addr_id_map[$address] = $entity->Id;
}
} else {
$addr_id_map[$address] = $prev_addr->Id;
}
}
$addr_id_drcr = []; // debits and credits grouped by address
$numeric_tx_id = -1;
if (!$data_error) {
// Create transaction
$new_tx = $all_tx_data['tx'];
$total_tx_value = 0;
foreach ($all_tx_data['outputs'] as $out) {
$total_tx_value = bcadd($total_tx_value, $out['Value'], 8);
}
if ($block_data) {
$new_tx['BlockHash'] = $block_data['hash'];
$new_tx['TransactionTime'] = $block_data['time'];
}
$new_tx['TransactionSize'] = ((strlen($raw_tx)) / 2);
$new_tx['InputCount'] = count($all_tx_data['inputs']);
$new_tx['OutputCount'] = count($all_tx_data['outputs']);
$new_tx['Hash'] = $tx_hash;
$new_tx['Value'] = $total_tx_value;
$new_tx['Raw'] = $raw_tx;
$tx_entity = new Transaction($new_tx);
$conn->statement('INSERT INTO Transactions (Version, LockTime, BlockHash, TransactionTime, InputCount, OutputCount, TransactionSize, Hash, Value, Raw, Created, Modified) VALUES ' .
'(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())',
[
$new_tx['Version'],
$new_tx['LockTime'],
isset($new_tx['BlockHash']) ? $new_tx['BlockHash'] : null,
isset($new_tx['TransactionTime']) ? $new_tx['TransactionTime'] : null,
$new_tx['InputCount'],
$new_tx['OutputCount'],
$new_tx['TransactionSize'],
$new_tx['Hash'],
$new_tx['Value'],
$new_tx['Raw']
]);
$stmt = $conn->getPdo()->query('SELECT LAST_INSERT_ID() AS txnId');
$linsert = $stmt->fetch(PDO::FETCH_OBJ);
$tx_entity->Id = $linsert->txnId;
if ($tx_entity->Id === 0) {
3;
} else {
$numeric_tx_id = $tx_entity->Id;
}
}
if (!$data_error && $numeric_tx_id > 0) {
// Create the inputs
$inputs = $all_tx_data['inputs'];
$outputs = $all_tx_data['outputs'];
foreach ($inputs as $in) {
$in['TransactionId'] = $numeric_tx_id;
$in['TransactionHash'] = $tx_hash;
if (isset($in['IsCoinbase']) && $in['IsCoinbase'] === 1) {
$in_entity = new Input($in);
$res = $in_entity->save();
if (!$res) {
$data_error = true;
break;
}
} else {
$in_tx_hash = $in['PrevoutHash'];
$in_prevout = $in['PrevoutN'];
// Find the corresponding previous output
$in_tx = Transaction::query()->select(['Id'])->where('Hash',$in_tx_hash)->first();
$src_output = null;
if ($in_tx) {
$stmt = $conn->getPdo()->query('SELECT Id, Value, Addresses FROM Outputs WHERE TransactionId = ? AND Vout = ?', [$in_tx->Id, $in_prevout]);
$src_output = $stmt->fetch(PDO::FETCH_OBJ);
if ($src_output) {
$in['Value'] = $src_output->Value;
$json_addr = json_decode($src_output->Addresses);
$in_addr_id = 0;
if (isset($addr_id_map[$json_addr[0]])) {
$in['AddressId'] = $addr_id_map[$json_addr[0]];
} else {
$in_addr = Address::query()->select(['Id'])->where('Address',$json_addr[0])->first();
if ($in_addr) {
$addr_id_map[$json_addr[0]] = $in_addr->Id;
$in['AddressId'] = $in_addr->Id;
}
}
}
}
$in_entity = new Input($in);
$conn->statement('INSERT INTO Inputs (TransactionId, TransactionHash, AddressId, PrevoutHash, PrevoutN, Sequence, Value, ScriptSigAsm, ScriptSigHex, Created, Modified) ' .
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())',
[$in['TransactionId'],
$in['TransactionHash'],
isset($in['AddressId']) ? $in['AddressId'] : null,
$in['PrevoutHash'],
$in['PrevoutN'],
$in['Sequence'],
isset($in['Value']) ? $in['Value'] : 0,
$in['ScriptSigAsm'],
$in['ScriptSigHex']
]);
// get last insert id
$stmt = $conn->getPdo()->query('SELECT LAST_INSERT_ID() AS inputId');
$linsert = $stmt->fetch(PDO::FETCH_OBJ);
$in_entity->Id = $linsert->inputId;
if ($in_entity->Id === 0) {
$data_error = true;
break;
}
// Update the src_output spent if successful
if ($src_output) {
try {
$conn->statement('UPDATE Outputs SET IsSpent = 1, SpentByInputId = ? WHERE Id = ?', [$in_entity->Id, $src_output->Id]);
$conn->statement('UPDATE Inputs SET PrevoutSpendUpdated = 1 WHERE Id = ?', [$in_entity->Id]);
} catch (Exception $e) {
$data_error = true;
break;
}
}
if (isset($in['AddressId']) && $in['AddressId'] > 0) {
$addr_id = $in['AddressId'];
if (!isset($addr_id_drcr[$addr_id])) {
$addr_id_drcr[$addr_id] = ['debit' => 0, 'credit' => 0];
}
$addr_id_drcr[$addr_id]['debit'] = bcadd($addr_id_drcr[$addr_id]['debit'], $in['Value'], 8);
try {
$conn->statement('INSERT INTO InputsAddresses (InputId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE InputId = InputId', [$in_entity->Id, $in['AddressId']]);
$conn->statement('UPDATE Addresses SET TotalSent = TotalSent + ? WHERE Id = ?', [$in['Value'], $in['AddressId']]);
$conn->statement('INSERT INTO TransactionsAddresses (TransactionId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE TransactionId = TransactionId', [$numeric_tx_id, $in['AddressId']]);
} catch (Exception $e) {
$data_error = true;
break;
}
}
}
}
foreach ($outputs as $out) {
$out['TransactionId'] = $numeric_tx_id;
$out_entity = new Output($out);
//$stmt->execute('INSERT INTO Outputs')
$conn->statement('INSERT INTO Outputs (TransactionId, Vout, Value, Type, ScriptPubKeyAsm, ScriptPubKeyHex, RequiredSignatures, Hash160, Addresses, Created, Modified) '.
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())',
[$out['TransactionId'],
$out['Vout'],
$out['Value'],
$out['Type'],
$out['ScriptPubKeyAsm'],
$out['ScriptPubKeyHex'],
$out['RequiredSignatures'],
$out['Hash160'],
$out['Addresses']
]);
// get the last insert id
$stmt = $conn->getPdo()->query('SELECT LAST_INSERT_ID() AS outputId');
$linsert = $stmt->fetch(PDO::FETCH_OBJ);
$out_entity->Id = $linsert->outputId;
if ($out_entity->Id === 0) {
$data_error = true;
break;
}
$json_addr = json_decode($out['Addresses']);
$out_addr_id = 0;
if (isset($addr_id_map[$json_addr[0]])) {
$out_addr_id = $addr_id_map[$json_addr[0]];
} else {
$out_addr = Address::query()->select(['Id'])->where('Address',$json_addr[0])->first();
if ($out_addr) {
$addr_id_map[$json_addr[0]] = $out_addr->Id;
$out_addr_id = $out_addr->Id;
}
}
if ($out_addr_id > 0) {
$addr_id = $out_addr_id;
if (!isset($addr_id_drcr[$addr_id])) {
$addr_id_drcr[$addr_id] = ['debit' => 0, 'credit' => 0];
}
$addr_id_drcr[$addr_id]['credit'] = bcadd($addr_id_drcr[$addr_id]['credit'], $out['Value'], 8);
try {
$conn->statement('INSERT INTO OutputsAddresses (OutputId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE OutputId = OutputId', [$out_entity->Id, $out_addr_id]);
$conn->statement('UPDATE Addresses SET TotalReceived = TotalReceived + ? WHERE Id = ?', [$out['Value'], $out_addr_id]);
$conn->statement('INSERT INTO TransactionsAddresses (TransactionId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE TransactionId = TransactionId', [$numeric_tx_id, $out_addr_id]);
} catch (Exception $e) {
print_r($e);
$data_error = true;
break;
}
}
// create the claim if the asm pub key starts with OP_CLAIM_NAME
if (strpos($out['ScriptPubKeyAsm'], 'OP_CLAIM_NAME') !== false) {
$all_claim_data = $this->_getclaimfortxout($out['ScriptPubKeyAsm'], $tx_hash, $out['Vout'], $block_ts);
$claim = $all_claim_data['claim_data'];
$claim_stream_data = $all_claim_data['claim_stream_data'];
if (!$claim) {
continue;
}
if ($claim['ClaimType'] == 2 && !$claim_stream_data) {
echo "***claim stream data missing for streamType claim\n";
continue;
}
$claim_entity = new Claim($claim);
$res = $claim_entity->save();
if (!$res) {
echo "***claim could not be saved.\n";
continue;
}
if (!$data_error && $claim_stream_data) {
$claim_stream_data['Id'] = $claim_entity->Id;
$claim_stream_entity = new ClaimStream($claim_stream_data);
$res = $claim_stream_entity->save();
if (!$res) {
echo "***claim stream could not be saved.\n";
}
}
}
}
}
// update tx amounts
if (!$data_error) {
foreach ($addr_id_drcr as $addr_id => $drcr) {
try {
$conn->statement('UPDATE TransactionsAddresses SET DebitAmount = ?, CreditAmount = ?, TransactionTime = UTC_TIMESTAMP() WHERE TransactionId = ? AND AddressId = ?',
[$drcr['debit'], $drcr['credit'], $numeric_tx_id, $addr_id]);
} catch (Exception $e) {
print_r($e);
$data_error = true;
break;
}
}
}
}
/**
* @return void
* @throws Throwable
*/
public function parsetxs(): void{
set_time_limit(0);
self::lock('parsetxs');
// Get the minimum block with no processed transactions
echo "Parsing transactions...\n";
$conn = DB::connection();
//$conn->execute('SET foreign_key_checks = 0');
//$conn->execute('SET unique_checks = 0');
try {
$unproc_blocks = Block::query()->select(['Id', 'Height', 'Hash', 'TransactionHashes', 'BlockTime'])->where('TransactionsProcessed',0)->orderBy('Height')->get();
foreach ($unproc_blocks as $min_block) {
$tx_hashes = json_decode($min_block->TransactionHashes);
if ($tx_hashes && is_array($tx_hashes)) {
$block_time = $min_block->BlockTime;
$block_ts = DateTime::createFromFormat('U', $block_time);
$count = count($tx_hashes);
echo "Processing " . $count . " transaction(s) for block $min_block->Height ($min_block->Hash)...\n";
$data_error = false;
$conn->beginTransaction();
$idx = 0;
foreach ($tx_hashes as $tx_hash) {
$idx++;
$idx_str = str_pad($idx, strlen($count), '0', STR_PAD_LEFT);
echo "[$idx_str/$count] Processing tx hash: $tx_hash... ";
$total_diff = 0;
$start_ms = round(microtime(true) * 1000);
$exist_tx = Transaction::query()->select(['Id'])->where('Hash', $tx_hash)->first();
$end_ms = round(microtime(true) * 1000);
$diff_ms = $end_ms - $start_ms;
$total_diff += $diff_ms;
echo "findtx took {$diff_ms}ms. ";
if ($exist_tx) {
echo "Exists. Skipping.\n";
continue;
}
$start_ms = round(microtime(true) * 1000);
$this->processtx($tx_hash, $block_ts, ['hash' => $min_block->Hash, 'time' => $min_block->BlockTime], $data_error);
$diff_ms = round(microtime(true) * 1000) - $start_ms;
$total_diff += $diff_ms;
echo "tx took {$diff_ms}ms. Total {$total_diff}ms. ";
if (!$data_error && self::$redis && self::$redis->sismember(self::mempooltxkey, $tx_hash)) {
self::$redis->srem(self::mempooltxkey, $tx_hash);
echo "Removed $tx_hash from redis mempooltx.\n";
}
echo "Done.\n";
}
if (!$data_error) {
$conn->statement('UPDATE Blocks SET TransactionsProcessed = 1 WHERE Id = ?', [$min_block->Id]);
}
if ($data_error) {
echo "Rolling back!\n";
$conn->rollback();
throw new Exception('Data save failed!');
} else {
echo "Data committed.\n";
$conn->commit();
}
}
}
// Try to update txs with null BlockHash
$mempooltx = Transaction::query()->select(['Id', 'Hash'])->where('BlockHash','IS','NULL')->orderBy('Created')->get();
$idx = 0;
$count = count($mempooltx);
foreach ($mempooltx as $tx) {
$idx++;
$tx_hash = $tx->Hash;
$idx_str = ($count > 10 && $idx < 10) ? '0' . $idx : $idx;
echo "[$idx_str/$count] Processing tx hash: $tx_hash... ";
$stmt = $conn->getPdo()->query("SELECT Hash, BlockTime FROM Blocks WHERE TransactionHashes LIKE CONCAT('%', ?, '%') AND Height > ((SELECT MAX(Height) FROM Blocks) - 10000) ORDER BY Height ASC LIMIT 1", [$tx_hash]);
$block = $stmt->fetch(PDO::FETCH_OBJ);
if ($block) {
$upd_tx = ['Id' => $tx->Id, 'BlockHash' => $block->Hash, 'TransactionTime' => $block->BlockTime];
$upd_entity = new Transaction($upd_tx);
$upd_entity->save();
echo "Done.\n";
if (self::$redis && self::$redis->sismember(self::mempooltxkey, $tx_hash)) {
self::$redis->srem(self::mempooltxkey, $tx_hash);
echo "Removed $tx_hash from redis mempooltx.\n";
}
} else {
echo "Block not found.\n";
}
}
} catch (Exception $e) {
print_r($e);
}
//$conn->execute('SET foreign_key_checks = 1');
//$conn->execute('SET unique_checks = 1');
self::unlock('parsetxs');
}
/**
* @throws Throwable
*/
public function parsenewblocks(): void{
set_time_limit(0);
self::lock('parsenewblocks');
echo "Parsing new blocks...\n";
self::$redis = Redis::connection()->client();
try {
// Get the best block hash
$req = ['method' => 'getbestblockhash', 'params' => [],'id'=>rand()];
$response = self::curl_json_post(self::$rpcurl, json_encode($req));
$json = json_decode($response);
print_r($response); print_r($json);
$best_hash = $json->result;
$req = ['method' => 'getblock', 'params' => [$best_hash],'id'=>rand()];
$response = self::curl_json_post(self::$rpcurl, json_encode($req));
$json = json_decode($response);
$best_block = $json->result;
$max_block = Block::query()->select(['Hash', 'Height'])->orderByDesc('Height')->first();
if (!$max_block) {
self::unlock('parsenewblocks');
return;
}
$min_height = min($max_block->Height, $best_block->height);
$max_height = max($max_block->Height, $best_block->height);
$height_diff = $best_block->height - $max_block->Height;
if ($height_diff <= 0) {
self::unlock('parsenewblocks');
return;
}
$conn = DB::connection();
for ($curr_height = $min_height; $curr_height <= $max_height; $curr_height++) {
// get the block hash
$req = ['method' => 'getblockhash', 'params' => [$curr_height],'id'=>rand()];
$response = self::curl_json_post(self::$rpcurl, json_encode($req));
$json = json_decode($response);
$curr_block_hash = $json->result;
$next_block_hash = null;
if ($curr_height < $max_height) {
$req = ['method' => 'getblockhash', 'params' => [$curr_height + 1],'id'=>rand()];
$response = self::curl_json_post(self::$rpcurl, json_encode($req));
$json = json_decode($response);
$next_block_hash = $json->result;
}
$req = ['method' => 'getblock', 'params' => [$curr_block_hash],'id'=>rand()];
$response = self::curl_json_post(self::$rpcurl, json_encode($req));
$json = json_decode($response);
$curr_block = $json->result;
if ($curr_block->confirmations < 0) {
continue;
}
$next_block = null;
if ($next_block_hash != null) {
$req = ['method' => 'getblock', 'params' => [$next_block_hash],'id'=>rand()];
$response = self::curl_json_post(self::$rpcurl, json_encode($req));
$json = json_decode($response);
$next_block = $json->result;
}
if ($curr_block != null) {
$curr_block_ins = $this->blockdb_data_from_json($curr_block);
if ($next_block != null && $curr_block_ins['NextBlockHash'] == null) {
$curr_block_ins['NextBlockHash'] = $next_block->hash;
}
$block_data = $curr_block;
$block_id = -1;
// Make sure the block does not exist before inserting
$old_block = Block::query()->select(['Id'])->where('Hash',$block_data->hash)->first();
if (!$old_block) {
echo "Inserting block $block_data->height ($block_data->hash)... ";
$curr_block_entity = new Block($curr_block_ins);
$conn->statement('INSERT INTO Blocks (Bits, Chainwork, Confirmations, Difficulty, Hash, Height, MedianTime, MerkleRoot, NameClaimRoot, Nonce, PreviousBlockHash, NextBlockHash, BlockSize, Target, BlockTime, TransactionHashes, Version, VersionHex, Created, Modified) ' .
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, UTC_TIMESTAMP(), UTC_TIMESTAMP())',
[
$curr_block_entity->Bits,
$curr_block_entity->Chainwork,
$curr_block_entity->Confirmations,
$curr_block_ins['Difficulty'], // cakephp 3 why?
$curr_block_entity->Hash,
$curr_block_entity->Height,
$curr_block_entity->MedianTime,
$curr_block_entity->MerkleRoot,
$curr_block_entity->NameClaimRoot,
$curr_block_entity->Nonce,
$curr_block_entity->PreviousBlockHash,
$curr_block_entity->NextBlockHash,
$curr_block_entity->BlockSize,
$curr_block_entity->Target,
$curr_block_entity->BlockTime,
$curr_block_entity->TransactionHashes,
$curr_block_entity->Version,
$curr_block_entity->VersionHex,
]);
$stmt = $conn->getPdo()->query('SELECT LAST_INSERT_ID() AS lBlockId');
$linsert = $stmt->fetch(PDO::FETCH_OBJ);
$curr_block_entity->Id = $linsert->lBlockId;
$block_id = $curr_block_entity->Id;
echo "Done.\n";
} else {
echo "Updating block $block_data->height ($block_data->hash) with next block hash: " . $curr_block_ins['NextBlockHash'] . " and confirmations: " . $curr_block_ins['Confirmations'] . "... ";
$upd_block = ['Id' => $old_block->Id, 'NextBlockHash' => $curr_block_ins['NextBlockHash'], 'Confirmations' => $curr_block_ins['Confirmations']];
$upd_entity = new Block($upd_block);
$block_id = $old_block->Id;
echo "Done.\n";
}
$txs = $block_data->tx;
$data_error = false;
foreach ($txs as $tx_hash) {
// Check if the transactions exist and then update the BlockHash and TxTime
$tx = Transaction::query()->select(['Id'])->where('Hash',$tx_hash)->first();
if ($tx) {
$upd_tx_data = [
'Id' => $tx->Id,
'BlockHash' => $block_data->hash,
'TransactionTime' => $block_data->time
];
$upd_tx_entity = new Transaction($upd_tx_data);
$upd_tx_entity->save();
echo "Updated tx $tx_hash with block hash and time $block_data->time.\n";
} else {
// Doesn't exist, create a new transaction
echo "Inserting tx $tx_hash for block height $block_data->height... ";
$conn->beginTransaction();
$block_ts = \DateTime::createFromFormat('U', $block_data->time);
$this->processtx($tx_hash, $block_ts, ['hash' => $block_data->hash, 'time' => $block_data->time], $data_error);
if ($data_error) {
$conn->rollback();
echo "Insert failed.\n";
} else {
$conn->commit();
echo "Done.\n";
}
}
// Remove from redis if present
if (!$data_error && self::$redis && self::$redis->sismember(self::mempooltxkey, $tx_hash)) {
self::$redis->srem(self::mempooltxkey, $tx_hash);
echo "Removed $tx_hash from redis mempooltx.\n";
}
}
if (!$data_error && $block_id > -1) {
// set TransactionsProcessed to true
$conn->statement('UPDATE Blocks SET TransactionsProcessed = 1 WHERE Id = ?', [$block_id]);
}
}
}
} catch (Exception $e) {
print_r($e);
}
self::unlock('parsenewblocks');
}
/**
* @throws Throwable
*/
public function forevermempool(): void{
self::lock('forevermempool');
$conn = DB::connection();
while (true) {
try {
$data = ['method' => 'getrawmempool', 'params' => [],'id'=>rand()];
$res = self::curl_json_post(self::$rpcurl, json_encode($data));
$json = json_decode($res);
$txs = $json->result;
$now = new DateTime('now', new DateTimeZone('UTC'));
$data_error = false;
if (count($txs) === 0) {
// If no transactions found, that means there's nothing in the mempool. Clear redis
if (self::$redis) {
self::$redis->del(self::mempooltxkey);
echo "Empty rawmempool. Cleared mempool txs from redis.\n";
}
}
foreach ($txs as $tx_hash) {
// Check redis mempool txs
if (self::$redis && self::$redis->exists(self::mempooltxkey)) {
if (self::$redis->sismember(self::mempooltxkey, $tx_hash)) {
echo "Found processed tx hash: $tx_hash. Skipping.\n";
continue;
}
}
echo "Processing tx hash: $tx_hash... ";
$exist_tx = Transaction::query()->select(['Id'])->where('Hash',$tx_hash)->first();
if ($exist_tx) {
echo "Exists. Skipping.\n";
continue;
}
// Process the tx
$conn->beginTransaction();
$block_ts = new DateTime('now', new DateTimeZone('UTC'));
$this->processtx($tx_hash, $block_ts, null, $data_error);
if ($data_error) {
echo "Rolling back!\n";
$conn->rollback();
throw new Exception('Data save failed!');
} else {
echo "Data committed.\n";
$conn->commit();
// Save to redis to prevent the DB from behing hit again
if (self::$redis) {
self::$redis->sadd(self::mempooltxkey, $tx_hash);
}
}
}
} catch (Exception $e) {
echo "Mempool database error. Attempting to reconnect.\n";
// Final fix for MySQL server has gone away (hopefully)
try {
$conn->disconnect();
} catch (\Exception $e) {
// ignore possible disconnect errors
}
$conn->reconnect();
}
echo "*******************\n";
sleep(1);
}
self::unlock('forevermempool');
}
private function txdb_data_from_decoded($decoded_tx): array{
$tx = [
'Version' => $decoded_tx['version'],
'LockTime' => $decoded_tx['locktime']
];
$addresses = [];
$inputs = [];
$outputs = [];
$vin = $decoded_tx['vin'];
$vout = $decoded_tx['vout'];
if (is_array($vin)) {
foreach ($vin as $in) {
if (isset($in['coinbase'])) {
$inputs[] = [
'IsCoinbase' => 1,
'Coinbase' => $in['coinbase']
];
} else {
$inputs[] = [
'PrevoutHash' => $in['txid'],
'PrevoutN' => $in['vout'],
'ScriptSigAsm' => $in['scriptSig']['asm'],
'ScriptSigHex' => $in['scriptSig']['hex'],
'Sequence' => $in['sequence']
];
}
}
foreach ($vout as $out) {
$outputs[] = [
'Vout' => $out['vout'],
'Value' => bcdiv($out['value'], 100000000, 8),
'Type' => isset($out['scriptPubKey']['type']) ? $out['scriptPubKey']['type'] : '',
'ScriptPubKeyAsm' => isset($out['scriptPubKey']['asm']) ? $out['scriptPubKey']['asm'] : '',
'ScriptPubKeyHex' => isset($out['scriptPubKey']['hex']) ? $out['scriptPubKey']['hex'] : '',
'RequiredSignatures' => isset($out['scriptPubKey']['reqSigs']) ? $out['scriptPubKey']['reqSigs'] : '',
'Hash160' => isset($out['scriptPubKey']['hash160']) ? $out['scriptPubKey']['hash160'] : '',
'Addresses' => isset($out['scriptPubKey']['addresses']) ? json_encode($out['scriptPubKey']['addresses']) : null
];
if (isset($out['scriptPubKey']['addresses'])) {
foreach ($out['scriptPubKey']['addresses'] as $address) {
$addresses[$address] = $address;
}
}
}
}
return ['tx' => $tx, 'addresses' => $addresses, 'inputs' => $inputs, 'outputs' => $outputs];
}
private function blockdb_data_from_json($json_block): array{
return [
'Bits' => $json_block->bits,
'Chainwork' => $json_block->chainwork,
'Confirmations' => $json_block->confirmations,
'Difficulty' => $json_block->difficulty,
'Hash' => $json_block->hash,
'Height' => $json_block->height,
'MedianTime' => $json_block->mediantime,
'MerkleRoot' => $json_block->merkleroot,
'NameClaimRoot' => $json_block->nameclaimroot,
'Nonce' => $json_block->nonce,
'PreviousBlockHash' => isset($json_block->previousblockhash) ? $json_block->previousblockhash : null,
'NextBlockHash' => isset($json_block->nextblockhash) ? $json_block->nextblockhash : null,
'BlockSize' => $json_block->size,
'Target' => $json_block->target ?? null,//TODO: just $json_block->target
'BlockTime' => $json_block->time,
'TransactionHashes' => json_encode($json_block->tx),
'Version' => $json_block->version,
'VersionHex' => $json_block->versionHex
];
}
/**
* @param $url
* @param $data
* @param $headers
* @return bool|string
* @throws Exception
*/
private static function curl_json_post($url, $data, $headers = []): string{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
//Log::debug('Request execution completed.');
if ($response === false) {
$error = curl_error($ch);
$errno = curl_errno($ch);
curl_close($ch);
throw new Exception(sprintf('The request failed: %s', $error), $errno);
} else {
curl_close($ch);
}
// Close any open file handle
return $response;
}
private static function curl_json_get($url, $headers = []): string{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
//Log::debug('Request execution completed.');
if ($response === false) {
$error = curl_error($ch);
$errno = curl_errno($ch);
curl_close($ch);
throw new \Exception(sprintf('The request failed: %s', $error), $errno);
} else {
curl_close($ch);
}
// Close any open file handle
return $response;
}
private static $base58chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
public static $op_codes = [
['OP_0', 0],
['OP_PUSHDATA', 76],
'OP_PUSHDATA2',
'OP_PUSHDATA4',
'OP_1NEGATE',
'OP_RESERVED',
'OP_1',
'OP_2',
'OP_3',
'OP_4',
'OP_5',
'OP_6',
'OP_7',
'OP_8', 'OP_9', 'OP_10', 'OP_11', 'OP_12', 'OP_13', 'OP_14', 'OP_15', 'OP_16',
'OP_NOP', 'OP_VER', 'OP_IF', 'OP_NOTIF', 'OP_VERIF', 'OP_VERNOTIF', 'OP_ELSE', 'OP_ENDIF', 'OP_VERIFY',
'OP_RETURN', 'OP_TOALTSTACK', 'OP_FROMALTSTACK', 'OP_2DROP', 'OP_2DUP', 'OP_3DUP', 'OP_2OVER', 'OP_2ROT', 'OP_2SWAP',
'OP_IFDUP', 'OP_DEPTH', 'OP_DROP', 'OP_DUP', 'OP_NIP', 'OP_OVER', 'OP_PICK', 'OP_ROLL', 'OP_ROT',
'OP_SWAP', 'OP_TUCK', 'OP_CAT', 'OP_SUBSTR', 'OP_LEFT', 'OP_RIGHT', 'OP_SIZE', 'OP_INVERT', 'OP_AND',
'OP_OR', 'OP_XOR', 'OP_EQUAL', 'OP_EQUALVERIFY', 'OP_RESERVED1', 'OP_RESERVED2', 'OP_1ADD', 'OP_1SUB', 'OP_2MUL',
'OP_2DIV', 'OP_NEGATE', 'OP_ABS', 'OP_NOT', 'OP_0NOTEQUAL', 'OP_ADD', 'OP_SUB', 'OP_MUL', 'OP_DIV',
'OP_MOD', 'OP_LSHIFT', 'OP_RSHIFT', 'OP_BOOLAND', 'OP_BOOLOR',
'OP_NUMEQUAL', 'OP_NUMEQUALVERIFY', 'OP_NUMNOTEQUAL', 'OP_LESSTHAN',
'OP_GREATERTHAN', 'OP_LESSTHANOREQUAL', 'OP_GREATERTHANOREQUAL', 'OP_MIN', 'OP_MAX',
'OP_WITHIN', 'OP_RIPEMD160', 'OP_SHA1', 'OP_SHA256', 'OP_HASH160',
'OP_HASH256', 'OP_CODESEPARATOR', 'OP_CHECKSIG', 'OP_CHECKSIGVERIFY', 'OP_CHECKMULTISIG',
'OP_CHECKMULTISIGVERIFY', 'OP_NOP1', 'OP_NOP2', 'OP_NOP3', 'OP_NOP4', 'OP_NOP5', 'OP_CLAIM_NAME',
'OP_SUPPORT_CLAIM', 'OP_UPDATE_CLAIM',
['OP_SINGLEBYTE_END', 0xF0],
['OP_DOUBLEBYTE_BEGIN', 0xF000],
'OP_PUBKEY', 'OP_PUBKEYHASH',
['OP_INVALIDOPCODE', 0xFFFF]
];
public static $op_code = [
'00' => 'OP_0', // or OP_FALSE
'51' => 'OP_1', // or OP_TRUE
'61' => 'OP_NOP',
'6a' => 'OP_RETURN',
'6d' => 'OP_2DROP',
'75' => 'OP_DROP',
'76' => 'OP_DUP',
'87' => 'OP_EQUAL',
'88' => 'OP_EQUALVERIFY',
'a6' => 'OP_RIPEMD160',
'a7' => 'OP_SHA1',
'a8' => 'OP_SHA256',
'a9' => 'OP_HASH160',
'aa' => 'OP_HASH256',
'ac' => 'OP_CHECKSIG',
'ae' => 'OP_CHECKMULTISIG',
'b5' => 'OP_CLAIM_NAME',
'b6' => 'OP_SUPPORT_CLAIM',
'b7' => 'OP_UPDATE_CLAIM'
];
/*protected static function hash160_to_bc_address($h160, $addrType = 0) {
$vh160 = $c . $h160;
$h = self::_dhash($vh160);
$addr = $vh160 . substr($h, 0, 4);
return $addr;
//return self::base58_encode($addr);
}*/
protected static function _dhash($str, $raw = false): string{
return hash('sha256', hash('sha256', $str, true), $raw);
}
public static function _get_vint(&$string): int{
// Load the next byte, convert to decimal.
$decimal = hexdec(self::_return_bytes($string, 1));
// Less than 253: Not encoding extra bytes.
// More than 253, work out the $number of bytes using the 2^(offset)
$num_bytes = ($decimal < 253) ? 0 : 2 ^ ($decimal - 253);
// Num_bytes is 0: Just return the decimal
// Otherwise, return $num_bytes bytes (order flipped) and converted to decimal
return ($num_bytes == 0) ? $decimal : hexdec(self::_return_bytes($string, $num_bytes, true));
}
public static function hash160($string): string{
$bs = @pack("H*", $string);
return hash("ripemd160", hash("sha256", $bs, true));
}
public static function hash256($string): string{
$bs = @pack("H*", $string);
return hash("sha256", hash("sha256", $bs, true));
}
public static function hash160_to_address($hash160, $address_version = null): string{
$c = '';
if ($address_version == self::pubKeyAddress[0]) {
$c = dechex(self::pubKeyAddress[1]);
} else if ($address_version == self::scriptAddress[0]) {
$c = dechex(self::scriptAddress[1]);
}
$hash160 = $c . $hash160;
$addr = $hash160;
return self::base58_encode_checksum($addr);
}
public static function base58_encode_checksum($hex): string{
$checksum = self::hash256($hex);
$checksum = substr($checksum, 0, 8);
$hash = $hex . $checksum;
return self::base58_encode($hash);
}
public static function _decode_script($script): string{
$pos = 0;
$data = array();
while ($pos < strlen($script)) {
$code = hexdec(substr($script, $pos, 2)); // hex opcode.
$pos += 2;
if ($code < 1) {
// OP_FALSE
$push = '0';
} elseif ($code <= 75) {
// $code bytes will be pushed to the stack.
$push = substr($script, $pos, ($code * 2));
$pos += $code * 2;
} elseif ($code <= 78) {
// In this range, 2^($code-76) is the number of bytes to take for the *next* number onto the stack.
$szsz = pow(2, $code - 75); // decimal number of bytes.
$sz = hexdec(substr($script, $pos, ($szsz * 2))); // decimal number of bytes to load and push.
$pos += $szsz;
$push = substr($script, $pos, ($pos + $sz * 2)); // Load the data starting from the new position.
$pos += $sz * 2;
} elseif ($code <= 108/*96*/) {
// OP_x, where x = $code-80
$push = ($code - 80);
} else {
$push = $code;
}
$data[] = $push;
}
return implode(" ", $data);
}
/*public static function script_getopname($bytes_hex) {
$index = hexdec($bytes_hex) - 75;
$op = self::$op_codes[$index];
if (is_array($op)) {
if ($bytes_hex == dechex($op[1])) {
return $op[0];
}
}
if ()
return str_replace('OP_', '', $op);
}*/
public static function get_opcode($opname): ?int{
$len = count(self::$op_codes);
for ($i = 0; $i < $len; $i++) {
$op = self::$op_codes[$i];
$op = is_array($op) ? $op[0] : $op;
if ($op === $opname) {
return $i;
}
}
return null;
}
public static function match_decoded($decoded, $to_match): bool{
if (strlen($decoded) != strlen($to_match)) {
return false;
}
for ($i = 0; $i < count($decoded); $i++) {
$pushdata4 = self::get_opcode('OP_PUSHDATA4');
if ($to_match[$i] == $pushdata4 && ($pushdata4 >= $decoded[$i][0]) > 0) {
continue;
}
if ($to_match[$i] != $decoded[$i][0]) {
return false;
}
}
return true;
}
public static function base58_encode($hex): string{
if (strlen($hex) == 0) {
return '';
}
// Convert the hex string to a base10 integer
$num = gmp_strval(gmp_init($hex, 16), 58);
// Check that number isn't just 0 - which would be all padding.
if ($num != '0') {
$num = strtr($num, '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuv', self::$base58chars);
} else {
$num = '';
}
// Pad the leading 1's
$pad = '';
$n = 0;
while (substr($hex, $n, 2) == '00') {
$pad .= '1';
$n += 2;
}
return $pad . $num;
}
public static function decode_tx($raw_transaction): array{
$math = EccFactory::getAdapter();
/*$magic_byte = BitcoinLib::magicByte($magic_byte);
$magic_p2sh_byte = BitcoinLib::magicP2SHByte($magic_p2sh_byte);*/
$raw_transaction = trim($raw_transaction);
if (((bool)preg_match('/^[0-9a-fA-F]{2,}$/i', $raw_transaction) !== true)
|| (strlen($raw_transaction)) % 2 !== 0
) {
throw new \InvalidArgumentException("Raw transaction is invalid hex");
}
$txHash = hash('sha256', hash('sha256', pack("H*", trim($raw_transaction)), true));
$txid = self::_flip_byte_order($txHash);
$info = array();
$info['txid'] = $txid;
$info['version'] = $math->hexDec(self::_return_bytes($raw_transaction, 4, true));
/*if (!in_array($info['version'], array('0', '1'))) {
throw new \InvalidArgumentException("Invalid transaction version");
}*/
$input_count = self::_get_vint($raw_transaction);
if (!($input_count >= 0 && $input_count <= 4294967296)) {
throw new \InvalidArgumentException("Invalid input count");
}
$info['vin'] = self::_decode_inputs($raw_transaction, $input_count);
if ($info['vin'] == false) {
throw new \InvalidArgumentException("No inputs in transaction");
}
$output_count = self::_get_vint($raw_transaction);
if (!($output_count >= 0 && $output_count <= 4294967296)) {
throw new \InvalidArgumentException("Invalid output count");
}
$info['vout'] = self::_decode_outputs($raw_transaction, $output_count);
$info['locktime'] = $math->hexDec(self::_return_bytes($raw_transaction, 4));
return $info;
}
public static function _decode_inputs(&$raw_transaction, $input_count): array{
$inputs = array();
// Loop until $input count is reached, sequentially removing the
// leading data from $raw_transaction reference.
for ($i = 0; $i < $input_count; $i++) {
// Load the TxID (32bytes) and vout (4bytes)
$txid = self::_return_bytes($raw_transaction, 32, true);
$vout = self::_return_bytes($raw_transaction, 4, true);
// Script is prefixed with a varint that must be decoded.
$script_length = self::_get_vint($raw_transaction); // decimal number of bytes.
$script = self::_return_bytes($raw_transaction, $script_length);
// Build input body depending on whether the TxIn is coinbase.
if ($txid == '0000000000000000000000000000000000000000000000000000000000000000') {
$input_body = array('coinbase' => $script);
} else {
$input_body = array('txid' => $txid,
'vout' => hexdec($vout),
'scriptSig' => array('asm' => self::_decode_script($script), 'hex' => $script));
}
// Append a sequence number, and finally add the input to the array.
$input_body['sequence'] = hexdec(self::_return_bytes($raw_transaction, 4));
$inputs[$i] = $input_body;
}
return $inputs;
}
public static function _decode_outputs(&$tx, $output_count): array{
$math = EccFactory::getAdapter();
/*$magic_byte = BitcoinLib::magicByte($magic_byte);
$magic_p2sh_byte = BitcoinLib::magicP2SHByte($magic_p2sh_byte);*/
$outputs = array();
for ($i = 0; $i < $output_count; $i++) {
// Pop 8 bytes (flipped) from the $tx string, convert to decimal,
// and then convert to Satoshis.
$satoshis = $math->hexDec(self::_return_bytes($tx, 8, true));
// Decode the varint for the length of the scriptPubKey
$script_length = self::_get_vint($tx); // decimal number of bytes
$script = self::_return_bytes($tx, $script_length);
try {
$asm = self::_decode_scriptPubKey($script);
} catch (\Exception $e) {
$asm = null;
}
// Begin building scriptPubKey
$scriptPubKey = array(
'asm' => $asm,
'hex' => $script
);
// Try to decode the scriptPubKey['asm'] to learn the transaction type.
$txn_info = self::_get_transaction_type($scriptPubKey['asm']);
if ($txn_info !== false) {
$scriptPubKey = array_merge($scriptPubKey, $txn_info);
}
$outputs[$i] = array(
'value' => $satoshis,
'vout' => $i,
'scriptPubKey' => $scriptPubKey);
}
return $outputs;
}
/**
* @throws Exception
*/
public function fixoutputs(): void{
$sql = 'SELECT * FROM Outputs WHERE Id NOT IN (SELECT OutputId FROM OutputsAddresses)';
$conn = DB::connection();
$stmt = $conn->getPdo()->query($sql);
$outs = $stmt->fetchAll(PDO::FETCH_OBJ);
foreach ($outs as $out) {
$txn_info = self::_get_transaction_type($out->ScriptPubKeyAsm);
$out_data = [
'Id' => $out->Id,
'Type' => $txn_info['type'],
'RequiredSignatures' => $txn_info['reqSigs'],
'Hash160' => $txn_info['hash160'],
'Addresses' => json_encode($txn_info['addresses'])
];
$out_entity = new Output($out_data);
$out_entity->save();
// Fix the addresses
foreach ($txn_info['addresses'] as $address) {
$prev_addr = Address::query()->where('Address',$address)->first();
$addr_id = -1;
if ($prev_addr) {
$addr_id = $prev_addr->Id;
} else {
$dt = new DateTime($out->Created, new DateTimeZone('UTC'));
$new_addr = [
'Address' => $address,
'FirstSeen' => $dt->format('Y-m-d H:i:s')
];
$new_addr_entity = new Address($new_addr);
if ($new_addr_entity->save()) {
$addr_id = $new_addr_entity->Id;
}
}
if ($addr_id > -1) {
$conn->statement('INSERT INTO OutputsAddresses (OutputId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE OutputId = OutputId', [$out->Id, $addr_id]);
}
}
echo "Fixed output $out->Id with new data: " . print_r($out_data, true);
}
}
public function fixinputs(): void{
$sql = 'SELECT * FROM Inputs WHERE IsCoinbase <> 1 AND Id NOT IN (SELECT InputId FROM InputsAddresses)';
$conn = DB::connection();
$stmt = $conn->getPdo()->query($sql);
$ins = $stmt->fetchAll(PDO::FETCH_OBJ);
foreach ($ins as $in) {
$prev_tx_hash = $in->PrevoutHash;
$prev_n = $in->PrevoutN;
// Get the previous transaction
$prev_tx = Transaction::query()->select(['Id'])->where('Hash',$prev_tx_hash)->first();
if (!$prev_tx) {
echo "Previous tx for hash $prev_tx_hash not found.\n";
continue;
}
$prev_tx_id = $prev_tx->Id;
$src_output = Output::query()->where('TransactionId',$prev_tx_id)->where('Vout',$prev_n)->first(); //TODO: ->contain(['OutputAddresses'])
$in_data = ['Id' => $in->Id];
if ($src_output) {
$in_data['Value'] = $src_output->Value;
$in_data['AddressId'] = $src_output->OutputAddresses[0]->Id;
$in_entity = new Input($in_data);
if ($in_entity->save()) {
$conn->statement('INSERT INTO InputsAddresses (InputId, AddressId) VALUES (?, ?) ON DUPLICATE KEY UPDATE InputId = InputId', [$in->Id, $in_data['AddressId']]);
}
}
echo "Fixed input $in->Id with new data: " . print_r($in_data, true);
}
}
public static function _get_transaction_type($data): mixed{
//$magic_byte = BitcoinLib::magicByte($magic_byte);
//$magic_p2sh_byte = BitcoinLib::magicP2SHByte($magic_p2sh_byte);
$has_claim = (strpos($data, 'CLAIM') !== false);
$has_update_claim = (strpos($data, 'UPDATE_CLAIM') !== false);
$has_op_0 = (strpos($data, 'OP_0') !== false);
$data = explode(" ", trim($data));
// Define information about eventual transactions cases, and
// the position of the hash160 address in the stack.
$define = array();
$rule = array();
// Other standard: pay to pubkey hash
$define['p2pk'] = array('type' => 'pubkeyhash',
'reqSigs' => 1,
'data_index_for_hash' => 0);
$rule['p2pk'] = [
'0' => '/^[0-9a-f]+$/i',
'1' => '/^OP_CHECKSIG/'
];
// Pay to script hash
$define['p2sh'] = array('type' => 'scripthash',
'reqSigs' => 1,
'data_index_for_hash' => 1);
$rule['p2sh'] = array(
'0' => '/^OP_HASH160/',
'1' => '/^[0-9a-f]{40}$/i', // pos 1
'2' => '/^OP_EQUAL/');
// Non-standard (claim_name and support_claim)
$define['p2c'] = array('type' => 'nonstandard',
'reqSigs' => 1,
'data_index_for_hash' => 7);
$rule['p2c'] = [
'0' => '/^OP_CLAIM_NAME|OP_SUPPORT_CLAIM/',
'1' => '/^[0-9a-f]+$/i',
'2' => '/^[0-9a-f]+$/i',
'3' => '/^OP_2DROP/',
'4' => '/^OP_DROP/',
'5' => '/^OP_DUP/',
'6' => '/^OP_HASH160/',
'7' => '/^[0-9a-f]{40}$/i', // pos 7
'8' => '/^OP_EQUALVERIFY/',
'9' => '/^OP_CHECKSIG/',
];
// Non-standard (claim_name and support_claim)
$define['p2c2'] = array('type' => 'nonstandard',
'reqSigs' => 1,
'data_index_for_hash' => 7);
$rule['p2c2'] = [
'0' => '/^OP_CLAIM_NAME|OP_SUPPORT_CLAIM/',
'1' => '/^OP_0/',
'2' => '/^[0-9a-f]+$/i',
'3' => '/^OP_2DROP/',
'4' => '/^OP_DROP/',
'5' => '/^OP_DUP/',
'6' => '/^OP_HASH160/',
'7' => '/^[0-9a-f]{40}$/i', // pos 8
'8' => '/^OP_EQUALVERIFY/',
'9' => '/^OP_CHECKSIG/'
];
// update_claim
$define['p2uc'] = array('type' => 'nonstandard',
'reqSigs' => 1,
'data_index_for_hash' => 8);
$rule['p2uc'] = [
'0' => '/^OP_UPDATE_CLAIM/',
'1' => '/^[0-9a-f]+$/i',
'2' => '/^[0-9a-f]+$/i',
'3' => '/^[0-9a-f]+$/i',
'4' => '/^OP_2DROP/',
'5' => '/^OP_2DROP/',
'6' => '/^OP_DUP/',
'7' => '/^OP_HASH160/',
'8' => '/^[0-9a-f]{40}$/i', // pos 8
'9' => '/^OP_EQUALVERIFY/',
'10' => '/^OP_CHECKSIG/'
];
// Standard: pay to pubkey hash
$define['p2ph'] = array('type' => 'pubkeyhash',
'reqSigs' => 1,
'data_index_for_hash' => 2);
$rule['p2ph'] = array(
'0' => '/^OP_DUP/',
'1' => '/^OP_HASH160/',
'2' => '/^[0-9a-f]{40}$/i', // 2
'3' => '/^OP_EQUALVERIFY/',
'4' => '/^OP_CHECKSIG/');
if ($has_claim) {
unset($rule['p2ph']);
unset($rule['p2sh']);
if ($has_op_0) {
unset($rule['p2c']);
} else {
unset($rule['p2c2']);
}
if ($has_update_claim) {
unset($rule['p2c']);
} else {
unset($rule['p2uc']);
}
} else {
unset($rule['p2c']);
unset($rule['p2c2']);
unset($rule['p2uc']);
}
// Work out how many rules are applied in each case
$valid = array();
foreach ($rule as $tx_type => $def) {
$valid[$tx_type] = count($def);
}
// Attempt to validate against each of these rules.
$matches = [];
for ($index = 0; $index < count($data); $index++) {
$test = $data[$index];
foreach ($rule as $tx_type => $def) {
if (isset($def[$index])) {
preg_match($def[$index], $test, $matches[$tx_type]);
if (count($matches[$tx_type]) == 1) {
$valid[$tx_type]--;
break;
}
}
}
}
// Loop through rules, check if any transaction is a match.
foreach ($rule as $tx_type => $def) {
if ($valid[$tx_type] == 0) {
// Load predefined info for this transaction type if detected.
$return = $define[$tx_type];
if ($tx_type === 'p2pk') {
$return['hash160'] = self::hash160($data[$define[$tx_type]['data_index_for_hash']]);
$return['addresses'][0] = self::hash160_to_address($return['hash160'], self::pubKeyAddress[0]);
} else {
$return['hash160'] = $data[$define[$tx_type]['data_index_for_hash']];
$return['addresses'][0] = self::hash160_to_address($return['hash160'], ($tx_type === 'p2sh') ? self::scriptAddress[0] : self::pubKeyAddress[0]); // TODO: Pay to claim transaction?
}
unset($return['data_index_for_hash']);
}
}
return (!isset($return)) ? false : $return;
}
public static function _decode_scriptPubKey($script, $matchBitcoinCore = false): string{
$data = array();
while (strlen($script) !== 0) {
$byteHex = self::_return_bytes($script, 1);
$byteInt = hexdec($byteHex);
if (isset(self::$op_code[$byteHex])) {
// This checks if the OPCODE is defined from the list of constants.
if ($matchBitcoinCore && self::$op_code[$byteHex] == "OP_0") {
$data[] = '0';
} else if ($matchBitcoinCore && self::$op_code[$byteHex] == "OP_1") {
$data[] = '1';
} else {
$data[] = self::$op_code[$byteHex];
}
} elseif ($byteInt >= 0x01 && $byteInt <= 0x4e) {
// This checks if the OPCODE falls in the PUSHDATA range
if ($byteInt == 0x4d) {
// OP_PUSHDATA2
$byteInt = hexdec(self::_return_bytes($script, 2, true));
$data[] = self::_return_bytes($script, $byteInt);
} else if ($byteInt == 0x4e) {
// OP_PUSHDATA4
$byteInt = hexdec(self::_return_bytes($script, 4, true));
$data[] = self::_return_bytes($script, $byteInt);
} else if ($byteInt == 0x4c) {
$num_bytes = hexdec(self::_return_bytes($script, 1, true));
$data[] = self::_return_bytes($script, $num_bytes);
} else {
$data[] = self::_return_bytes($script, $byteInt);
}
} elseif ($byteInt >= 0x51 && $byteInt <= 0x60) {
// This checks if the CODE falls in the OP_X range
$data[] = $matchBitcoinCore ? ($byteInt - 0x50) : 'OP_' . ($byteInt - 0x50);
} else {
throw new \RuntimeException("Failed to decode scriptPubKey");
}
}
return implode(" ", $data);
}
/**
* Lock
* @param $process_name
* @return void
*/
public static function lock($process_name): void{
$lock = Cache::lock($process_name);
if(!$lock->get()){
echo $process_name." is already running.\n";
exit(0);
}
}
/**
* Unlock
* @param $process_name
* @return bool
*/
public static function unlock($process_name): bool{
Cache::lock($process_name)->forceRelease();
return true;
}
public static function _return_bytes(&$string, $byte_count, $reverse = false): string{
if (strlen($string) < $byte_count * 2) {
throw new InvalidArgumentException("Could not read enough bytes");
}
$requested_bytes = substr($string, 0, $byte_count * 2);
// Overwrite $string, starting $byte_count bytes from the start.
$string = substr($string, $byte_count * 2);
// Flip byte order if requested.
return ($reverse == false) ? $requested_bytes : self::_flip_byte_order($requested_bytes);
}
public static function _flip_byte_order($bytes): string{
return implode('', array_reverse(str_split($bytes, 2)));
}
/*public function parsehistoryblocks() {
set_time_limit(0);
header('Content-type: text/plain');
$block_hash = null;
// Get the minimum block hash first
$minBlock = $this->Blocks->find()->select(['Hash'])->order(['Height' => 'asc'])->first();
if (!$minBlock) {
// get the best block
$req = ['method' => 'status','id'=>rand()];
$response = self::curl_json_post(self::lbryurl, json_encode($req));
$json = json_decode($response);
$block_hash = $json->result->blockchain_status->best_blockhash;
} else {
$block_hash = $minBlock->Hash;
}
echo "Processing block: $block_hash... ";
$req = ['method' => 'block_show', 'params' => ['blockhash' => $block_hash],'id'=>rand()];
$response = self::curl_json_post(self::lbryurl, json_encode($req));
$json = json_decode($response);
$block_data = $json->result;
// Check if the block exists
$oldBlock = $this->Blocks->find()->select(['Id'])->where(['Hash' => $block_hash])->first();
if (!$oldBlock) {
// Block does not exist, create the block
$newBlock = $this->blockdb_data_from_json($block_data);
$entity = $this->Blocks->newEntity($newBlock);
$this->Blocks->save($entity);
}
echo "Done.\n";
$prevBlockHash = isset($block_data->previousblockhash) ? $block_data->previousblockhash : null;
do {
$oldBlock = $this->Blocks->find()->select(['Id'])->where(['Hash' => $prevBlockHash])->first();
$req = ['method' => 'block_show', 'params' => ['blockhash' => $prevBlockHash],'id'=>rand()];
$response = self::curl_json_post(self::lbryurl, json_encode($req));
$json = json_decode($response);
$block_data = $json->result;
$prevBlockHash = isset($block_data->previousblockhash) ? $block_data->previousblockhash : null;
if (!$oldBlock) {
echo "Inserting block: $block_data->hash... ";
$newBlock = $this->blockdb_data_from_json($block_data);
$entity = $this->Blocks->newEntity($newBlock);
$this->Blocks->save($entity);
} else {
echo "Updating block: $block_data->hash with confirmations: $block_data->confirmations... ";
$updData = ['Id' => $oldBlock->Id, 'Confirmations' => $block_data->confirmations];
$entity = $this->Blocks->newEntity($newBlock);
$this->Blocks->save($entity);
}
echo "Done.\n";
} while($prevBlockHash != null && strlen(trim($prevBlockHash)) > 0);
exit(0);
}
public function updatespends() {
set_time_limit(0);
self::lock('updatespends');
try {
$conn = ConnectionManager::get('default');
$inputs = $this->Inputs->find()->select(['Id', 'PrevoutHash', 'PrevoutN'])->where(['PrevoutSpendUpdated' => 0, 'IsCoinbase <>' => 1])->limit(500000)->toArray();
$count = count($inputs);
$idx = 0;
echo sprintf("Processing %d inputs.\n", $count);
foreach ($inputs as $in) {
$idx++;
$idx_str = str_pad($idx, strlen($count), '0', STR_PAD_LEFT);
$tx = $this->Transactions->find()->select(['Id'])->where(['Hash' => $in->PrevoutHash])->first();
if ($tx) {
$data_error = false;
$conn->begin();
try {
// update the corresponding output and set it as spent
$conn->execute('UPDATE Outputs SET IsSpent = 1, SpentByInputId = ?, Modified = UTC_TIMESTAMP() WHERE TransactionId = ? AND Vout = ?', [$in->Id, $tx->Id, $in->PrevoutN]);
} catch (\Exception $e) {
$data_error = true;
}
if (!$data_error) {
// update the input
$in_data = ['Id' => $in->Id, 'PrevoutSpendUpdated' => 1];
$in_entity = $this->Inputs->newEntity($in_data);
$result = $this->Inputs->save($in_entity);
if (!$result) {
$data_error = true;
}
}
if ($data_error) {
echo sprintf("[$idx_str/$count] Could NOT update vout %s for transaction hash %s.\n", $in->PrevoutN, $in->PrevoutHash);
$conn->rollback();
} else {
echo sprintf("[$idx_str/$count] Updated vout %s for transaction hash %s.\n", $in->PrevoutN, $in->PrevoutHash);
$conn->commit();
}
} else {
echo sprintf("[$idx_str/$count] Transaction NOT found for tx hash %s.\n", $in->PrevoutHash);
}
}
} catch (\Exception $e) {
print_r($e);
}
self::unlock('updatespends');
}
*/
}