// Copyright (c) 2014-2018 Zano Project
// Copyright (c) 2014-2018 The Louisdor Project
// Copyright (c) 2012-2013 The Boolberry developers
// Copyright (c) 2017-2025 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// You may obtain a copy of the licence at:
//
//     https://joinup.ec.europa.eu/software/page/eupl/licence-eupl
//
// The EUPL is a copyleft licence that is compatible with the MIT/X11
// licence used by the original projects; the MIT terms are therefore
// considered “grandfathered” under the EUPL for this code.
//
// SPDX‑License‑Identifier: EUPL-1.2
//

#include "stratum_server.h"
#include "stratum_helpers.h"
#include "net/abstract_tcp_server2.h"
#include "config/currency_config.h"
#include "currency_core/currency_core.h"
#include "common/command_line.h"
#include "common/int-util.h"
#include "version.h"
#include "currency_protocol/currency_protocol_handler.h"

#undef LOG_DEFAULT_CHANNEL 
#define LOG_DEFAULT_CHANNEL "stratum"
ENABLE_CHANNEL_BY_DEFAULT("stratum");

using namespace stratum;

namespace currency
{

namespace
{
// This username should be used by stratum clients to log in IN CASE stratum server was started with "stratum-miner-address" option.
// Alternatevly, valid and the very same wallet address should be user among all the workers.
#define WORKER_ALLOWED_USERNAME "miner"

#define STRATUM_BIND_IP_DEFAULT "0.0.0.0"
#define STRATUM_THREADS_COUNT_DEFAULT 2
#define STRATUM_BLOCK_TEMPLATE_UPD_PERIOD_DEFAULT 30 // sec
#define STRATUM_TOTAL_HR_PRINT_INTERVAL_S_DEFAULT 60 // sec
#define VDIFF_TARGET_MIN_DEFAULT 100000000ull // = 100 Mh
#define VDIFF_TARGET_MAX_DEFAULT 100000000000ull // = 100 Gh
#define VDIFF_TARGET_TIME_DEFAULT 30 // sec
#define VDIFF_RETARGET_TIME_DEFAULT 240 // sec
#define VDIFF_RETARGET_SHARES_COUNT 12 // enforce retargeting if this many shares are received (high performace in terms of current difficulty)
#define VDIFF_VARIANCE_PERCENT_DEFAULT 25 // %

  const command_line::arg_descriptor<bool>        arg_stratum                ("stratum",                   "Stratum server: enable" );
  const command_line::arg_descriptor<std::string> arg_stratum_bind_ip        ("stratum-bind-ip",           "Stratum server: IP to bind",                STRATUM_BIND_IP_DEFAULT );
  const command_line::arg_descriptor<std::string> arg_stratum_bind_port      ("stratum-bind-port",         "Stratum server: port to listen at",         std::to_string(STRATUM_DEFAULT_PORT) );
  const command_line::arg_descriptor<size_t>      arg_stratum_threads        ("stratum-threads-count",     "Stratum server: number of server threads",  STRATUM_THREADS_COUNT_DEFAULT );
  const command_line::arg_descriptor<std::string> arg_stratum_miner_address = {"stratum-miner-address",     "Stratum server: miner address. All workers"
    " will mine to this address. If not set here, ALL workers should use the very same wallet address as username."
    " If set here - they're allowed to log in with username '" WORKER_ALLOWED_USERNAME "' instead of address."};
  
  const command_line::arg_descriptor<size_t>      arg_stratum_block_template_update_period = {"stratum-template-update-period",
    "Stratum server: if there are no new blocks, update block template this often (sec.)",  STRATUM_BLOCK_TEMPLATE_UPD_PERIOD_DEFAULT };
  const command_line::arg_descriptor<uint64_t>    arg_stratum_hr_print_interval  ("stratum-hr-print-interval", "Stratum server: how often to print hashrate stats (sec.)", STRATUM_TOTAL_HR_PRINT_INTERVAL_S_DEFAULT );
  
  const command_line::arg_descriptor<uint64_t>    arg_stratum_vdiff_target_min  ("stratum-vdiff-target-min",  "Stratum server: minimum worker difficulty",  VDIFF_TARGET_MIN_DEFAULT );
  const command_line::arg_descriptor<uint64_t>    arg_stratum_vdiff_target_max  ("stratum-vdiff-target-max",  "Stratum server: maximum worker difficulty",  VDIFF_TARGET_MAX_DEFAULT );
  const command_line::arg_descriptor<uint64_t>    arg_stratum_vdiff_target_time  ("stratum-vdiff-target-time",  "Stratum server: target time per share (i.e. try to get one share per this many seconds)",  VDIFF_TARGET_TIME_DEFAULT );
  const command_line::arg_descriptor<uint64_t>    arg_stratum_vdiff_retarget_time  ("stratum-vdiff-retarget-time",  "Stratum server: check to see if we should retarget this often (sec.)",  VDIFF_RETARGET_TIME_DEFAULT );
  const command_line::arg_descriptor<uint64_t>    arg_stratum_vdiff_retarget_shares  ("stratum-vdiff-retarget-shares",  "Stratum server: enforce retargeting if got this many shares",  VDIFF_RETARGET_SHARES_COUNT );
  const command_line::arg_descriptor<uint64_t>    arg_stratum_vdiff_variance_percent   ("stratum-vdiff-variance-percent",  "Stratum server: allow average time to very this % from target without retarget",  VDIFF_VARIANCE_PERCENT_DEFAULT );
  const command_line::arg_descriptor<bool>        arg_stratum_always_online  ( "stratum-always-online",                   "Stratum server consider the core being synchronized regardless of online status, useful for debugging with --offline-mode" );

//==============================================================================================================================

  static jsonrpc_id_null_t jsonrpc_id_null; // object of jsonrpc_id_null_t for convenience

// JSON-RPC error codes
#define JSONRPC_ERROR_CODE_DEFAULT          -32000 // -32000 to -32099 : Reserved for implementation-defined server-errors.
#define JSONRPC_ERROR_CODE_PARSE            -32700
#define JSONRPC_ERROR_CODE_METHOD_NOT_FOUND -32601

#define LP_CC_WORKER(         ct, message, log_level)  LOG_PRINT_CC(          ct, "WORKER " << ct.m_worker_name << ": " << message, log_level)
#define LP_CC_WORKER_GREEN(   ct, message, log_level)  LOG_PRINT_CC_GREEN(    ct, "WORKER " << ct.m_worker_name << ": " << message, log_level)
#define LP_CC_WORKER_RED(     ct, message, log_level)  LOG_PRINT_CC_RED(      ct, "WORKER " << ct.m_worker_name << ": " << message, log_level)
#define LP_CC_WORKER_BLUE(    ct, message, log_level)  LOG_PRINT_CC_BLUE(     ct, "WORKER " << ct.m_worker_name << ": " << message, log_level)
#define LP_CC_WORKER_YELLOW(  ct, message, log_level)  LOG_PRINT_CC_YELLOW(   ct, "WORKER " << ct.m_worker_name << ": " << message, log_level)
#define LP_CC_WORKER_CYAN(    ct, message, log_level)  LOG_PRINT_CC_CYAN(     ct, "WORKER " << ct.m_worker_name << ": " << message, log_level)
#define LP_CC_WORKER_MAGENTA( ct, message, log_level)  LOG_PRINT_CC_MAGENTA(  ct, "WORKER " << ct.m_worker_name << ": " << message, log_level)

#define HR_TO_STREAM_IN_MHS_1P(hr) std::fixed << std::setprecision(1) << hr / 1000000.0
#define HR_TO_STREAM_IN_MHS_3P(hr) std::fixed << std::setprecision(3) << hr / 1000000.0

// debug stuff
#define DBG_NETWORK_DIFFICULTY 0 // if non-zero: use this value as net difficulty when checking shares (useful for debugging on testnet, recommended value is 3000000000ull)
#define DBG_CORE_ALWAYS_SYNCRONIZED 0 // if set to 1: allows the server to start even if the core is not syncronized, useful for debugging with --offline-mode
#define STRINGIZE_DETAIL(x) #x
#define STRINGIZE(x) STRINGIZE_DETAIL(x)
#define DP(x) LOG_PRINT_L0("LINE " STRINGIZE(__LINE__) ":             " #x " = " << x)

//==============================================================================================================================
  struct vdiff_params_t
  {
    explicit vdiff_params_t()
      : target_min(0)
      , target_max(0)
      , target_time_ms(0)
      , retarget_time_ms(0)
      , retarget_shares_count(0)
      , variance_percent(0)
    {}

