denpamusic/php-levin

View on GitHub
src/Nodes/DummyNode.php

Summary

Maintainability
A
0 mins
Test Coverage
F
0%
<?php

declare(strict_types=1);

namespace Denpa\Levin\Nodes;

use Denpa\Levin;
use Denpa\Levin\Bucket;
use Denpa\Levin\Connection;
use Denpa\Levin\Traits\InteractsWithConsole;
use Throwable;

/**
 * This dummy node connects to the peer, reports that
 * it's fully synced and just sits there
 * replying to timedsync requests
 * and handling new block notifications.
 */
class DummyNode extends Node
{
    use InteractsWithConsole;

    /**
     * @var int Contains current node difficulty.
     */
    public $difficulty = 0;

    /**
     * @var int Contains current node heigh.
     */
    public $height = 0;

    /**
     * @var string Contains current top id.
     */
    public $topId = '';

    /**
     * @var int Contains current top version.
     */
    public $topVersion = 0;

    /**
     * @var array Contains peerslist received from remote.
     */
    public $peerlist = [];

    /**
     * @var bool
     */
    protected $verbose = false;

    /**
     * Registers handlers for buckets.
     *
     * @return void
     */
    public function __construct()
    {
        $this
            ->registerRequestHandler('payloadDataHandler', 'timedsync')
            ->registerRequestHandler('requestChainHandler', 'requestchain')
            ->registerRequestHandler('responseHandler', 'ping', 'supportflags')
            ->registerRequestHandler('timedSyncHandler', 'timedsync')
            ->registerRequestHandler('newTransactionsHandler', 'newtransactions')
            ->registerRequestHandler('newBlockHandler', 'newblock', 'newfluffyblock')
            ->registerResponseHandler('peerlistHandler', 'handshake')
            ->registerResponseHandler('payloadDataHandler', 'handshake')
            ->registerResponseHandler('sendPingHandler', 'handshake')
            ->registerResponseHandler('recvPingHandler', 'ping');
    }

    /**
     * {@inheritdoc}
     *
     * @param \Denpa\Levin\Bucket     $bucket
     * @param \Denpa\Levin\Connection $connection
     *
     * @return mixed
     */
    public function handle(Bucket $bucket, Connection $connection): mixed
    {
        $this->printBucket($bucket, 'in');

        return parent::handle($bucket, $connection);
    }

    /**
     * {@inheritdoc}
     *
     * @param string $address
     * @param mixed  $port
     * @param array  $options
     *
     * @return void
     */
    public function connect(string $address, $port, array $options = []): void
    {
        $this->verbose = isset($options['v']);

        if (isset($options['colors'])) {
            $this->console()->enableColors();
        }

        parent::connect($address, $port, $options);
    }

    /**
     * Handles exception by outputing it to stderr.
     *
     * @param \Throwable $exception
     *
     * @return void
     */
    public function handleException(Throwable $exception): void
    {
        $this
            ->console()->target(STDERR)
            ->error('Exception: %s'.PHP_EOL, $exception->getMessage());
    }

    /**
     * Handles remote peerlist.
     *
     * @param \Denpa\Levin\Bucket $bucket
     *
     * @return mixed
     */
    protected function peerlistHandler(Bucket $bucket): mixed
    {
        $peers = $bucket->getPayload()['local_peerlist_new'] ?? [];

        foreach ($peers as $entry) {
            $addr = $entry['adr']['addr'] ?? null;

            if (is_null($addr)) {
                continue;
            }

            // add peer to peerlist
            $this->peerlist[] = [
                'ip'        => inet_ntop($addr['m_ip']->toBinary()),
                'port'      => $addr['m_port']->toInt(),
                'last_seen' => isset($entry['last_seen']) ?
                    date('Y-m-d H:i:s', $entry['last_seen']->toInt()) : '',
            ];
        }

        $this->printPeerlist();
    }

    /**
     * Handles requests and responses that have payload data.
     *
     * @param \Denpa\Levin\Bucket $bucket
     *
     * @return mixed
     */
    protected function payloadDataHandler(Bucket $bucket): mixed
    {
        $payloadData = $bucket->getPayload()['payload_data'];

        $this->difficulty = $payloadData['cumulative_difficulty']->toInt();
        $this->height = $payloadData['current_height']->toInt();
        $this->topVersion = $payloadData['top_version']->toInt();
        $this->topId = $payloadData['top_id']->getValue();

        $this
            ->console()
            ->info(
                '%sTop Id: %s, Version: %d, Height: %d, Difficulty: %d%s',
                PHP_EOL,
                bin2hex($this->topId),
                $this->topVersion,
                $this->height,
                $this->difficulty,
                PHP_EOL
            );
    }

    /**
     * Handles chain request chain notification.
     *
     * @param \Denpa\Levin\Bucket     $bucket
     * @param \Denpa\Levin\Connection $connection
     *
     * @return mixed
     */
    protected function requestChainHandler(Bucket $bucket, Connection $connection): mixed
    {
        $responsechainentry = Levin\notification('responsechainentry', [
            'start_height'          => $this->height - 1,
            'total_height'          => $this->height,
            'cumulative_difficulty' => $this->difficulty,
            'm_block_ids'           => $this->topId,
        ]);

        // report that we already have lastest block available to remote
        $this->write($responsechainentry, $connection);
    }

