/* XMRig
 * Copyright (c) 2010      Jeff Garzik <jgarzik@pobox.com>
 * Copyright (c) 2012-2014 pooler      <pooler@litecoinpool.org>
 * Copyright (c) 2014      Lucas Jones <https://github.com/lucasjones>
 * Copyright (c) 2014-2016 Wolf9466    <https://github.com/OhGodAPet>
 * Copyright (c) 2016      Jay D Dee   <jayddee246@gmail.com>
 * Copyright (c) 2017-2018 XMR-Stak    <https://github.com/fireice-uk>, <https://github.com/psychocrypt>
 * Copyright (c) 2019      Howard Chu  <https://github.com/hyc>
 * Copyright (c) 2018-2023 SChernykh   <https://github.com/SChernykh>
 * Copyright (c) 2016-2023 XMRig       <https://github.com/xmrig>, <support@xmrig.com>
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include <uv.h>


#include "base/net/stratum/DaemonClient.h"
#include "3rdparty/rapidjson/document.h"
#include "3rdparty/rapidjson/error/en.h"
#include "base/io/json/Json.h"
#include "base/io/json/JsonRequest.h"
#include "base/io/log/Log.h"
#include "base/kernel/interfaces/IClientListener.h"
#include "base/kernel/Platform.h"
#include "base/net/dns/Dns.h"
#include "base/net/dns/DnsRecords.h"
#include "base/net/http/Fetch.h"
#include "base/net/http/HttpData.h"
#include "base/net/http/HttpListener.h"
#include "base/net/stratum/SubmitResult.h"
#include "base/net/tools/NetBuffer.h"
#include "base/tools/bswap_64.h"
#include "base/tools/cryptonote/Signatures.h"
#include "base/tools/Cvt.h"
#include "base/tools/Timer.h"
#include "net/JobResult.h"


#ifdef XMRIG_FEATURE_TLS
#include <openssl/ssl.h>
#endif


#include <algorithm>
#include <cassert>
#include <random>


namespace xmrig {


Storage<DaemonClient> DaemonClient::m_storage;


static const char* kBlocktemplateBlob       = "blocktemplate_blob";
static const char* kBlockhashingBlob        = "blockhashing_blob";
static const char *kGetHeight               = "/getheight";
static const char *kGetInfo                 = "/getinfo";
static const char *kHash                    = "hash";
static const char *kHeight                  = "height";
static const char *kJsonRPC                 = "/json_rpc";

static constexpr size_t kBlobReserveSize    = 8;

static const char kZMQGreeting[64] = { static_cast<char>(-1), 0, 0, 0, 0, 0, 0, 0, 0, 127, 3, 0, 'N', 'U', 'L', 'L' };
static constexpr size_t kZMQGreetingSize1 = 11;

static const char kZMQHandshake[] = "\4\x19\5READY\xbSocket-Type\0\0\0\3SUB";
static const char kZMQSubscribe[] = "\0\x18\1json-minimal-chain_main";

} // namespace xmrig


xmrig::DaemonClient::DaemonClient(int id, IClientListener *listener) :
    BaseClient(id, listener)
{
    m_httpListener  = std::make_shared<HttpListener>(this);
    m_timer         = new Timer(this);
    m_key           = m_storage.add(this);
}


xmrig::DaemonClient::~DaemonClient()
{
    delete m_timer;
    delete m_ZMQSocket;
}


void xmrig::DaemonClient::deleteLater()
{
    if (m_pool.zmq_port() >= 0) {
        ZMQClose(true);
    }
    else {
        delete this;
    }
}


bool xmrig::DaemonClient::disconnect()
{
    if (m_state != UnconnectedState) {
        setState(UnconnectedState);
    }

    return true;
}


bool xmrig::DaemonClient::isTLS() const
{
#   ifdef XMRIG_FEATURE_TLS
    return m_pool.isTLS();
#   else
    return false;
#   endif
}


int64_t xmrig::DaemonClient::submit(const JobResult &result)
{
    if (result.jobId != m_currentJobId) {
        return -1;
    }

    char *data = m_blocktemplateStr.data();

    const size_t sig_offset = m_job.nonceOffset() + m_job.nonceSize();

#   ifdef XMRIG_PROXY_PROJECT

    memcpy(data + m_job.nonceOffset() * 2, result.nonce, 8);

    if (m_blocktemplate.hasMinerSignature() && result.sig) {
        memcpy(data + sig_offset * 2, result.sig, 64 * 2);
        memcpy(data + m_blocktemplate.offset(BlockTemplate::TX_PUBKEY_OFFSET) * 2, result.sig_data, 32 * 2);
        memcpy(data + m_blocktemplate.offset(BlockTemplate::EPH_PUBLIC_KEY_OFFSET) * 2, result.sig_data + 32 * 2, 32 * 2);

        // Handle view tag for txout_to_tagged_key outputs
        if (m_blocktemplate.outputType() == 3) {
            Cvt::toHex(data + m_blocktemplate.offset(BlockTemplate::EPH_PUBLIC_KEY_OFFSET) * 2 + 32 * 2, 2, &result.view_tag, 1);
        }
    }

    if (result.extra_nonce >= 0) {
        Cvt::toHex(data + m_blocktemplate.offset(BlockTemplate::TX_EXTRA_NONCE_OFFSET) * 2, 8, reinterpret_cast<const uint8_t*>(&result.extra_nonce), 4);
    }

#   else

    Cvt::toHex(data + m_job.nonceOffset() * 2, 8, reinterpret_cast<const uint8_t*>(&result.nonce), 4);

    if (m_blocktemplate.hasMinerSignature()) {
        Cvt::toHex(data + sig_offset * 2, 128, result.minerSignature(), 64);
    }

#   endif

    using namespace rapidjson;
    Document doc(kObjectType);

    Value params(kArrayType);
    params.PushBack(m_blocktemplateStr.toJSON(), doc.GetAllocator());

    JsonRequest::create(doc, m_sequence, "submitblock", params);

#   ifdef XMRIG_PROXY_PROJECT
    m_results[m_sequence] = SubmitResult(m_sequence, result.diff, result.actualDiff(), result.id, 0);
#   else
    m_results[m_sequence] = SubmitResult(m_sequence, result.diff, result.actualDiff(), 0, result.backend);
#   endif

    std::map<std::string, std::string> headers;
    headers.insert({"X-Hash-Difficulty", std::to_string(result.actualDiff())});

    return rpcSend(doc, headers);
}


void xmrig::DaemonClient::connect()
{
    auto connectError = [this](const char *message) {
        if (!isQuiet()) {
            LOG_ERR("%s " RED("connect error: ") RED_BOLD("\"%s\""), tag(), message);
        }

        retry();
    };

    setState(ConnectingState);

    if (!m_coin.isValid() && !m_pool.algorithm().isValid()) {
        return connectError("Invalid algorithm.");
    }

    if (!m_pool.algorithm().isValid()) {
        m_pool.setAlgo(m_coin.algorithm());
    }

    if ((m_apiVersion == API_MONERO) && !m_walletAddress.isValid()) {
        return connectError("Invalid wallet address.");
    }

    if (m_pool.zmq_port() >= 0) {
        m_dns = Dns::resolve(m_pool.host(), this);
    }
    else {
        getBlockTemplate();
    }
}


void xmrig::DaemonClient::connect(const Pool &pool)
{
    setPool(pool);
    connect();
}


void xmrig::DaemonClient::setPool(const Pool &pool)
{
    BaseClient::setPool(pool);

    m_walletAddress.decode(m_user);

    m_coin = pool.coin().isValid() ?  pool.coin() : m_walletAddress.coin();

    if (!m_coin.isValid() && pool.algorithm() == Algorithm::RX_WOW) {
        m_coin = Coin::WOWNERO;
    }

    if (!m_coin.isValid() && pool.algorithm() == Algorithm::RX_XEQ) {
        m_coin = Coin::XEQ;
    }
}


void xmrig::DaemonClient::onHttpData(const HttpData &data)
{
    if (data.status != 200) {
        return retry();
    }

    m_ip = data.ip().c_str();

#   ifdef XMRIG_FEATURE_TLS
    m_tlsVersion     = data.tlsVersion();
    m_tlsFingerprint = data.tlsFingerprint();
#   endif

    rapidjson::Document doc;
    if (doc.Parse(data.body.c_str()).HasParseError()) {
        if (!isQuiet()) {
            LOG_ERR("%s " RED("JSON decode failed: ") RED_BOLD("\"%s\""), tag(), rapidjson::GetParseError_En(doc.GetParseError()));
        }

        return retry();
    }

    if (data.method == HTTP_GET) {
        if (data.url == kGetHeight) {
            if (!doc.HasMember(kHash)) {
                m_apiVersion = API_CRYPTONOTE_DEFAULT;

                return send(kGetInfo);
            }

            const uint64_t height = Json::getUint64(doc, kHeight);
            const String hash = Json::getString(doc, kHash);

            if (isOutdated(height, hash)) {
                // Multiple /getheight responses can come at once resulting in multiple getBlockTemplate() calls
                if ((height != m_blocktemplateRequestHeight) || (hash != m_blocktemplateRequestHash)) {
                    m_blocktemplateRequestHeight = height;
                    m_blocktemplateRequestHash = hash;
                    getBlockTemplate();
                }
            }
        }
        else if (data.url == kGetInfo) {
            const uint64_t height = Json::getUint64(doc, kHeight);
            const String hash = Json::getString(doc, "top_block_hash");

            if (isOutdated(height, hash)) {
                // Multiple /getinfo responses can come at once resulting in multiple getBlockTemplate() calls
                if ((height != m_blocktemplateRequestHeight) || (hash != m_blocktemplateRequestHash)) {
                    m_blocktemplateRequestHeight = height;
                    m_blocktemplateRequestHash = hash;
                    getBlockTemplate();
                }
            }
        }

        return;
    }

    if (!parseResponse(Json::getInt64(doc, "id", -1), Json::getObject(doc, "result"), Json::getObject(doc, "error"))) {
        retry();
    }
}


void xmrig::DaemonClient::onTimer(const Timer *)
{
    if (m_pool.zmq_port() >= 0) {
        m_prevHash = nullptr;
        m_blocktemplateRequestHash = nullptr;
        send(kGetHeight);
        return;
    }

    if (Chrono::steadyMSecs() >= m_jobSteadyMs + m_pool.jobTimeout()) {
        m_prevHash = nullptr;
        m_blocktemplateRequestHash = nullptr;
    }

    if (m_state == ConnectingState) {
        connect();
    }
    else if (m_state == ConnectedState) {
        send((m_apiVersion == API_MONERO) ? kGetHeight : kGetInfo);
    }
}


void xmrig::DaemonClient::onResolved(const DnsRecords &records, int status, const char* error)
{
    m_dns.reset();

    if (status < 0 && records.isEmpty()) {
        if (!isQuiet()) {
            LOG_ERR("%s " RED("DNS error: ") RED_BOLD("\"%s\""), tag(), error);
        }

        retry();
        return;
    }


    const auto &record = records.get();
    m_ip = record.ip();

    auto req = new uv_connect_t;
    req->data = m_storage.ptr(m_key);

    uv_tcp_t* s = new uv_tcp_t;
    s->data = m_storage.ptr(m_key);

    uv_tcp_init(uv_default_loop(), s);
    uv_tcp_nodelay(s, 1);

    if (Platform::hasKeepalive()) {
        uv_tcp_keepalive(s, 1, 60);
    }

    if (m_pool.zmq_port() > 0) {
        delete m_ZMQSocket;
        m_ZMQSocket = s;
        uv_tcp_connect(req, s, record.addr(m_pool.zmq_port()), onZMQConnect);
    }
}


bool xmrig::DaemonClient::isOutdated(uint64_t height, const char *hash) const
{
    return m_job.height() != height || m_prevHash != hash || Chrono::steadyMSecs() >= m_jobSteadyMs + m_pool.jobTimeout();
}


bool xmrig::DaemonClient::parseJob(const rapidjson::Value &params, int *code)
{
    auto jobError = [this, code](const char *message) {
        if (!isQuiet()) {
            LOG_ERR("%s " RED("job error: ") RED_BOLD("\"%s\""), tag(), message);
        }

        *code = 1;

        return false;
    };

    Job job(false, m_pool.algorithm(), String());

    String blocktemplate = Json::getString(params, kBlocktemplateBlob);

    if (blocktemplate.isNull()) {
        return jobError("Empty block template received from daemon."); // FIXME
    }

    if (!m_blocktemplate.parse(blocktemplate, m_coin)) {
        return jobError("Invalid block template received from daemon.");
    }

#   ifdef XMRIG_PROXY_PROJECT
    const size_t k = m_blocktemplate.offset(BlockTemplate::MINER_TX_PREFIX_OFFSET);
    job.setMinerTx(
        m_blocktemplate.blob() + k,
        m_blocktemplate.blob() + m_blocktemplate.offset(BlockTemplate::MINER_TX_PREFIX_END_OFFSET),
        m_blocktemplate.offset(BlockTemplate::EPH_PUBLIC_KEY_OFFSET) - k,
        m_blocktemplate.offset(BlockTemplate::TX_PUBKEY_OFFSET) - k,
        m_blocktemplate.offset(BlockTemplate::TX_EXTRA_NONCE_OFFSET) - k,
        m_blocktemplate.txExtraNonce().size(),
        m_blocktemplate.minerTxMerkleTreeBranch(),
        m_blocktemplate.outputType() == 3
    );
#   endif

    m_blockhashingblob = Json::getString(params, kBlockhashingBlob);

    if (m_blocktemplate.hasMinerSignature()) {
        if (m_pool.spendSecretKey().isEmpty()) {
            return jobError("Secret spend key is not set.");
        }

        if (m_pool.spendSecretKey().size() != 64) {
            return jobError("Secret spend key has invalid length. It must be 64 hex characters.");
        }

        uint8_t secret_spendkey[32];
        if (!Cvt::fromHex(secret_spendkey, 32, m_pool.spendSecretKey(), 64)) {
            return jobError("Secret spend key is not a valid hex data.");
        }

        uint8_t public_spendkey[32];
        if (!secret_key_to_public_key(secret_spendkey, public_spendkey)) {
            return jobError("Secret spend key is invalid.");
        }

#       ifdef XMRIG_PROXY_PROJECT
        job.setSpendSecretKey(secret_spendkey);
#       else
        uint8_t secret_viewkey[32];
        derive_view_secret_key(secret_spendkey, secret_viewkey);

        uint8_t public_viewkey[32];
        if (!secret_key_to_public_key(secret_viewkey, public_viewkey)) {
            return jobError("Secret view key is invalid.");
        }

        uint8_t derivation[32];
        if (!generate_key_derivation(m_blocktemplate.blob(BlockTemplate::TX_PUBKEY_OFFSET), secret_viewkey, derivation, nullptr)) {
            return jobError("Failed to generate key derivation for miner signature.");
        }

        if (!m_walletAddress.decode(m_pool.user())) {
            return jobError("Invalid wallet address.");
        }

        if (memcmp(m_walletAddress.spendKey(), public_spendkey, sizeof(public_spendkey)) != 0) {
            return jobError("Wallet address and spend key don't match.");
        }

        if (memcmp(m_walletAddress.viewKey(), public_viewkey, sizeof(public_viewkey)) != 0) {
            return jobError("Wallet address and view key don't match.");
        }

        uint8_t eph_secret_key[32];
        derive_secret_key(derivation, 0, secret_spendkey, eph_secret_key);

        job.setEphemeralKeys(m_blocktemplate.blob(BlockTemplate::EPH_PUBLIC_KEY_OFFSET), eph_secret_key);
#       endif
    }

    if (m_coin.isValid()) {
        job.setAlgorithm(m_coin.algorithm(m_blocktemplate.majorVersion()));
    }

    if (!job.setBlob(m_blockhashingblob)) {
        *code = 3;
        return false;
    }

    job.setSeedHash(Json::getString(params, "seed_hash"));
    job.setHeight(Json::getUint64(params, kHeight));
    job.setDiff(Json::getUint64(params, "difficulty"));

    m_currentJobId = Cvt::toHex(Cvt::randomBytes(4));
    job.setId(m_currentJobId);

    m_job              = std::move(job);
    m_blocktemplateStr = std::move(blocktemplate);
    m_prevHash         = Json::getString(params, "prev_hash");
    m_jobSteadyMs      = Chrono::steadyMSecs();

    if (m_state == ConnectingState) {
        setState(ConnectedState);
    }

    m_listener->onJobReceived(this, m_job, params);
    return true;
}


bool xmrig::DaemonClient::parseResponse(int64_t id, const rapidjson::Value &result, const rapidjson::Value &error)
{
    if (id == -1) {
        return false;
    }

    if (error.IsObject()) {
        const char *message = error["message"].GetString();

        if (!handleSubmitResponse(id, message) && !isQuiet()) {
            LOG_ERR("[%s:%d] error: " RED_BOLD("\"%s\"") RED_S ", code: %d", m_pool.host().data(), m_pool.port(), message, error["code"].GetInt());
        }

        return false;
    }

    if (!result.IsObject()) {
        return false;
    }

    if (result.HasMember("top_block_hash")) {
        if (m_prevHash != Json::getString(result, "top_block_hash")) {
            getBlockTemplate();
        }
        return true;
    }

    int code = -1;
    if (result.HasMember(kBlocktemplateBlob) && parseJob(result, &code)) {
        return true;
    }

    const char* error_msg = nullptr;

    if (handleSubmitResponse(id, error_msg)) {
        if (error_msg || (m_pool.zmq_port() < 0)) {
            getBlockTemplate();
        }
        return true;
    }


    return false;
}


int64_t xmrig::DaemonClient::getBlockTemplate()
{
    using namespace rapidjson;
    Document doc(kObjectType);
    auto &allocator = doc.GetAllocator();

    Value params(kObjectType);
    params.AddMember("wallet_address", m_user.toJSON(), allocator);
    params.AddMember("extra_nonce", Cvt::toHex(Cvt::randomBytes(kBlobReserveSize)).toJSON(doc), allocator);

    JsonRequest::create(doc, m_sequence, "getblocktemplate", params);

    return rpcSend(doc);
}


int64_t xmrig::DaemonClient::rpcSend(const rapidjson::Document &doc, const std::map<std::string, std::string> &headers)
{
    FetchRequest req(HTTP_POST, m_pool.host(), m_pool.port(), kJsonRPC, doc, m_pool.isTLS(), isQuiet());
    for (const auto &header : headers) {
        req.headers.insert(header);
    }

    fetch(tag(), std::move(req), m_httpListener);

    return m_sequence++;
}


void xmrig::DaemonClient::retry()
{
    m_failures++;
    m_listener->onClose(this, static_cast<int>(m_failures));

    if (m_failures == -1) {
        return;
    }

    if (m_state == ConnectedState) {
        setState(ConnectingState);
    }

    if ((m_ZMQConnectionState != ZMQ_NOT_CONNECTED) && (m_ZMQConnectionState != ZMQ_DISCONNECTING)) {
        if (Platform::hasKeepalive()) {
            uv_tcp_keepalive(m_ZMQSocket, 0, 60);
        }
        uv_close(reinterpret_cast<uv_handle_t*>(m_ZMQSocket), onZMQClose);
    }

    m_timer->stop();
    m_timer->start(m_retryPause, 0);
}


void xmrig::DaemonClient::send(const char *path)
{
    FetchRequest req(HTTP_GET, m_pool.host(), m_pool.port(), path, m_pool.isTLS(), isQuiet());
    fetch(tag(), std::move(req), m_httpListener);
}


void xmrig::DaemonClient::setState(SocketState state)
{
    if (m_state == state) {
        return;
    }

    m_state = state;

    switch (state) {
    case ConnectedState:
        {
            m_failures = 0;
            m_listener->onLoginSuccess(this);

            if (m_pool.zmq_port() < 0) {
                const uint64_t interval = std::max<uint64_t>(20, m_pool.pollInterval());
                m_timer->start(interval, interval);
            }
            else {
                const uint64_t t = m_pool.jobTimeout();
                m_timer->start(t, t);
            }
        }
        break;

    case UnconnectedState:
        m_failures = -1;
        m_timer->stop();
        break;

    default:
        break;
    }
}


void xmrig::DaemonClient::onZMQConnect(uv_connect_t* req, int status)
{
    DaemonClient* client = getClient(req->data);
    delete req;

    if (!client) {
        return;
    }

    if (status < 0) {
        LOG_ERR("%s " RED("ZMQ connect error: ") RED_BOLD("\"%s\""), client->tag(), uv_strerror(status));
        client->retry();
        return;
    }

    client->ZMQConnected();
}


void xmrig::DaemonClient::onZMQRead(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf)
{
    DaemonClient* client = getClient(stream->data);
    if (client) {
        client->ZMQRead(nread, buf);
    }

    NetBuffer::release(buf);
}


void xmrig::DaemonClient::onZMQClose(uv_handle_t* handle)
{
    DaemonClient* client = getClient(handle->data);
    if (client) {
#       ifdef APP_DEBUG
        LOG_DEBUG(CYAN("tcp-zmq://%s:%u") BLACK_BOLD(" disconnected"), client->m_pool.host().data(), client->m_pool.zmq_port());
#       endif
        client->m_ZMQConnectionState = ZMQ_NOT_CONNECTED;
    }
}


void xmrig::DaemonClient::onZMQShutdown(uv_handle_t* handle)
{
    DaemonClient* client = getClient(handle->data);
    if (client) {
#       ifdef APP_DEBUG
        LOG_DEBUG(CYAN("tcp-zmq://%s:%u") BLACK_BOLD(" shutdown"), client->m_pool.host().data(), client->m_pool.zmq_port());
#       endif
        client->m_ZMQConnectionState = ZMQ_NOT_CONNECTED;
        m_storage.remove(client->m_key);
    }
}


void xmrig::DaemonClient::ZMQConnected()
{
#   ifdef APP_DEBUG
    LOG_DEBUG(CYAN("tcp-zmq://%s:%u") BLACK_BOLD(" connected"), m_pool.host().data(), m_pool.zmq_port());
#   endif

    m_ZMQConnectionState = ZMQ_GREETING_1;
    m_ZMQSendBuf.reserve(256);
    m_ZMQRecvBuf.reserve(256);

    if (ZMQWrite(kZMQGreeting, kZMQGreetingSize1)) {
        uv_read_start(reinterpret_cast<uv_stream_t*>(m_ZMQSocket), NetBuffer::onAlloc, onZMQRead);
    }
}


bool xmrig::DaemonClient::ZMQWrite(const char* data, size_t size)
{
    m_ZMQSendBuf.assign(data, data + size);

    uv_buf_t buf;
    buf.base = m_ZMQSendBuf.data();
    buf.len = static_cast<uint32_t>(m_ZMQSendBuf.size());

    const int rc = uv_try_write(reinterpret_cast<uv_stream_t*>(m_ZMQSocket), &buf, 1);

    if (static_cast<size_t>(rc) == buf.len) {
        return true;
    }

    LOG_ERR("%s " RED("ZMQ write failed, rc = %d"), tag(), rc);
    ZMQClose();
    return false;
}


void xmrig::DaemonClient::ZMQRead(ssize_t nread, const uv_buf_t* buf)
{
    if (nread <= 0) {
        LOG_ERR("%s " RED("ZMQ read failed, nread = %" PRId64), tag(), nread);
        ZMQClose();
        return;
    }

    m_ZMQRecvBuf.insert(m_ZMQRecvBuf.end(), buf->base, buf->base + nread);

    do {
        switch (m_ZMQConnectionState) {
        case ZMQ_GREETING_1:
            if (m_ZMQRecvBuf.size() >= kZMQGreetingSize1) {
                if ((m_ZMQRecvBuf[0] == static_cast<char>(-1)) && (m_ZMQRecvBuf[9] == 127) && (m_ZMQRecvBuf[10] == 3)) {
                    ZMQWrite(kZMQGreeting + kZMQGreetingSize1, sizeof(kZMQGreeting) - kZMQGreetingSize1);
                    m_ZMQConnectionState = ZMQ_GREETING_2;
                    break;
                }

                LOG_ERR("%s " RED("ZMQ handshake failed: invalid greeting format"), tag());
                ZMQClose();
            }
            return;

        case ZMQ_GREETING_2:
            if (m_ZMQRecvBuf.size() >= sizeof(kZMQGreeting)) {
                if (memcmp(m_ZMQRecvBuf.data() + 12, kZMQGreeting + 12, 20) == 0) {
                    m_ZMQConnectionState = ZMQ_HANDSHAKE;
                    m_ZMQRecvBuf.erase(m_ZMQRecvBuf.begin(), m_ZMQRecvBuf.begin() + sizeof(kZMQGreeting));

                    ZMQWrite(kZMQHandshake, sizeof(kZMQHandshake) - 1);
                    break;
                }

                LOG_ERR("%s " RED("ZMQ handshake failed: invalid greeting format 2"), tag());
                ZMQClose();

            }
            return;

        case ZMQ_HANDSHAKE:
            if (m_ZMQRecvBuf.size() >= 2) {
                if (m_ZMQRecvBuf[0] != 4) {
                    LOG_ERR("%s " RED("ZMQ handshake failed: invalid handshake format"), tag());
                    ZMQClose();
                    return;
                }

                const size_t size = static_cast<unsigned char>(m_ZMQRecvBuf[1]);
                if (size < 18) {
                    LOG_ERR("%s " RED("ZMQ handshake failed: invalid handshake size"), tag());
                    ZMQClose();
                    return;
                }

                if (m_ZMQRecvBuf.size() < size + 2) {
                    return;
                }

                if (memcmp(m_ZMQRecvBuf.data() + 2, kZMQHandshake + 2, 18) != 0) {
                    LOG_ERR("%s " RED("ZMQ handshake failed: invalid handshake data"), tag());
                    ZMQClose();
                    return;
                }

                ZMQWrite(kZMQSubscribe, sizeof(kZMQSubscribe) - 1);

                m_ZMQConnectionState = ZMQ_CONNECTED;
                m_ZMQRecvBuf.erase(m_ZMQRecvBuf.begin(), m_ZMQRecvBuf.begin() + size + 2);

                getBlockTemplate();
                break;
            }
            return;

        case ZMQ_CONNECTED:
            ZMQParse();
            return;

        default:
            return;
        }
    } while (true);
}


void xmrig::DaemonClient::ZMQParse()
{
#   ifdef APP_DEBUG
    std::vector<char> msg;
#   endif

    size_t msg_size = 0;

    char *data   = m_ZMQRecvBuf.data();
    size_t avail = m_ZMQRecvBuf.size();
    bool more    = false;

    do {
        if (avail < 1) {
            return;
        }

        more                 = (data[0] & 1) != 0;
        const bool long_size = (data[0] & 2) != 0;
        const bool command   = (data[0] & 4) != 0;

        ++data;
        --avail;

        uint64_t size = 0;
        if (long_size)
        {
            if (avail < sizeof(uint64_t)) {
                return;
            }
            size = bswap_64(*((uint64_t*)data));
            data += sizeof(uint64_t);
            avail -= sizeof(uint64_t);
        }
        else
        {
            if (avail < sizeof(uint8_t)) {
                return;
            }
            size = static_cast<uint8_t>(*data);
            ++data;
            --avail;
        }

        if (size > 1024U - msg_size)
        {
            LOG_ERR("%s " RED("ZMQ message is too large, size = %" PRIu64 " bytes"), tag(), size);
            ZMQClose();
            return;
        }

        if (avail < size) {
            return;
        }

        if (!command) {
#           ifdef APP_DEBUG
            msg.insert(msg.end(), data, data + size);
#           endif

            msg_size += size;
        }

        data += size;
        avail -= size;
    } while (more);

    m_ZMQRecvBuf.erase(m_ZMQRecvBuf.begin(), m_ZMQRecvBuf.begin() + (data - m_ZMQRecvBuf.data()));

#   ifdef APP_DEBUG
    msg.push_back('\0');
    LOG_DEBUG(CYAN("tcp-zmq://%s:%u") BLACK_BOLD(" read ") CYAN_BOLD("%zu") BLACK_BOLD(" bytes") " %s", m_pool.host().data(), m_pool.zmq_port(), msg.size() - 1, msg.data());
#   endif

    // Clear previous hash and check daemon height to guarantee that xmrig will call get_block_template RPC later
    // We can't call get_block_template directly because daemon is not ready yet
    m_prevHash = nullptr;
    m_blocktemplateRequestHash = nullptr;
    send(kGetHeight);

    const uint64_t t = m_pool.jobTimeout();
    m_timer->stop();
    m_timer->start(t, t);
}


bool xmrig::DaemonClient::ZMQClose(bool shutdown)
{
    if ((m_ZMQConnectionState == ZMQ_NOT_CONNECTED) || (m_ZMQConnectionState == ZMQ_DISCONNECTING)) {
        if (shutdown) {
            m_storage.remove(m_key);
        }
        return false;
    }

    m_ZMQConnectionState = ZMQ_DISCONNECTING;

    if (uv_is_closing(reinterpret_cast<uv_handle_t*>(m_ZMQSocket)) == 0) {
        if (Platform::hasKeepalive()) {
            uv_tcp_keepalive(m_ZMQSocket, 0, 60);
        }
        uv_close(reinterpret_cast<uv_handle_t*>(m_ZMQSocket), shutdown ? onZMQShutdown : onZMQClose);
        if (!shutdown) {
            retry();
        }
        return true;
    }

    return false;
}