    vdiff_params_t(uint64_t target_min, uint64_t target_max, uint64_t target_time_s, uint64_t retarget_time_s, uint64_t retarget_shares_count, uint64_t variance_percent) 
      : target_min(target_min)
      , target_max(target_max)
      , target_time_ms(target_time_s * 1000)
      , retarget_time_ms(retarget_time_s * 1000)
      , retarget_shares_count(retarget_shares_count)
      , variance_percent(variance_percent)
    {}

    uint64_t target_min;
    uint64_t target_max;
    uint64_t target_time_ms;
    uint64_t retarget_time_ms;
    uint64_t retarget_shares_count;
    uint64_t variance_percent;
  };

  struct stratum_connection_context : public epee::net_utils::connection_context_base
  {
    explicit stratum_connection_context()
      : m_worker_name("?")
      , m_worker_difficulty(1)
      , m_ts_started(0)
      , m_ts_share_period_timer(0)
      , m_ts_difficulty_updated(0)
      , m_valid_shares_count(0)
      , m_wrong_shares_count(0)
      , m_hashes_calculated(0)
      , m_blocks_count(0)
    {}

    void set_worker_name(const std::string& worker_name)
    {
      CRITICAL_REGION_LOCAL(m_lock);
      m_worker_name = worker_name;
    }

    uint64_t get_hr_estimate_duration()
    {
      CRITICAL_REGION_LOCAL(m_lock);
      return (epee::misc_utils::get_tick_count() - m_ts_started) / 1000;
    }
    
    uint64_t estimate_worker_hashrate()
    {
      CRITICAL_REGION_LOCAL(m_lock);
      if (m_ts_started == 0)
        return 0;
      uint64_t duration = (epee::misc_utils::get_tick_count() - m_ts_started) / 1000;
      if (duration == 0)
        return 0;
      return (m_hashes_calculated / duration).convert_to<uint64_t>();
    }

    uint64_t get_average_share_period_ms()
    {
      CRITICAL_REGION_LOCAL(m_lock);
      if (m_ts_share_period_timer == 0)
        return 0;
      uint64_t duration_ms = (epee::misc_utils::get_tick_count() - m_ts_share_period_timer);
      if (m_valid_shares_count == 0)
        return 0;
      return duration_ms / m_valid_shares_count;
    }

    void reset_average_share_period_ms()
    {
      CRITICAL_REGION_LOCAL(m_lock);
      m_ts_share_period_timer = epee::misc_utils::get_tick_count();
      m_valid_shares_count = 0;
    }

    void set_worker_difficulty(const wide_difficulty_type& d)
    {
      CRITICAL_REGION_LOCAL(m_lock);
      m_worker_difficulty = d;
      m_ts_difficulty_updated = epee::misc_utils::get_tick_count();
    }

    void init_and_start_timers(const vdiff_params_t& vd_params)
    {
      CRITICAL_REGION_LOCAL(m_lock);
      m_ts_started = epee::misc_utils::get_tick_count();
      m_vd_params = vd_params;
      set_worker_difficulty(m_vd_params.target_min);
      reset_average_share_period_ms();
    }

    wide_difficulty_type get_worker_difficulty()
    {
      CRITICAL_REGION_LOCAL(m_lock);
      return m_worker_difficulty;
    }

    // returns true if worker difficulty has just been changed
    bool adjust_worker_difficulty_if_needed()
    {
      CRITICAL_REGION_LOCAL(m_lock);

      // check retarget condition if 1) there're too many shares received; 2) difficulty was updated long enough time ago
      if (m_valid_shares_count >= m_vd_params.retarget_shares_count ||
          epee::misc_utils::get_tick_count() - m_ts_difficulty_updated > m_vd_params.retarget_time_ms)
      {
        int64_t average_share_period_ms = 0;
        wide_difficulty_type new_d = 0;
        if (m_valid_shares_count != 0)
        {
          // there were shares, calculate using average share period
          average_share_period_ms = static_cast<int64_t>(get_average_share_period_ms());
          if (average_share_period_ms != 0 &&
              uint64_t(llabs(average_share_period_ms - int64_t(m_vd_params.target_time_ms))) > m_vd_params.target_time_ms * m_vd_params.variance_percent / 100)
          {
            new_d = m_worker_difficulty * m_vd_params.target_time_ms / average_share_period_ms;
          }
        }
        else
        {
          // no shares are found during retarget_time_ms, perhaps the difficulty is too high, lower it significantly
          new_d = (m_vd_params.target_min + m_worker_difficulty) / 4;
        }

        if (new_d != 0)
        {
          if (new_d < m_vd_params.target_min)
            new_d = m_vd_params.target_min;
          if (new_d > m_vd_params.target_max)
            new_d = m_vd_params.target_max;
          
          if (new_d != m_worker_difficulty)
          {
            LP_CC_WORKER_YELLOW((*this), "difficulty update: " << m_worker_difficulty << " -> " << new_d <<
              " (av. share pediod was: " << average_share_period_ms << ", target: " << m_vd_params.target_time_ms <<
              ", shares: " << m_valid_shares_count << ", variance: " << int64_t(100 * average_share_period_ms / m_vd_params.target_time_ms - 100) << "%)", LOG_LEVEL_2);
            set_worker_difficulty(new_d);
            reset_average_share_period_ms();
            return true;
          }
        }
      }
      return false;
    }

    void increment_normal_shares_count()
    {
      CRITICAL_REGION_LOCAL(m_lock);
      ++m_valid_shares_count;
      m_hashes_calculated += m_worker_difficulty;
    }

    void increment_stale_shares_count()
    {
      CRITICAL_REGION_LOCAL(m_lock);
      ++m_valid_shares_count;
      m_hashes_calculated += m_worker_difficulty;
    }

    void increment_wrong_shares_count()
    {
      //++m_wrong_shares_count; not implemented yet
    }

    void increment_blocks_count()
    {
      CRITICAL_REGION_LOCAL(m_lock);
      ++m_blocks_count;
    }

    size_t get_blocks_count()
    {
      CRITICAL_REGION_LOCAL(m_lock);
      return m_blocks_count;
    }

    uint64_t get_current_valid_shares_count()
    {
      CRITICAL_REGION_LOCAL(m_lock);
      return m_valid_shares_count;
    }

    mutable epee::critical_section m_lock;
    std::string m_worker_name;
    wide_difficulty_type m_worker_difficulty;
    uint64_t m_ts_started;
    uint64_t m_ts_share_period_timer;
    uint64_t m_ts_difficulty_updated;
    size_t m_valid_shares_count; // number of shares that satisfy worker's difficulty (valid + stale)
    size_t m_wrong_shares_count;
    size_t m_blocks_count;
    wide_difficulty_type m_hashes_calculated;
    vdiff_params_t m_vd_params;
  }; // struct stratum_connection_context

//==============================================================================================================================
  template<typename connection_context_t>
  class stratum_protocol_handler;

  template<typename connection_context_t>
  struct stratum_protocol_handler_config : public i_blockchain_update_listener
  {
    typedef stratum_protocol_handler_config<connection_context_t> this_t;
    typedef stratum_protocol_handler<connection_context_t> protocol_handler_t;