    /**
     * Responds to the request.
     *
     * @param \Denpa\Levin\Bucket     $bucket
     * @param \Denpa\Levin\Connection $connection
     *
     * @return mixed
     */
    protected function responseHandler(Bucket $bucket, Connection $connection): mixed
    {
        $this->write($bucket->response(), $connection);
    }

    /**
     * Handles timed sync requests.
     *
     * @param \Denpa\Levin\Bucket     $bucket
     * @param \Denpa\Levin\Connection $connection
     *
     * @return mixed
     */
    protected function timedSyncHandler(Bucket $bucket, Connection $connection): mixed
    {
        $payloadData = $bucket->getPayload()['payload_data'];

        $timedsync = Levin\response('timedsync', [
            'top_id'                => $payloadData['top_id']->getValue(),
            'top_version'           => $payloadData['top_version']->toInt(),
            'cumulative_difficulty' => $payloadData['cumulative_difficulty']->toInt(),
            'current_height'        => $payloadData['current_height']->toInt(),
        ]);

        $this->write($timedsync, $connection);
    }

    /**
     * Handles new block notifications.
     *
     * @param \Denpa\Levin\Bucket $bucket
     *
     * @return mixed
     */
    protected function newBlockHandler(Bucket $bucket): mixed
    {
        $payload = $bucket->getPayload();

        $this->height = $payload['current_blockchain_height']->toInt();

        $this
            ->console()
            ->line('New block: #%d'.PHP_EOL, $this->height)->line('Block hex:')
            ->startBlock()
            ->indent()->line(bin2hex($payload['b']['block']).PHP_EOL)
            ->endBlock();
    }

    /**
     * Handles new transactions notifications.
     *
     * @param \Denpa\Levin\Bucket $bucket
     *
     * @return mixed
     */
    protected function newTransactionsHandler(Bucket $bucket): void
    {
        $txs = $bucket->getPayload()['txs'] ?? [];

        $this
            ->console()
            ->info('Received %d new transactions:'.PHP_EOL, count($txs))
            ->startBlock();

        foreach ($txs as $tx) {
            $this->console()->indent()->line($tx->toHex().PHP_EOL.PHP_EOL);
        }

        $this->console()->endBlock();
    }

    /**
     * Sends ping request.
     *
     * @param \Denpa\Levin\Bucket     $bucket
     * @param \Denpa\Levin\Connection $connection
     *
     * @return mixed
     */
    protected function sendPingHandler(Bucket $bucket, $connection)
    {
        $this->write(Levin\request('ping'), $connection);
    }

    /**
     * Outputs received ping status.
     *
     * @param \Denpa\Levin\Bucket $bucket
     *
     * @return mixed
     */
    protected function recvPingHandler(Bucket $bucket): mixed
    {
        $status = $bucket->getPayload()['status'] ?? 'FAIL';

        $this
            ->console()
            ->info(PHP_EOL.'PING: %s'.PHP_EOL, $status);
    }

    /**
     * End of bucket handler methods.
     */

    /**
     * Write bucket to the connection and output.
     *
     * @param \Denpa\Levin\Bucket     $bucket
     * @param \Denpa\Levin\Connection $connection
     *
     * @return void
     */
    protected function write(Bucket $bucket, Connection $connection): void
    {
        $this->printBucket($bucket, 'out');

        $connection->write($bucket);
    }

    /**
     * Outputs line containing info about bucket.
     *
     * @param \Denpa\Levin\Bucket $bucket
     * @param string              $direction
     *
     * @return void
     */
    protected function printBucket(Bucket $bucket, string $direction = ''): void
    {
        $this->printDirectionArrows($direction);
        $this->printBucketType($bucket);

        $this
            ->console()
            ->background('white')->color('black')
            ->line(get_class($bucket->getCommand()).PHP_EOL)
            ->resetColors();

        if ($this->verbose) {
            $this->console()->dump($bucket);
        }
    }

    /**
     * Prints bucket type, which can be response, request or notification.
     *
     * @param \Denpa\Levin\Bucket $bucket
     *
     * @return void
     */
    protected function printBucketType(Bucket $bucket): void
    {
        $type = $bucket->isResponse() ? 'response' : 'request';

        if ($bucket->isRequest() && !$bucket->getReturnData()->getValue()) {
            $type = 'notification';
        }

        $this
            ->console()
            ->line(' (')
            ->color('bright-yellow')
            ->line($type)
            ->resetColors()
            ->line(')  ');
    }

    /**
     * Prints bucket direction arrows.
     *
     * @param string $direction
     *
     * @return void
     */
    protected function printDirectionArrows(string $direction = ''): void
    {
        switch ($direction) {
            case 'in':
                $direction = '>>>';
                break;
            case 'out':
                $direction = '<<<';
                break;
            default:
                $direction = '   ';
        }

        $this
            ->console()
            ->resetColors()
            ->line(PHP_EOL.$direction);
    }

    /**
     * @return void
     */
    protected function printPeerlist(): void
    {
        $this->console()->info('Remote peers:'.PHP_EOL)->startBlock();

        foreach ($this->peerlist as $peer) {
            $this
                ->console()
                ->indent()
                ->line(
                    '%s  seen %s'.PHP_EOL,
                    str_pad($peer['ip'].':'.$peer['port'], 21),
                    $peer['last_seen']
                );
        }

        $this
            ->console()
            ->indent()
            ->line(
                '%sTotal: %d known peers%s%s',
                PHP_EOL,
                count($this->peerlist),
                PHP_EOL,
                PHP_EOL
            )
            ->endBlock();
    }
}