    stratum_protocol_handler_config()
      : m_max_packet_size(10240)
      , m_p_core(nullptr)
      , m_network_difficulty(0)
      , m_miner_addr(null_pub_addr)
      , m_block_template_ethash(null_hash)
      , m_blockchain_last_block_id(null_hash)
      , m_block_template_height(0)
      , m_block_template_update_ts(0)
      , m_last_ts_total_hr_was_printed(epee::misc_utils::get_tick_count())
      , m_total_hr_print_interval_ms(STRATUM_TOTAL_HR_PRINT_INTERVAL_S_DEFAULT * 1000)
      , m_block_template_update_pediod_ms(STRATUM_BLOCK_TEMPLATE_UPD_PERIOD_DEFAULT * 1000)
      , m_nameless_worker_id(0)
      , m_total_blocks_found(0)
      , m_stop_flag(false)
      , m_blocktemplate_update_thread(&this_t::block_template_update_thread, this)
      , m_is_core_always_online(false)
    {
      LOG_PRINT_L4("stratum_protocol_handler_config::ctor()");
    }

    ~stratum_protocol_handler_config()
    {
      NESTED_TRY_ENTRY();

      LOG_PRINT_L4("stratum_protocol_handler_config::dtor()");

      m_stop_flag = true;
      m_blocktemplate_update_thread.join();

      if (m_p_core)
        m_p_core->remove_blockchain_update_listener(this);

      NESTED_CATCH_ENTRY(__func__);
    }

    void add_protocol_handler(protocol_handler_t* p_ph)
    {
      CRITICAL_REGION_BEGIN(m_ph_map_lock);
      m_protocol_handlers[p_ph->get_context().m_connection_id] = p_ph;
      CRITICAL_REGION_END();
      LOG_PRINT_CC(p_ph->get_context(), "stratum_protocol_handler_config: protocol handler added", LOG_LEVEL_4);
      //m_pcommands_handler->on_connection_new(pconn->m_connection_context);
    }

    void remove_protocol_handler(protocol_handler_t* p_ph)
    {
      CRITICAL_REGION_BEGIN(m_ph_map_lock);
      m_protocol_handlers.erase(p_ph->get_context().m_connection_id);
      CRITICAL_REGION_END();
      LOG_PRINT_CC(p_ph->get_context(), "stratum_protocol_handler_config: protocol handler removed", LOG_LEVEL_4);
      //m_pcommands_handler->on_connection_close(pconn->m_connection_context);
    }

    void test()
    {
      //protocol_handler_t* p_ph = m_protocol_handlers.begin()->second;
      //std::string test = " \t  { \n \"{ \\\\  \":\"}\", \"id\":1,\"jsonrpc\":\"2.0\",\"result\":[\"0x63de85850f7e02baba2dc0765ebfb01040e56354729be70358ff341b48c608ad\",\"0xa92afa474bb50595e467a1722ed7a74fc00b3307de5022d5b76cf979490f2222\",\"0x00000000dbe6fecebdedd5beb573440e5a884d1b2fbf06fcce912adcb8d8422e\"]}";
      //std::string test = "{\"error\": [0], \"id\" : 555, \"result\" : \"AAA!\"}";
      //std::string test = "{\"id\":10,\"method\":\"eth_submitWork\",\"params\":[\"0x899624c0078e824f\",\"0xb98617aec14a2872fb45ab755f6273a1ae719d0ff0d441a25bb8235d82cb4123\",\"0x30f599f39276df17656727f16c3230c072dd8f2dd780161625479d352e8b2a97\"]}";
      //std::string test = "{\"id\":10,\"method\":\"eth_submitWork\",\"parasms\":[\"0x899624c0078e824f\"]}";
      //std::string test = R"({"worker": "", "jsonrpc": "2.0", "params": [], "id": 3, "method": "eth_getWork"})";
      //std::string test = R"({"worker": "eth1.0", "jsonrpc": "2.0", "params": ["HeyPnNRKwWzPF3JQMi1dcTJBRr3anA3iEMyY5vokAusYj73grFg8VhiYgWXJ4S31oT5ZV7rCjXn3QgZxQ9xr6DQJ1LThkj3", "x"], "id": 2, "method": "eth_submitLogin"})";
      //p_ph->handle_recv(test.c_str(), test.size());
    }

    void block_template_update_thread()
    {
      epee::log_space::log_singletone::set_thread_log_prefix("[ST]");
      while (!m_stop_flag)
      {
        if (is_core_syncronized() && epee::misc_utils::get_tick_count() - m_block_template_update_ts >= m_block_template_update_pediod_ms)
        {
          update_block_template();
        }
        print_total_hashrate_if_needed();
        epee::misc_utils::sleep_no_w(200);
      }
    }

    bool update_block_template(bool enforce_update = false)
    {
      CRITICAL_REGION_LOCAL(m_work_change_lock);
      uint64_t stub;
      crypto::hash top_block_id = null_hash;
      m_p_core->get_blockchain_top(stub, top_block_id);
      if (!enforce_update && top_block_id == m_blockchain_last_block_id && epee::misc_utils::get_tick_count() - m_block_template_update_ts < m_block_template_update_pediod_ms)
        return false;// no new blocks since last update, keep the same work
      
      LOG_PRINT("stratum_protocol_handler_config::update_block_template(" << (enforce_update ? "true" : "false") << ")", LOG_LEVEL_4);
      m_block_template = AUTO_VAL_INIT(m_block_template);
      wide_difficulty_type block_template_difficulty;
      blobdata extra = AUTO_VAL_INIT(extra);
      bool r = m_p_core->get_block_template(m_block_template, m_miner_addr, m_miner_addr, block_template_difficulty, m_block_template_height, extra);
      CHECK_AND_ASSERT_MES(r, false, "get_block_template failed");
#if DBG_NETWORK_DIFFICULTY == 0
      m_network_difficulty = block_template_difficulty;
#else
      m_network_difficulty = DBG_NETWORK_DIFFICULTY; // for debug purpose only
#endif
      m_blockchain_last_block_id = top_block_id;

      m_block_template_hash_blob = get_block_hashing_blob(m_block_template);      
      if (get_nonce_from_blockblob(m_block_template_hash_blob) != 0)
      {
        LOG_PRINT_RED("non-zero nonce in generated block template", LOG_LEVEL_0);
        set_nonce_to_blockblob(m_block_template_hash_blob, 0);
      }
      m_prev_block_template_ethash = m_block_template_ethash;
      m_block_template_ethash = crypto::cn_fast_hash(m_block_template_hash_blob.data(), m_block_template_hash_blob.size());
      m_block_template_update_ts = epee::misc_utils::get_tick_count();

      set_work_for_all_workers(); // notify all workers of updated work

      return true;
    }

    void set_work_for_all_workers(protocol_handler_t* ph_to_skip = nullptr)
    {
      LOG_PRINT("stratum_protocol_handler_config::set_work_for_all_workers()", LOG_LEVEL_4);
      CRITICAL_REGION_LOCAL(m_ph_map_lock);
      for (auto& ph : m_protocol_handlers)
      {
        if (ph.second == ph_to_skip)
          continue;
        ph.second->get_context().adjust_worker_difficulty_if_needed(); // some miners seem to not give a f*ck about updated job taget if the block hash wasn't changed, so change difficulty only on work update
        std::string new_work_json = get_work_json(ph.second->get_context().get_worker_difficulty());
        ph.second->set_work(new_work_json);
        ph.second->send_notification(new_work_json);
      }
    }

    std::string get_work_json(const wide_difficulty_type& worker_difficulty)
    {
      CRITICAL_REGION_LOCAL(m_work_change_lock);
      if (!is_core_syncronized())
        return R"("result":[])";

      crypto::hash target_boundary = null_hash;
      difficulty_to_boundary_long(worker_difficulty, target_boundary);

      ethash_hash256 seed_hash = ethash_calculate_epoch_seed(ethash_height_to_epoch(m_block_template_height));
      return R"("result":[")" + pod_to_net_format(m_block_template_ethash) + R"(",")" + pod_to_net_format(seed_hash) + R"(",")" + pod_to_net_format_reverse(target_boundary) + R"(",")" + pod_to_net_format_reverse(m_block_template_height) + R"("])";
    }

    void update_work(protocol_handler_t* p_ph)
    {
      LOG_PRINT_CC(p_ph->get_context(), "stratum_protocol_handler_config::update_work()", LOG_LEVEL_4);
      bool updated = false;
      if (is_core_syncronized())
      {
        updated = update_block_template();
      }
      else
      {
        LP_CC_WORKER_YELLOW(p_ph->get_context(), "core is NOT synchronized, respond with empty job package", LOG_LEVEL_2);
      }
      
      if (!updated)
        p_ph->set_work(get_work_json(p_ph->get_context().get_worker_difficulty()));
    }

    bool handle_work(protocol_handler_t* p_ph, const jsonrpc_id_t& id, const std::string& worker, uint64_t nonce, const crypto::hash& block_ethash)
    {
      CRITICAL_REGION_LOCAL(m_work_change_lock);
      bool r = false;

      if (!is_core_syncronized())
      {
        // TODO: make an option for more aggressive mining in case there's a little difference in blockchain size (1..2)
        LP_CC_WORKER_BLUE(p_ph->get_context(), "Work received, but the core is NOT syncronized. Skip...", LOG_LEVEL_1);
        p_ph->send_response_default(id);
        return true;
      }
  
      const uint64_t height = get_block_height(m_block_template);

      // make sure worker sent work with correct block ethash
      if (block_ethash != m_block_template_ethash)
      {
        if (block_ethash == m_prev_block_template_ethash)
        {
          // Got stale share, do nothing. In future it can be used for more aggressive mining strategies
          LP_CC_WORKER_BLUE(p_ph->get_context(), "got stale share, skip it", LOG_LEVEL_1);
          p_ph->send_response_default(id);
          p_ph->get_context().increment_stale_shares_count();
          return true;
        }

        LP_CC_WORKER_RED(p_ph->get_context(), "wrong work submitted, ethhash " << block_ethash << ", expected: " << m_block_template_ethash, LOG_LEVEL_0);
        p_ph->send_response_error(id, JSONRPC_ERROR_CODE_DEFAULT, "wrong work");
        p_ph->get_context().increment_wrong_shares_count();
        return false;
      }

      crypto::hash block_pow_hash = get_block_longhash(height, m_block_template_ethash, nonce);
      wide_difficulty_type worker_difficulty = p_ph->get_context().get_worker_difficulty();

      if (!check_hash(block_pow_hash, worker_difficulty))
      {
        LP_CC_WORKER_RED(p_ph->get_context(), "block pow hash " << block_pow_hash << " doesn't meet worker difficulty: " << worker_difficulty << ENDL <<
          "nonce: " << nonce << " (0x" << epee::string_tools::pod_to_hex(nonce) << ")", LOG_LEVEL_0);
        p_ph->send_response_error(id, JSONRPC_ERROR_CODE_DEFAULT, "not enough work was done");
        p_ph->get_context().increment_wrong_shares_count();
        return false;
      }

      p_ph->send_response_default(id);
      p_ph->get_context().increment_normal_shares_count();
      m_shares_per_minute.chick();

      if (!check_hash(block_pow_hash, m_network_difficulty))
      {
        // work is enough for worker difficulty, but not enough for network difficulty -- it's okay, move on!
        LP_CC_WORKER_GREEN(p_ph->get_context(), "share found for difficulty " << worker_difficulty << ", nonce: 0x" << epee::string_tools::pod_to_hex(nonce), LOG_LEVEL_1);
        LP_CC_WORKER_GREEN(p_ph->get_context(), "shares: " << p_ph->get_context().get_current_valid_shares_count() << ", share period av: " << p_ph->get_context().get_average_share_period_ms() << ", target: " << p_ph->get_context().m_vd_params.target_time_ms
          << ", variance: " << int64_t(100 * p_ph->get_context().get_average_share_period_ms() / p_ph->get_context().m_vd_params.target_time_ms - 100) << " %", LOG_LEVEL_3);
        return true;
      }

      // seems we've just found a block!
      // create a block template and push it to the core
      m_block_template.nonce = nonce;
      crypto::hash block_hash = get_block_hash(m_block_template);

      LP_CC_WORKER_GREEN(p_ph->get_context(), "block found " << block_hash << " at height " << height << " for difficulty " << m_network_difficulty << " pow: " << block_pow_hash << ENDL <<
        "nonce: " << nonce << " (0x" << epee::string_tools::pod_to_hex(nonce) << ")", LOG_LEVEL_1);

      block_verification_context bvc = AUTO_VAL_INIT(bvc);
      r = m_p_core->handle_block_found(m_block_template, &bvc, false);
      if (r)
      {
        if (!bvc.m_verification_failed && !bvc.m_added_to_altchain && bvc.m_added_to_main_chain && !bvc.m_already_exists && !bvc.m_marked_as_orphaned)
        {
          LP_CC_WORKER_GREEN(p_ph->get_context(), "found block " << block_hash << " at height " << height << " was successfully added to the blockchain, difficulty " << m_network_difficulty, LOG_LEVEL_0);
          r = update_block_template();
          CHECK_AND_ASSERT_MES_NO_RET(r, "Stratum: internal error. Block template wasn't updated as expected after handling found block.");
          p_ph->get_context().increment_blocks_count();
          ++m_total_blocks_found;
        }
        else
        {
          LP_CC_WORKER_RED(p_ph->get_context(), "block " << block_hash << " at height " << height << " was NOT added to the blockchain:" << ENDL <<
            "    verification_failed: " << bvc.m_verification_failed  << ENDL <<
            "    added_to_altchain:   " << bvc.m_added_to_altchain      << ENDL <<
            "    added_to_main_chain: " << bvc.m_added_to_main_chain  << ENDL <<
            "    already_exists:      " << bvc.m_already_exists       << ENDL <<
            "    marked_as_orphaned:  " << bvc.m_marked_as_orphaned, LOG_LEVEL_0);
        }
      }
      else
      {
        LP_CC_WORKER_RED(p_ph->get_context(), "block " << block_hash << " was rejected by the core", LOG_LEVEL_0);
      }

      return true;
    }

    bool handle_login(protocol_handler_t* p_ph, const jsonrpc_id_t& id, const std::string& user_str, const std::string& pass_str, const std::string& worker_str, uint64_t start_difficulty)
    {
      CRITICAL_REGION_LOCAL(m_work_change_lock);
      bool r = false, error = false;
      std::stringstream error_str;

      if (user_str == WORKER_ALLOWED_USERNAME)
      {
        // it's a valid login only in case miner address was already set
        if (m_miner_addr == null_pub_addr)
        {
          error = true;
          error_str << "trying to log in with '" WORKER_ALLOWED_USERNAME "' username, while mining address WAS NOT previously set in daemon. Set mining address in daemon OR use it as a username.";
        }
      }
      else
      {
        // mining address is used as username, make sure it's correct and match with previously set
        account_public_address address = null_pub_addr;
        r = get_account_address_from_str(address, user_str);
        if (!r)
        {
          error = true;
          error_str << "can't parse wallet address from username: " << user_str << ".";
        }

        if (m_miner_addr != null_pub_addr && address != m_miner_addr)
        {
          error = true;
          error_str << "wallet address " << user_str << " doesn't match the address previously set in daemon and/or other workers.";
        }

        if (!error)
          set_miner_address(address);
      }

      if (error)
      {
        LP_CC_WORKER_RED(p_ph->get_context(), error_str.str() << " Connection will be dropped.", LOG_LEVEL_0);
        p_ph->send_response_error(id, JSONRPC_ERROR_CODE_DEFAULT, error_str.str());
        return false;
      }

      p_ph->get_context().set_worker_name(worker_str);
      if (start_difficulty != 0)
        p_ph->get_context().set_worker_difficulty(start_difficulty);

      LP_CC_WORKER_GREEN(p_ph->get_context(), "logged in with username " << user_str << ", start difficulty: " << (start_difficulty != 0 ? std::to_string(start_difficulty) : "default"), LOG_LEVEL_0);
      p_ph->send_response_default(id);
      
      // send initial work
      update_work(p_ph);

      return true;
    }

    bool handle_submit_hashrate(protocol_handler_t* p_ph, uint64_t rate, const crypto::hash& rate_submit_id)
    {
      LP_CC_WORKER_CYAN(p_ph->get_context(), "reported hashrate: " << HR_TO_STREAM_IN_MHS_3P(rate) << " Mh/s" << 
        ", estimated hashrate: " << HR_TO_STREAM_IN_MHS_3P(p_ph->get_context().estimate_worker_hashrate()) << " Mh/s, run time: " <<
        epee::misc_utils::get_time_interval_string(p_ph->get_context().get_hr_estimate_duration()), LOG_LEVEL_3);
      return true;
    }

    // required member for epee::net_utils::boosted_tcp_server concept
    void on_send_stop_signal()
    {
      LOG_PRINT_L4("stratum_protocol_handler_config::on_send_stop_signal()");
      CRITICAL_REGION_LOCAL(m_ph_map_lock);
      m_protocol_handlers.clear();
    }

    // i_blockchain_update_listener member
    virtual void on_blockchain_update() override
    {
      LOG_PRINT_L3("stratum_protocol_handler_config::on_blockchain_update()");

      if (!is_core_syncronized())
        return; // don't notify workers when blockchain is synchronizing

      update_block_template();
    }

    void set_core(currency::core* c)
    {
      m_p_core = c;
      m_p_core->add_blockchain_update_listener(this);
    }

    void set_is_core_always_online(bool is_core_always_online)
    {
      m_is_core_always_online = is_core_always_online;
    }

    void set_miner_address(const account_public_address& miner_addr)
    {
      m_miner_addr = miner_addr;
    }

    bool is_core_syncronized()
    {
      if (!m_p_core)
        return false;

#if DBG_CORE_ALWAYS_SYNCRONIZED == 1
      return true; // standalone mode, usefull for debugging WITH --offline-mode option
#endif
      if (m_is_core_always_online)
        return true;

      // TODO!!! Bad design, need more correct way of getting this information
      currency::i_currency_protocol* proto = m_p_core->get_protocol();
      currency::t_currency_protocol_handler<currency::core>* protocol = dynamic_cast<currency::t_currency_protocol_handler<currency::core>*>(proto);
      if (!protocol)
        return false;
      return protocol->is_synchronized();
    }

    void set_vdiff_params(const vdiff_params_t& p)
    {
      m_vdiff_params = p;
    }

    const vdiff_params_t& get_vdiff_params() const
    {
      return m_vdiff_params;
    }

    void print_total_hashrate_if_needed()
    {
      if (epee::misc_utils::get_tick_count() - m_last_ts_total_hr_was_printed < m_total_hr_print_interval_ms)
        return;

      if (!is_core_syncronized())
      {
        LOG_PRINT_L0("Blockchain is synchronizing...");
      }

      CRITICAL_REGION_LOCAL(m_ph_map_lock);
      if (m_protocol_handlers.empty())
      {
        LOG_PRINT_CYAN("Blocks found: [" << m_total_blocks_found << "], no miners connected", LOG_LEVEL_0);
      }
      else
      {
        std::stringstream ss;
        uint64_t total_reported_hr = 0, total_estimated_hr = 0;
        for (auto& ph : m_protocol_handlers)
        {
          uint64_t reported_hr = 0, estimated_hr = 0;
          ph.second->get_hashrate(reported_hr, estimated_hr);
          total_reported_hr += reported_hr;
          total_estimated_hr += estimated_hr;
          ss << ph.second->get_context().m_worker_name << ": [" << ph.second->get_context().get_blocks_count() << "] " << HR_TO_STREAM_IN_MHS_1P(reported_hr) << " (" << HR_TO_STREAM_IN_MHS_1P(estimated_hr) << "), ";
        }
        auto s = ss.str();
        LOG_PRINT_CYAN("Blocks found: [" << m_total_blocks_found << "], total speed: " << HR_TO_STREAM_IN_MHS_3P(total_reported_hr) << " Mh/s as reported by miners (" << HR_TO_STREAM_IN_MHS_3P(total_estimated_hr) << " Mh/s estimated by the server), current shares/min: " << m_shares_per_minute.get_speed() << ENDL <<
          m_protocol_handlers.size() << " worker(s): " << s.substr(0, s.length() > 2 ? s.length() - 2 : 0), LOG_LEVEL_0);
      }

      m_last_ts_total_hr_was_printed = epee::misc_utils::get_tick_count();
    }

    void set_total_hr_print_interval_s(uint64_t s)
    {
      m_total_hr_print_interval_ms = s * 1000;
    }

    void set_block_template_update_period(uint64_t s)
    {
      m_block_template_update_pediod_ms = s * 1000;
    }

    size_t get_number_id_for_nameless_worker()
    {
      CRITICAL_REGION_LOCAL(m_generic_lock);
      return m_nameless_worker_id++;
    }


    size_t m_max_packet_size;

  private:
    typedef std::unordered_map<boost::uuids::uuid, protocol_handler_t* , boost::hash<boost::uuids::uuid> > protocol_handlers_map;
    typedef epee::math_helper::speed<60 * 1000 /* ms */> shares_per_minute_rate_t;

    protocol_handlers_map m_protocol_handlers;
    mutable epee::critical_section m_ph_map_lock;
    mutable epee::critical_section m_work_change_lock;
    mutable epee::critical_section m_generic_lock;

    // job data
    block m_block_template;
    std::string m_block_template_hash_blob;
    crypto::hash m_block_template_ethash;
    crypto::hash m_blockchain_last_block_id;
    uint64_t m_block_template_height;
    std::atomic<uint64_t> m_block_template_update_ts;

    // previous job (for handling stale shares)
    crypto::hash m_prev_block_template_ethash;

    vdiff_params_t m_vdiff_params;
    wide_difficulty_type m_network_difficulty;

    core* m_p_core;
    account_public_address m_miner_addr; // all workers will share the same miner address
    std::atomic<uint64_t> m_last_ts_total_hr_was_printed;
    uint64_t m_total_hr_print_interval_ms;
    uint64_t m_block_template_update_pediod_ms;
    size_t m_nameless_worker_id;
    size_t m_total_blocks_found;
    shares_per_minute_rate_t m_shares_per_minute;
    bool m_is_core_always_online;

    std::atomic<bool> m_stop_flag;
    std::thread m_blocktemplate_update_thread;
  }; // struct stratum_protocol_handler_config

  //==============================================================================================================================

  template<typename connection_context_t>
  class stratum_protocol_handler
  {
  public:
    typedef stratum_protocol_handler<connection_context_t> this_t;
    typedef epee::net_utils::connection<this_t> connection_t;

    typedef connection_context_t connection_context; // required type for epee::net_utils::boosted_tcp_server concept
    typedef stratum_protocol_handler_config<connection_context_t> config_type; // required type for epee::net_utils::boosted_tcp_server concept

    // required member for epee::net_utils::boosted_tcp_server concept
    stratum_protocol_handler(connection_t* p_connection, config_type& config, connection_context_t& context)
      : m_p_connection(p_connection)
      , m_config(config)
      , m_context(context)
      , m_connection_initialized(false)
      , m_last_reported_hashrate(0)
      , m_use_standard_stratum(false)
    {
      LOG_PRINT_CC(m_context, "stratum_protocol_handler::ctor()", LOG_LEVEL_4);
    }

    ~stratum_protocol_handler()
    {
      NESTED_TRY_ENTRY();

      if (m_connection_initialized)
      {
        m_config.remove_protocol_handler(this);
        m_connection_initialized = false;
      }

      LOG_PRINT_CC(m_context, "stratum_protocol_handler::dtor()", LOG_LEVEL_4);

      NESTED_CATCH_ENTRY(__func__);
    }

    // required member for epee::net_utils::boosted_tcp_server concept
    bool handle_recv(const void* p_data, size_t data_size)
    {
      const char* str = static_cast<const char*>(p_data);

      if (!m_connection_initialized)
        return false;

      if (data_size > m_config.m_max_packet_size)
      {
        LP_CC_WORKER_RED(m_context, "Maximum packet size exceeded! maximum: " << m_config.m_max_packet_size << ", received: " << data_size << ". Connection will be closed.", LOG_LEVEL_0);
        return false;
      }

      m_json_helper.feed(str, data_size);
      LP_CC_WORKER(m_context, "DATA received <<<<<<<<<<<<< " << data_size << " bytes:" << ENDL << std::string(str, data_size), LOG_LEVEL_4);

      if (m_json_helper.has_objects())
      {
        std::string json;
        while (m_json_helper.pop_object(json))
        {
          epee::serialization::portable_storage ps;
          if (ps.load_from_json(json))
          {
            if (!handle_json_request(ps, json))
            {
              LP_CC_WORKER_RED(m_context, "JSON request handling failed. JSON:" << ENDL << json << ENDL, LOG_LEVEL_1);
            }
          }
          else
          {
            LP_CC_WORKER_RED(m_context, "JSON object detected, but can't be parsed. JSON:" << ENDL << json << ENDL, LOG_LEVEL_1);
          }
        }
      }

      return true;
    }

    bool handle_json_request(epee::serialization::portable_storage& ps, const std::string& json)
    {
      std::stringstream error_stream;
      jsonrpc_id_t id = jsonrpc_id_null;
      read_jsonrpc_id(ps, id);

      std::string method;
      if (ps_get_value_noexcept(ps, "method", method, nullptr))
      {
        epee::serialization::portable_storage::hsection params_section = ps.open_section("params", nullptr);
        auto handler_it = m_methods_handlers.find(method);
        if (handler_it == m_methods_handlers.end())
        {
          error_stream << "unknown method is requested: " << method << ENDL << "JSON request: " << json;
          LP_CC_WORKER_RED(m_context, error_stream.str(), LOG_LEVEL_1);
          send_response_error(id, JSONRPC_ERROR_CODE_METHOD_NOT_FOUND, error_stream.str());
          return false;
        }

        return (this->*(handler_it->second))(id, ps, params_section);
      }

      // if it's no a method call -- it should be result
      std::string result;
      if (ps_get_value_noexcept(ps, "result", result, nullptr))
      {
        std::string error;
        ps_get_value_noexcept(ps, "error", error, nullptr);
        epee::serialization::portable_storage::hsection error_section = ps.open_section("error", nullptr);
        if (error_section != nullptr || !error.empty())
        {
          LP_CC_WORKER_RED(m_context, "received an error: " << json, LOG_LEVEL_1);
          return true; // means it is handled ok
        }
      }

      LP_CC_WORKER_YELLOW(m_context, "Unrecongized request received: " << json, LOG_LEVEL_2);
      return true; // means it handled ok
    }

    static void init()
    {
      if (m_methods_handlers.empty())
      {
        m_methods_handlers.insert(std::make_pair("eth_submitLogin",             &this_t::handle_method_eth_submitLogin));
        m_methods_handlers.insert(std::make_pair("eth_getWork",                 &this_t::handle_method_eth_getWork));
        m_methods_handlers.insert(std::make_pair("eth_submitHashrate",          &this_t::handle_method_eth_submitHashrate));
        m_methods_handlers.insert(std::make_pair("eth_submitWork",              &this_t::handle_method_eth_submitWork));
        // Standard ETH stratum (mining.*) — used by XMRig-based miners
        m_methods_handlers.insert(std::make_pair("mining.subscribe",            &this_t::handle_method_mining_subscribe));
        m_methods_handlers.insert(std::make_pair("mining.authorize",            &this_t::handle_method_mining_authorize));
        m_methods_handlers.insert(std::make_pair("mining.submit",               &this_t::handle_method_mining_submit));
        m_methods_handlers.insert(std::make_pair("mining.extranonce.subscribe", &this_t::handle_method_mining_extranonce_subscribe));
      }
    }

    bool handle_method_eth_submitLogin(const jsonrpc_id_t& id, epee::serialization::portable_storage& ps, epee::serialization::portable_storage::hsection params_section)
    {
      std::string user_str, pass_str;
      epee::serialization::harray params_array = ps.get_first_value("params", user_str, nullptr);
      if (params_array != nullptr)
        ps.get_next_value(params_array, pass_str);

      std::string worker_str;
      ps_get_value_noexcept(ps, "worker", worker_str, nullptr);
      if (worker_str.empty())
        worker_str = std::to_string(m_config.get_number_id_for_nameless_worker());

      uint64_t start_difficulty = 0; // default
      size_t start_diff_delim_pos = user_str.find('-');
      if (start_diff_delim_pos != std::string::npos)
      {
        // extract start difficulty from username presuming it complies format "username.difficulty"
        std::string start_difficulty_str = user_str.substr(start_diff_delim_pos + 1);
        TRY_ENTRY()
          start_difficulty = std::stoull(start_difficulty_str);
        CATCH_ENTRY_CUSTOM("submitLogin", { LOG_PRINT_L0(worker_str << ": Can't parse start difficulty from " << start_difficulty_str); }, false);
        user_str = user_str.substr(0, start_diff_delim_pos);
      }

      LOG_PRINT_CC(m_context, "Stratum [submitLogin] USER: " << user_str << ", pass: " << pass_str << ", worker: " << worker_str << ", start diff.: " << (start_difficulty == 0 ? std::string("default") : std::to_string(start_difficulty)), LOG_LEVEL_3);
      return m_config.handle_login(this, id, user_str, pass_str, worker_str, start_difficulty);
    }

    bool handle_method_eth_getWork(const jsonrpc_id_t& id, epee::serialization::portable_storage& ps, epee::serialization::portable_storage::hsection params_section)
    {
      m_config.update_work(this);

      CRITICAL_REGION_LOCAL(m_work_change_lock);
      if (!m_cached_work_json.empty())
        send_response(id, m_cached_work_json);

      return true;
    }

    bool handle_method_eth_submitHashrate(const jsonrpc_id_t& id, epee::serialization::portable_storage& ps, epee::serialization::portable_storage::hsection params_section)
    {
      std::string rate_str, rate_submit_id_str;
      epee::serialization::harray params_array = ps.get_first_value("params", rate_str, nullptr);
      bool r = params_array != nullptr && ps.get_next_value(params_array, rate_submit_id_str);
      CHECK_AND_ASSERT_MES(r, false, "Incorrect parameters");

      struct { uint64_t low, high, higher, highest; } rate_256 = { 0 }; // this will allow any length of data from 64 to 256 bits (helpful for odd miners)
      CHECK_AND_ASSERT_MES(pod_from_net_format_reverse(rate_str, rate_256, true), false, "Can't parse rate from " << rate_str);
      CHECK_AND_ASSERT_MES(rate_256.high == 0 && rate_256.higher == 0 && rate_256.highest == 0, false, "rate overflow, rate str: " << rate_str);
      crypto::hash rate_submit_id = null_hash;
      CHECK_AND_ASSERT_MES(pod_from_net_format(rate_submit_id_str, rate_submit_id), false, "Can't parse rate_submit_id from " << rate_submit_id_str);

      m_last_reported_hashrate = rate_256.low;
      return m_config.handle_submit_hashrate(this, rate_256.low, rate_submit_id);
    }

    bool handle_method_eth_submitWork(const jsonrpc_id_t& id, epee::serialization::portable_storage& ps, epee::serialization::portable_storage::hsection params_section)
    {
      bool r = true;
      std::string nonce_str, header_str, mixhash_str;
      epee::serialization::harray params_array = ps.get_first_value("params", nonce_str, nullptr);
      r = params_array != nullptr && ps.get_next_value(params_array, header_str);
      r = params_array != nullptr && ps.get_next_value(params_array, mixhash_str);
      CHECK_AND_ASSERT_MES(r, false, "Incorrect parameters");

      std::string worker;
      ps_get_value_noexcept(ps, "worker", worker, nullptr);

      uint64_t nonce = 0;
      CHECK_AND_ASSERT_MES(pod_from_net_format_reverse(nonce_str, nonce, true), false, "Can't parse nonce from " << nonce_str);
      crypto::hash header_hash = null_hash;
      CHECK_AND_ASSERT_MES(pod_from_net_format(header_str, header_hash), false, "Can't parse header hash from " << header_str);

      return m_config.handle_work(this, id, worker, nonce, header_hash);
    }

    // --- Standard ETH stratum (mining.*) handlers ---

    bool handle_method_mining_subscribe(const jsonrpc_id_t& id, epee::serialization::portable_storage& ps, epee::serialization::portable_storage::hsection params_section)
    {
      m_use_standard_stratum = true;
      // Respond with: [[["mining.notify", "subscription_id"]], "extranonce"]
      // Use "0000" as a default 2-byte extra nonce
      send_response(id, R"("result":[["mining.notify","1"],"0000"])");
      return true;
    }

    bool handle_method_mining_authorize(const jsonrpc_id_t& id, epee::serialization::portable_storage& ps, epee::serialization::portable_storage::hsection params_section)
    {
      // params: [user, pass] — same format as eth_submitLogin
      std::string user_str, pass_str;
      epee::serialization::harray params_array = ps.get_first_value("params", user_str, nullptr);
      if (params_array != nullptr)
        ps.get_next_value(params_array, pass_str);

      std::string worker_str = std::to_string(m_config.get_number_id_for_nameless_worker());

      LOG_PRINT_CC(m_context, "Stratum [mining.authorize] USER: " << user_str << ", pass: " << pass_str << ", worker: " << worker_str, LOG_LEVEL_3);
      return m_config.handle_login(this, id, user_str, pass_str, worker_str, 0);
    }

    bool handle_method_mining_submit(const jsonrpc_id_t& id, epee::serialization::portable_storage& ps, epee::serialization::portable_storage::hsection params_section)
    {
      // params: [worker, job_id, nonce, header_hash, mix_hash]
      std::string worker_str, job_id_str, nonce_str, header_str, mixhash_str;
      epee::serialization::harray params_array = ps.get_first_value("params", worker_str, nullptr);
      bool r = params_array != nullptr && ps.get_next_value(params_array, job_id_str);
      r = r && ps.get_next_value(params_array, nonce_str);
      r = r && ps.get_next_value(params_array, header_str);
      r = r && ps.get_next_value(params_array, mixhash_str);
      CHECK_AND_ASSERT_MES(r, false, "Incorrect parameters for mining.submit");

      uint64_t nonce = 0;
      CHECK_AND_ASSERT_MES(pod_from_net_format_reverse(nonce_str, nonce, true), false, "Can't parse nonce from " << nonce_str);
      crypto::hash header_hash = null_hash;
      CHECK_AND_ASSERT_MES(pod_from_net_format(header_str, header_hash), false, "Can't parse header hash from " << header_str);

      return m_config.handle_work(this, id, worker_str, nonce, header_hash);
    }

    bool handle_method_mining_extranonce_subscribe(const jsonrpc_id_t& id, epee::serialization::portable_storage& ps, epee::serialization::portable_storage::hsection params_section)
    {
      send_response(id, R"("result":true)");
      return true;
    }

    // ---

    void send(const std::string& data)
    {
      static_cast<epee::net_utils::i_service_endpoint*>(m_p_connection)->do_send(data.c_str(), data.size());
      LOG_PRINT_CC(m_context, "DATA sent >>>>>>>>>>>>> " << ENDL << data, LOG_LEVEL_4);
    }

    void send_notification(const std::string& json)
    {
      if (m_use_standard_stratum)
      {
        // Convert EthProxy "result":["header","seed","target","height"] to
        // standard stratum mining.notify format: "method":"mining.notify","params":["job_id","header","seed","target",true,"height"]
        // json looks like: "result":["0xHEADER","0xSEED","0xTARGET","0xHEIGHT"]
        // We need: "method":"mining.notify","params":["job_id","0xHEADER","0xSEED","0xTARGET",true,"0xHEIGHT"]
        std::string notify_json = json;
        size_t pos = notify_json.find(R"("result":)");
        if (pos != std::string::npos)
        {
          // Extract the array content from "result":[...]
          size_t arr_start = notify_json.find('[', pos);
          size_t arr_end = notify_json.rfind(']');
          if (arr_start != std::string::npos && arr_end != std::string::npos)
          {
            std::string arr_content = notify_json.substr(arr_start + 1, arr_end - arr_start - 1);
            // Insert job ID at beginning and clean_jobs=true before height
            // arr_content is: "0xHEADER","0xSEED","0xTARGET","0xHEIGHT"
            // We need:        "job_id","0xHEADER","0xSEED","0xTARGET",true,"0xHEIGHT"
            static uint64_t s_job_counter = 0;
            std::string job_id = "\"" + std::to_string(++s_job_counter) + "\"";

            // Find the last comma to insert clean_jobs before height
            size_t last_comma = arr_content.rfind(',');
            if (last_comma != std::string::npos)
            {
              std::string params = job_id + "," + arr_content.substr(0, last_comma) + ",true" + arr_content.substr(last_comma);
              send(R"({"jsonrpc":"2.0","method":"mining.notify","params":[)" + params + "]}\n");
              return;
            }
          }
        }
      }
      // EthProxy mode (default): send as-is
      // JSON-RPC 2.0 spec: "A Notification is a Request object without an "id" member."
      send(R"({"jsonrpc":"2.0",)" + json + "}" "\n"); // LF character is not specified by JSON-RPC standard, but it is REQUIRED by ethminer 0.12 to work
    }
    
    void send_response_method(const jsonrpc_id_t& id, const std::string& method, const std::string& response)
    {
      send_response(id, std::string(R"("method":")") + method + R"(",)" + response);
    }

    void send_response(const jsonrpc_id_t& id, const std::string& response)
    {
      send(R"({"jsonrpc":"2.0","id":)" + jsonrpc_id_to_value_str(id) + "," + response + "}" "\n"); // LF character is not specified by JSON-RPC standard, but it is REQUIRED by ethminer 0.12 to work
    }

    void send_response_default(const jsonrpc_id_t& id)
    {
      send_response(id, R"("result":true)");
    }

    void send_response_error(const jsonrpc_id_t& id, int64_t error_code, const std::string& error_message)
    {
      send_response(id, R"("error":{"code":)" + std::to_string(error_code) + R"(,"message":")" + error_message + R"("})");
    }

    void set_work(const std::string& json)
    {
      CRITICAL_REGION_LOCAL(m_work_change_lock);
      if (m_cached_work_json != json)
      {
        LP_CC_WORKER(m_context, "work updated: " << json, LOG_LEVEL_2);
      }
      m_cached_work_json = json;
    }

    // required member for epee::net_utils::boosted_tcp_server concept
    void handle_qued_callback()
    {
    }

    // required member for epee::net_utils::boosted_tcp_server concept
    bool after_init_connection()
    {
      LOG_PRINT_CC(m_context, "stratum_protocol_handler::after_init_connection()", LOG_LEVEL_4);
      if (!m_connection_initialized)
      {
        m_connection_initialized = true;
        m_config.add_protocol_handler(this);
        m_context.init_and_start_timers(m_config.get_vdiff_params());
      }
      LP_CC_WORKER_CYAN(m_context, "connected", LOG_LEVEL_1);
      return true;
    }

    // required member for epee::net_utils::boosted_tcp_server concept
    bool release_protocol()
    {
      LOG_PRINT_CC(m_context, "stratum_protocol_handler::release_protocol()", LOG_LEVEL_4);
      LP_CC_WORKER(m_context, "disconnected", LOG_LEVEL_0);
      return true;
    }

    connection_context_t& get_context() { return m_context; }
    const connection_context_t& get_context() const { return m_context; }

    void get_hashrate(uint64_t& last_reported_hr, uint64_t& estimated_hr)
    {
      last_reported_hr = m_last_reported_hashrate;
      estimated_hr = m_context.estimate_worker_hashrate();
    }


  private:
    connection_t* m_p_connection; // m_p_connection owns this protocol handler as data member, so back link is a simple pointer
    config_type& m_config;
    connection_context_t& m_context;

    json_helper m_json_helper;
    std::string m_cached_work_json;

    epee::critical_section m_work_change_lock;
    uint64_t m_last_reported_hashrate;
    bool m_use_standard_stratum; // true = mining.* protocol, false = eth_* EthProxy protocol

    typedef bool (this_t::*method_handler_func_t)(const jsonrpc_id_t& id, epee::serialization::portable_storage& ps, epee::serialization::portable_storage::hsection params_section);
    static std::unordered_map<std::string, method_handler_func_t> m_methods_handlers;

    std::atomic<bool> m_connection_initialized;
  }; // class stratum_protocol_handler
//==============================================================================================================================
  typedef stratum_protocol_handler<stratum_connection_context> protocol_handler_t;
  typedef epee::net_utils::boosted_tcp_server<protocol_handler_t> tcp_server_t;

  // static memeber definition
  template<typename connection_context_t>
  std::unordered_map<std::string, typename stratum_protocol_handler<connection_context_t>::method_handler_func_t> stratum_protocol_handler<connection_context_t>::m_methods_handlers;
} // anonumous namespace

//------------------------------------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------------------------------
struct stratum_server_impl
{
  tcp_server_t server;
};
//------------------------------------------------------------------------------------------------------------------------------
stratum_server::stratum_server(core* c)
  : m_p_core(c)
  , m_threads_count(0)
{
  m_impl = new stratum_server_impl();
}
//------------------------------------------------------------------------------------------------------------------------------
stratum_server::~stratum_server()
{
  delete m_impl;
  m_impl = nullptr;
}
//------------------------------------------------------------------------------------------------------------------------------
void stratum_server::init_options(boost::program_options::options_description& desc)
{
  stratum_protocol_handler<stratum_connection_context>::init();

  command_line::add_arg(desc, arg_stratum);
  command_line::add_arg(desc, arg_stratum_bind_ip);
  command_line::add_arg(desc, arg_stratum_bind_port);
  command_line::add_arg(desc, arg_stratum_threads);
  command_line::add_arg(desc, arg_stratum_miner_address);
  command_line::add_arg(desc, arg_stratum_vdiff_target_min);
  command_line::add_arg(desc, arg_stratum_vdiff_target_max);
  command_line::add_arg(desc, arg_stratum_vdiff_target_time);
  command_line::add_arg(desc, arg_stratum_vdiff_retarget_time);
  command_line::add_arg(desc, arg_stratum_vdiff_retarget_shares);
  command_line::add_arg(desc, arg_stratum_vdiff_variance_percent);
  command_line::add_arg(desc, arg_stratum_block_template_update_period);
  command_line::add_arg(desc, arg_stratum_hr_print_interval);
  command_line::add_arg(desc, arg_stratum_always_online);

}
//------------------------------------------------------------------------------------------------------------------------------
bool stratum_server::should_start(const boost::program_options::variables_map& vm)
{
  return command_line::get_arg(vm, arg_stratum);
}
//------------------------------------------------------------------------------------------------------------------------------
bool stratum_server::init(const boost::program_options::variables_map& vm)
{
  bool r = false;
  m_impl->server.set_threads_prefix("ST");

  std::string bind_ip_str = command_line::get_arg(vm, arg_stratum_bind_ip);
  std::string bind_port_str = command_line::get_arg(vm, arg_stratum_bind_port);
  m_threads_count = command_line::get_arg(vm, arg_stratum_threads);

  auto& config = m_impl->server.get_config_object();
  config.set_core(m_p_core);
  
  if (command_line::has_arg(vm, arg_stratum_always_online))
  {
     config.set_is_core_always_online(command_line::get_arg(vm, arg_stratum_always_online));
  }


  if (command_line::has_arg(vm, arg_stratum_miner_address))
  {
    std::string miner_address_str = command_line::get_arg(vm, arg_stratum_miner_address);
    account_public_address miner_address = null_pub_addr;
    r = get_account_address_from_str(miner_address, miner_address_str);
    CHECK_AND_ASSERT_MES(r, false, "Stratum server: invalid miner address given: " << miner_address_str);
    config.set_miner_address(miner_address);
  }

  config.set_vdiff_params(
    vdiff_params_t(
      command_line::get_arg(vm, arg_stratum_vdiff_target_min),
      command_line::get_arg(vm, arg_stratum_vdiff_target_max),
      command_line::get_arg(vm, arg_stratum_vdiff_target_time),
      command_line::get_arg(vm, arg_stratum_vdiff_retarget_time),
      command_line::get_arg(vm, arg_stratum_vdiff_retarget_shares),
      command_line::get_arg(vm, arg_stratum_vdiff_variance_percent)
    )
  );

  config.set_block_template_update_period(command_line::get_arg(vm, arg_stratum_block_template_update_period));

  config.set_total_hr_print_interval_s(command_line::get_arg(vm, arg_stratum_hr_print_interval));

  LOG_PRINT_L0("Stratum server: start listening at " << bind_ip_str << ":" << bind_port_str << "...");
  r = m_impl->server.init_server(bind_port_str, bind_ip_str);
  CHECK_AND_ASSERT_MES(r, false, "Stratum server: initialization failure");

  return true;
}
//------------------------------------------------------------------------------------------------------------------------------
bool stratum_server::run(bool wait /* = true */)
{
  //m_impl->server.get_config_object().test();

  LOG_PRINT("Stratum server: start net server with " << m_threads_count << " threads...", LOG_LEVEL_0);
  if (!m_impl->server.run_server(m_threads_count, wait))
  {
    LOG_ERROR("Stratum server: net server failure");
    return false;
  }

  if (wait)
    LOG_PRINT("Stratum server: net server stopped", LOG_LEVEL_0);
  return true;
}
//------------------------------------------------------------------------------------------------------------------------------
bool stratum_server::deinit()
{
  return m_impl->server.deinit_server();
}
//------------------------------------------------------------------------------------------------------------------------------
bool stratum_server::timed_wait_server_stop(uint64_t ms)
{
  return m_impl->server.timed_wait_server_stop(ms);
}
//------------------------------------------------------------------------------------------------------------------------------
bool stratum_server::send_stop_signal()
{
  m_impl->server.send_stop_signal();
  return true;
}
//------------------------------------------------------------------------------------------------------------------------------

} // namespace currency
