{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
-- |
-- Module: Worker.Stratum.Server
-- Copyright: Copyright © 2021 Kadena LLC.
-- License: MIT
-- Maintainer: Lars Kuhtz <lars@kadena.io>
-- Stability: experimental
--
-- TODO
--
module Worker.Stratum.Server
( withStratumServer
, Job(..)
, StratumServerCtx(..) -- TODO only expose API
, StratumDifficulty(..)
, stratumDifficultyFromText
) where
import Control.Concurrent.Async
import Control.Concurrent.MVar
import Control.Concurrent.STM
import Control.Monad
import Control.Monad.Catch
import Control.Monad.IO.Class
import qualified Data.Aeson as A
import qualified Data.Aeson.Types as A
import qualified Data.Attoparsec.ByteString as P
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.HashMap.Strict as HM
import Data.Int
import Data.IORef
import Data.Maybe
import Data.Streaming.Network
import qualified Data.Text as T
import Network.HostAddress
import Numeric.Natural
import qualified Streaming.Prelude as S
import System.Clock
import System.IO.Unsafe
import qualified System.LogLevel as L
import System.Random.MWC
import Text.Read
-- internal modules
import JsonRpc
import Logger
import Target
import Utils
import Worker
import WorkerUtils
import Worker.Stratum.Protocol
-- -------------------------------------------------------------------------- --
-- Clients and Shares
-- | 16 bit allows for 65536 mining clients
--
-- nonce1Size :: NonceSize
-- nonce1Size = 2
data PoolCtx = PoolCtx
{ _workerAuthorization :: Authorize
}
-- | A share is a solution of a job with respect to the session target.
--
-- Shares are the basis for paying out miners. This structure stores all
-- information about a share that is needed for a pool to implement differnt
-- payment methods.
--
data Share = Share
{ _shareId :: !JobId
, _shareTarget :: !Target
, _shareWork :: !Work
-- ^ work with nonce of the share
, _shareMiner :: !Username
, _shareWorkerId :: !ClientWorker
, _shareIsBlock :: !Bool
}
data MiningClient = MiningClient
{ _miningClientNonce1 :: !Nonce1
, _miningClientCurrentTarget :: !Target
, _miningClientShares :: !Share
}
-- authorize :: Username -> WorkerId -> Password -> IO Bool
-- authorize = error "TODO"
-- -------------------------------------------------------------------------- --
-- Intialize Global Stratum Server
serverSalt :: Int
serverSalt = unsafePerformIO $ do
createSystemRandom >>= uniform
{-# NOINLINE serverSalt #-}
type Authorize = Username -> Password -> IO (Either T.Text ())
-- | TODO distinguish between job and share:
--
-- Job: global from worker, solution for block
-- Share: session, solution for share target
--
data Job = Job
{ _jobId :: !JobId
, _jobTarget :: !Target
, _jobWork :: !Work
, _jobResult :: !(MVar Nonce)
}
incrementJobTime :: Int64 -> Job -> Job
incrementJobTime i job = job
{ _jobWork = incrementTimeMicros i (_jobWork job)
}
noopJob :: Job
noopJob = unsafePerformIO $ Job noJobId nullTarget (Work "") <$> newEmptyMVar
{-# NOINLINE noopJob #-}
-- | Stratum Server Context
--
-- A stratum server distributes work to clients. Solved work is recorded in
-- shares and the server keeps track of those.
--
-- Work is distributed in a non-overlapping way by assigning clients nonce
-- ranges. A nonce range is defined through an nonce prefix. The nonce prefix
-- is assigned during subscription.
--
-- The difficulty of a share is defined via a target.
--
-- Targets are adjusted from time to time and are the basis for measuring the
-- difficulty of the work that determines the value of share.
--
-- JobIds are used to match solutions (nonces) to the respective work work
-- items.
--
data StratumServerCtx = StratumServerCtx
{ _ctxAuthorizeFn :: !Authorize
-- ^ function for user authentication and authorization. This sets
-- the public key for paying out the mining rewards for a session.
--
-- Kadena pools don't use passwords and the scope of authorization is
-- the stratum session.
--
-- For bookkeeping it is also possible to associate a device/worker id
-- with the shares from a session.
, _ctxJobs :: !(TVar (HM.HashMap JobId Job))
-- ^ there might be some contention on this variable, but we don't care
-- too much. It is modified only by the workers, which access it for
-- each new work item that they get. That's roughly the block rate
-- time the numbers of worker. The latter number should be small.
-- (there's little benefit in having a larger number)
--
-- It is read by all sessions using readTVarIO. If a job disappears
-- while processing the share, the share is still counted, even if the
-- block is solved in between. So, the time of the `readTVarIO` of the
-- job marks the point in time until a share must be found. (we could
-- change that by doing another read + job lookup when the share is
-- recorded)
--
-- Solutions are synchronized on the result MVar within a job.
-- Contention on that is much lower, since only valid solution are
-- submitted to that variable.
, _ctxCurrentJob :: !(TVar Job)
-- ^ the most recent job. This is used as basis for the job stream
-- of each session. There is no guarantee that each session sees each
-- job. Actually, it may even make sense for sessions to ignore some of
-- these in order to reduce the number of notifications that are send to
-- the client.
, _ctxCurrentId :: !(IORef JobId)
-- ^ Ticket counter for job ids.
, _ctxDifficulty :: !StratumDifficulty
, _ctxRate :: !Natural
-- ^ Rate in milliseconds at which a jobs for a given work item are
-- emitted. Note that each indiviual stratum worker will emit jobs at
-- this rate.
}
newStratumServerCtx :: StratumDifficulty -> Natural -> IO StratumServerCtx
newStratumServerCtx spec rate = StratumServerCtx
(\_ _ -> return (Right ()))
<$> newTVarIO mempty
<*> newTVarIO noopJob
<*> newIORef noJobId
<*> pure spec
<*> pure rate
-- -------------------------------------------------------------------------- --
-- Sessions
-- | 16 bit allows for 65536 mining clients/spaces
--
-- This leaves about 2^(6*8) different nonces for the miner. That's corresponds
-- to 280TH. So a miner with 280TH/s would exhaustively search this space in 1s.
--
defaultNonce1Size :: NonceSize
defaultNonce1Size = fromJust $ nonceSize @Int 2
sessionNonce2Size :: SessionState -> NonceSize
sessionNonce2Size = complementNonceSize . nonce1Size . _sessionNonce1
data SessionState = SessionState
{ _sessionNonce1 :: !Nonce1
-- ^ a fixed unique nonce that is derived from the application
-- and a server salt. It is used as prefix to provide the session
-- with a private space of nonces.
, _sessionTarget :: !(TVar Target)
, _authorized :: !(TMVar Username)
, _subscribed :: !(TMVar Agent)
-- the followint entries are used to compute the estimated hashrate
, _sessionLastShare :: !(TVar TimeSpec)
, _sessionHashRateSum :: !(TVar HashRate)
, _sessionShardCount :: !(TVar Int)
-- ^ Estiumated hash rate of current session. Meassured as Hashes per second
}
estimateHashRate :: SessionState -> IO HashRate
estimateHashRate ctx = do
now <- getTime Monotonic
atomically $ do
lastShare <- swapTVar (_sessionLastShare ctx) now
Difficulty currentDifficulty <- targetToDifficulty <$> readTVar (_sessionTarget ctx)
HashRate prevR <- readTVar (_sessionHashRateSum ctx)
curCount <- (+ 1) <$> readTVar (_sessionShardCount ctx)
let currentPeriodSeconds = realToFrac (toNanoSecs (diffTimeSpec now lastShare)) / 1_000_000_000
let recentHashRate = currentDifficulty / currentPeriodSeconds
let newAvgHashRateSum = prevR + recentHashRate
writeTVar (_sessionHashRateSum ctx) $ HashRate newAvgHashRateSum
writeTVar (_sessionShardCount ctx) curCount
return $ HashRate $ newAvgHashRateSum / int curCount
data AppResult
= JsonRpcError T.Text
| StratumError T.Text
| TimeoutError T.Text
| ConnectionClosed T.Text
deriving (Show, Eq, Ord)
instance Exception AppResult
type RequestStream = S.Stream (S.Of MiningRequest) IO AppResult
send :: A.ToJSON a => AppData -> a -> IO ()
send app a = appWrite app (BL.toStrict $ A.encode a) >> appWrite app "\n"
replyError :: AppData -> Error -> IO ()
replyError = send
reply :: AppData -> MiningResponse -> IO ()
reply = send
targetPeriod :: Period
targetPeriod = Period 10
notify :: Logger -> AppData -> SessionState -> Job -> IO ()
notify logger app sessionCtx job = do
writeLog logger L.Info "sending notification"
send app $ Notify (_jobId job, _jobWork job, True) -- for now we always replace previous work
-- | TODO: we probably need some protection against clients keeping
-- connections open without making progress.
--
-- * limit size of messages and fail parsing if the limit is exeeced
-- * add timeout within parsing of a single message
-- * add timeout for time between messages?
--
--
messages :: SessionState -> AppData -> RequestStream
messages sessionCtx app = go mempty
where
n2s = sessionNonce2Size sessionCtx
go :: B.ByteString -> S.Stream (S.Of MiningRequest) IO AppResult
go i = P.parseWith (liftIO (appRead app)) A.json' i >>= \case
P.Fail _ path err ->
return $ JsonRpcError $ "Failed to parse JSON RPC message: " <> T.pack err <> " " <> sshow path
P.Partial _cont ->
-- Can this actually happen, or would it a P.Fail?
return $ ConnectionClosed "Connection closed unexpectedly"
P.Done i' val -> case A.parse (parseMiningRequest n2s) val of
A.Error err ->
return $ StratumError $ "Unrecognized message: " <> T.pack err <> ". " <> sshow val
A.Success result -> do
S.yield result
go i'
-- -------------------------------------------------------------------------- --
-- Stratum Server Config
data StratumDifficulty
= WorkDifficulty
| DifficultyLevel Level
| DifficultyPeriod Period
deriving (Show, Eq, Ord)
stratumDifficultyFromText :: MonadThrow m => T.Text -> m StratumDifficulty
stratumDifficultyFromText "block" = return WorkDifficulty
stratumDifficultyFromText t = case readEither @Int $ T.unpack t of
Right n
| 0 <= n && n <= 256 -> return $ DifficultyLevel $ level n
| otherwise -> throwM $ FromTextException $ "Illegal value for stratum difficulty level. Expected a number between 0 and 256 but got " <> sshow n
Left _ ->
throwM $ FromTextException $ "failed to read stratum difficulty specification: " <> t
instance A.ToJSON StratumDifficulty where
toJSON WorkDifficulty = "block"
toJSON (DifficultyLevel i) = A.toJSON i
toJSON (DifficultyPeriod i) = error "ToJSON StratumDifficulty: difficulty period is currently not supported"
instance A.FromJSON StratumDifficulty where
parseJSON v = case v of
A.String "block" -> return WorkDifficulty
i@A.Number{} -> DifficultyLevel <$> A.parseJSON i
e -> fail $ "failed to parse stratum difficulty specification: " <> show e
-- -------------------------------------------------------------------------- --
-- Target Adjustment
-- | ASICs are powerful machines. When connected to a network with low
-- difficulty it may produce a large amount of shares that can overload the
-- network, the stratum server, or the device itself. For that reason we set a
-- minimum target -- even if it means that the client does more work than needed
-- to solve a single block. In the end, producing blocks faster than they can be
-- processed down stream has no benefit and can even be harmful.
--
-- TODO: this should be configurable (possibly also on a per agent base)
--
maxSessionTarget :: Target
maxSessionTarget = mkTargetLevel $ level (42 :: Int)
-- | Start easy, the target will adjust quickly
--
initialTarget :: Target
initialTarget = maxSessionTarget
periodTolerance :: Double
periodTolerance = 0.25
-- | Purely compute a new session target
--
getNewSessionTarget
:: StratumDifficulty
-- ^ type of target computation
-> HashRate
-- ^ Current estimated hash rate
-> Target
-- ^ Current session target
-> Target
-- ^ job target
-> Maybe Target
-- ^ updated target
getNewSessionTarget stratumDifficulty currentHashRate currentTarget jobTarget
| newTarget == currentTarget = Nothing
| otherwise = Just newTarget
where
newTarget = case stratumDifficulty of
WorkDifficulty -> jobTarget
DifficultyLevel l -> mkTargetLevel l
DifficultyPeriod p -> newPeriodTarget p
-- The final target must be inbetween maxSessionTarget and jobTarget
newPeriodTarget p = max jobTarget (min maxSessionTarget candidate)
where
curD = targetToDifficulty currentTarget
newD = adjustDifficulty periodTolerance currentHashRate targetPeriod curD
candidate = leveled $ difficultyToTarget newD
-- | Update the session target when a new share is found.
--
updateSessionTarget
:: Logger
-> StratumServerCtx
-> AppData
-> SessionState
-> Job
-> IO ()
updateSessionTarget logger serverCtx app sessionCtx job = do
newHashRate <- estimateHashRate sessionCtx
-- there is the chance of some race here, if we get several shares
-- concurrently. It's possible that the effective target doesn't match the
-- latest update of the period. We anways consider all previous targets, So
-- not sure how much we care about this race.
-- FIXME Find out whether changing target slows down the ASICs
--
-- TODO: the pool context should provide guidance what session target
-- should be used.
--
currentTarget <- readTVarIO (_sessionTarget sessionCtx)
writeLog logger L.Info $ "calculating new target"
<> "; hashrate: " <> sshow newHashRate
<> "; " <> prettyTarget "session" currentTarget
<> "; " <> prettyTarget "job" (_jobTarget job)
<> "; target Priod: " <> sshow targetPeriod
case getNewSessionTarget (_ctxDifficulty serverCtx) newHashRate currentTarget (_jobTarget job) of
Just t -> do
writeLog logger L.Info $ "setting session target"
<> "; " <> prettyTarget "new" t
<> "; " <> prettyTarget "old" currentTarget
<> "; " <> prettyTarget "job" (_jobTarget job)
<> "; hashrate: " <> sshow newHashRate
send app $ SetTarget $ T1 t
atomically $ writeTVar (_sessionTarget sessionCtx) t
-- Note, that there is a small chance of a race here, if the device is really fast
-- and returns a solution before this is updated. We could solve that by
-- making the target a TMVar or MVar and taking it before we send the
-- "mining.set_target".
--
-- Most likely target changes are minor and shares are accepted even in
-- case of a race.
Nothing -> do
t <- readTVarIO $ _sessionTarget sessionCtx
writeLog logger L.Info $ "session target unchanged"
<> "; " <> prettyTarget "session" t
<> "; " <> prettyTarget "job" (_jobTarget job)
where
prettyTarget :: T.Text -> Target -> T.Text
prettyTarget l t = l <> " " <> sshow t <> "[" <> sshow (getTargetLevel t) <> "]"
-- -------------------------------------------------------------------------- --
-- Session Loop
initialSessionState :: AppData -> IO SessionState
initialSessionState app = do
SessionState sessionNonce1
<$> newTVarIO initialTarget
<*> newEmptyTMVarIO
<*> newEmptyTMVarIO
<*> (getTime Monotonic >>= newTVarIO)
<*> newTVarIO (HashRate 0)
<*> newTVarIO 0
where
sessionNonce1 = deriveNonce1 defaultNonce1Size serverSalt
$ show (appSockAddr app) <> show (appLocalAddr app)
session :: Logger -> StratumServerCtx -> AppData -> IO ()
session l ctx app = withLogTag l "Stratum Session" $ \l2 -> withLogTag l2 (sshow (appSockAddr app)) $ \logger ->
flip finally (appCloseConnection app) $ do
sessionCtx <- initialSessionState app
writeLog logger L.Info $ "new session"
<> "; sessionNonce1 " <> sshow (_sessionNonce1 sessionCtx)
-- Run Request Stream and Job Stream
r <- race (processRequests logger sessionCtx) $ do
awaitSubscribe sessionCtx
-- initial target
now <- getTime Monotonic
atomically $ writeTVar (_sessionLastShare sessionCtx) now
curJob <- liftIO $ readTVarIO (_ctxCurrentJob ctx)
let jt = _jobTarget curJob
-- TODO not sure what is a good starting value...
-- let t = max jt (min maxSessionTarget (avgTarget maxSessionTarget jt))
let t = max jt maxSessionTarget
writeLog logger L.Info $ "setting initial session target: " <> sshow t
atomically $ writeTVar (_sessionTarget sessionCtx) t
send app $ SetTarget $ T1 t
S.mapM_ (notify logger app sessionCtx) jobStream
case r of
Right _ -> writeLog logger L.Error "Stratum session ended unexpectedly because an internal chainweb-mining-client issue"
Left e@(JsonRpcError msg) -> do
replyError app $ Error (-32700, "Parse Error", A.String msg)
writeLog logger L.Warn $ "session termianted with " <> sshow e
Left e@(StratumError msg) -> do
replyError app $ Error (-32600, "Invalid Request", A.String msg)
-- FIXME be more specific (cf. json rpc internal error codes)
writeLog logger L.Warn $ "session termianted with " <> sshow e
Left e@(TimeoutError msg) -> do
replyError app $ Error (-1, "Request Timeout", A.String msg)
-- TODO not yet implemented
writeLog logger L.Warn $ "session termianted with " <> sshow e
Left e@(ConnectionClosed msg) -> do
replyError app $ Error (-2, "Connection Error", A.String msg)
-- TODO: is the connection actually closed if we receive this?
-- TODO: if the connection got closed we can't reply
writeLog logger L.Warn $ "session termianted with " <> sshow e
where
authorize = _ctxAuthorizeFn ctx
awaitSubscribe sctx = atomically $ void $ takeTMVar (_subscribed sctx)
jobStream :: S.Stream (S.Of Job) IO ()
jobStream = do
cur <- liftIO $ readTVarIO (_ctxCurrentJob ctx)
go cur
where
go :: Job -> S.Stream (S.Of Job) IO ()
go old = do
new <- liftIO $ atomically $ do
cur <- readTVar (_ctxCurrentJob ctx)
if _jobId old == _jobId cur
then retry
else return cur
S.yield new
go new
processRequests :: Logger -> SessionState -> IO AppResult
processRequests logger sessionCtx = S.mapM_ (handleRequest logger sessionCtx) (messages sessionCtx app)
handleRequest :: Logger -> SessionState -> MiningRequest -> IO ()
handleRequest logger sessionCtx = \case
-- AUTHORIZE
(Authorize mid (user, pwd)) -> do
authorize user pwd >>= \case
Right () -> reply app $ authorizeResponse mid
Left err -> reply app $ authorizeError mid err
atomically $ writeTMVar (_authorized sessionCtx) user
-- SUBSCRIBE
(Subscribe mid (agent, _)) -> do
reply app $ subscribeResponse mid (_sessionNonce1 sessionCtx) (sessionNonce2Size sessionCtx)
atomically $ writeTMVar (_subscribed sessionCtx) agent
-- SUBMIT
(Submit mid (_u, _w, j, n2)) -> withLogTag logger ("job-" <> sshow j) $ \jlog -> do
-- Do all checks before obtaining the lock on the job
readTVarIO (_ctxJobs ctx) >>= \m -> case HM.lookup j m of
-- Inactive job: Discard stale share
Nothing -> do
writeLog jlog L.Info "Discarded stale share"
reply app $ SubmitResponse mid (Right False) -- FIXME is this the correct response?
-- Active Job: Check share
Just job -> do
let n = composeNonce (_sessionNonce1 sessionCtx) n2
-- Check if share is valid (matches share target)
finalWork <- injectNonce n (_jobWork job)
-- we make sure that we never reject shares that solved a block!
-- (this add a little bit of skew to the share computation but everybody
-- benefits from it. The actual problem is that we don't associate
-- shares-works with their respective targets at notification in first place.
-- Also, this mostly affects solo miners that use sessions targets that
-- are, or a close to, the job targets.)
st <- max (_jobTarget job) <$> readTVarIO (_sessionTarget sessionCtx)
checkTarget st finalWork >>= \case
-- Invalid Share:
False -> do
writeLog jlog L.Warn "reject invalid nonce"
writeLog jlog L.Info $ "invalid nonce"
<> "; nonce2:" <> sshow n2
<> "; nonce: " <> sshow n
<> "; work: " <> sshow (_jobWork job)
<> "; final work: " <> sshow finalWork
<> "; target: " <> sshow (_jobTarget job)
<> "; session target: " <> sshow st
reply app $ SubmitResponse mid (Left $ Error (31, "invalid nonce", A.Null)) -- FIXME is this correct reponse?
-- Valid Share:
True -> do
updateSessionTarget logger ctx app sessionCtx job
-- We've got a valid share
--
-- TODO: record share in the Pool Context
writeLog jlog L.Info $ "got valid share"
<> "; nonce2:" <> sshow n2
<> "; nonce: " <> sshow n
<> "; work: " <> sshow finalWork
<> "; target: " <> sshow (_jobTarget job)
-- Check whether it is a solution for the job and
-- only submit if it is. We do this here in order to
-- fail early and avoid contention on the job result
-- MVar.
-- TODO we could save a few CPU cycles by reusing the hash
-- from the previous check
checkTarget (_jobTarget job) finalWork >>= \case
False ->
reply app $ SubmitResponse mid (Right True)
True -> do
writeLog jlog L.Info $ "solved block: nonce2:" <> sshow n2 <> "; nonce: " <> sshow n
-- Yeah, we've solved a block
-- Commit final result to job
void $ tryPutMVar (_jobResult job) n
reply app $ SubmitResponse mid (Right True)
-- -------------------------------------------------------------------------- --
-- Stratum Server
withStratumServer
:: Logger
-> Port
-> HostPreference
-> StratumDifficulty
-> Natural
-> (StratumServerCtx -> IO ())
-> IO ()
withStratumServer l port host spec rate inner = withLogTag l "Stratum Server" $ \logger -> do
ctx <- newStratumServerCtx spec rate
race (server logger ctx) (inner ctx) >>= \case
Left _ -> writeLog logger L.Error "server exited unexpectedly"
Right _ -> do
writeLog logger L.Error "mining loop existed unexpectedly"
return ()
where
server logger ctx = flip finally (writeLog logger L.Info "server stopped") $ do
writeLog logger L.Info "Start stratum server"
runTCPServer (serverSettingsTCP (int port) host) (session logger ctx)
-- -------------------------------------------------------------------------- --
-- Example sessions
-- $ telnet kda.f2pool.com 5400
-- Trying 47.111.206.170...
-- Connected to kda.f2pool.com.
-- Escape character is '^]'.
-- { "id":1, "method": "mining.authorize", "params":["136029d4a40642de75bebca833ea150fc027e08fbedd29c943aab87810e77378", ""] }
-- {"id":1,"result":true,"error":null}
-- { "id":2, "method": "mining.subscribe", "params":["my-miner", null] }
-- {"id":2,"result":[null,"09e8",6],"error":null}
-- {"id":null,"method":"mining.set_target","params":["0000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffff"]}
-- {"id":null,"method":"mining.notify","params":["6412800009e8","00000000000000001128f13f5ccf050092f4573bc51d0fcf58a3729d3d4f57f9f279bbb50c793a6446fcfd226c8a4c1d0300080000003903c8b45da2ceaf0fab587860f7868ab29ea35fc4ac16796f7dcba43749f86b0d000000e77649998da94d3be41ad1e8d4038c0d326680a7ee9fdbaccb98f98ac25f7fe512000000d88e2cf14591a36a52bf31f1176f5306a196e95dde5e18abf6af306442a402f1019fd29b54b24d0c484656cd24bc0ba6aade86888e889f9c100100000000000014e70eccbc36b15e8960c845a895817f9cc3722059a51a233eb4839ffaa5824503000000b44b93673c8f1b633403000000000000000000000000000000000000000000006e0620000000000005000000648c1c745bcf05000000000000000000",true]}
-- {"id":null,"method":"mining.notify","params":["645a800809e8","0000000000000000f4450a405ccf0500d8c9800d1a2021618d83f88b5d1b3d21fd068d40e73676fb3b8a164d8878d51e0300010000002f4fa19a2ddbc10ffb1e6076e6ca881fbd254e109b7490ac6362c8ce371e501d08000000b8b8d54da5d10f79b9920dce6eb1884ad9cedf85640eea2ef5164fea8de08a4c09000000423e846c73a0d1f7f002bb34de2ba9b4559dea999c8a00a94caa84408cf87ceff9e53f39b6b03dbb6e67fe63d1659afc888d4842049ac7260f01000000000000dcc0c0a68efe1a30ea8d72f4a748a46b7cc1ded1d5cc5932d7709c1b9f16abd606000000f37dbe0df9aac5663403000000000000000000000000000000000000000000006d0620000000000005000000615dc5725bcf05000000000000000000",true]}
--
-- $ telenet kda.ss.poolflare.com 443
-- Trying 192.99.233.13...
-- Connected to kda.ss.poolflare.com.
-- Escape character is '^]'.
-- { "id":1, "method": "mining.authorize", "params":["136029d4a40642de75bebca833ea150fc027e08fbedd29c943aab87810e77378", ""] }
-- {"id":1,"result":true,"error":null}
-- { "id":2, "method": "mining.subscribe", "params":["my-miner", null] }
-- {"id":2,"result":[null,"a796",6],"error":null}
-- {"id":null,"method":"mining.set_target","params":["0000000000400000000000000000000000000000000000000000000000000000"]}
-- {"id":null,"method":"mining.notify","params":["5d064e41","0000000000000000ab87cfaf5ccf050098d107d0470e4de368c92aa0235e190e67cc5c7cb9ee9c485439b51c566fc249030008000000608351babdabc848f90ad4891044462291d652e390a1dd6ecbb44ab733a33dce0d000000377b67dfa6df2f33f3edcb784cf3044ceee29fefaef0b90a69996c91b874c80812000000339a23f547d271eb4a68c0761b16bb5d558a1cd927a57312ece1f5ea7381f839693e8cb25bc71cc2bcb4ef84fe0119f21300b83ab768a44105010000000000002491767e21e31c67eef18cf5e8a704dc936e3a98b026f2175e672b506fa9068203000000bfde81ee849ecca0340300000000000000000000000000000000000000000000ad0620000000000005000000e243b7415ccf05000000000000000000"]}
-- {"id":null,"method":"mining.notify","params":["02040844","0000000000000000ac87cfaf5ccf050098d107d0470e4de368c92aa0235e190e67cc5c7cb9ee9c485439b51c566fc249030008000000608351babdabc848f90ad4891044462291d652e390a1dd6ecbb44ab733a33dce0d000000377b67dfa6df2f33f3edcb784cf3044ceee29fefaef0b90a69996c91b874c80812000000339a23f547d271eb4a68c0761b16bb5d558a1cd927a57312ece1f5ea7381f839693e8cb25bc71cc2bcb4ef84fe0119f21300b83ab768a44105010000000000002491767e21e31c67eef18cf5e8a704dc936e3a98b026f2175e672b506fa9068203000000bfde81ee849ecca0340300000000000000000000000000000000000000000000ad0620000000000005000000e243b7415ccf05000000000000000000"]}
-- {"id":null,"method":"mining.notify","params":["243fc4d7","0000000000000000a298c5b05ccf050098d107d0470e4de368c92aa0235e190e67cc5c7cb9ee9c485439b51c566fc249030008000000608351babdabc848f90ad4891044462291d652e390a1dd6ecbb44ab733a33dce0d000000377b67dfa6df2f33f3edcb784cf3044ceee29fefaef0b90a69996c91b874c80812000000339a23f547d271eb4a68c0761b16bb5d558a1cd927a57312ece1f5ea7381f839693e8cb25bc71cc2bcb4ef84fe0119f21300b83ab768a44105010000000000002491767e21e31c67eef18cf5e8a704dc936e3a98b026f2175e672b506fa9068203000000bfde81ee849ecca0340300000000000000000000000000000000000000000000ad0620000000000005000000e243b7415ccf05000000000000000000"]}
-- $ telnet kda.antpool.com 9026
-- Trying 172.65.236.4...
-- Connected to 707385506aed4610a1756cb8ac239332.pacloudflare.com.
-- Escape character is '^]'.
-- { "id":1, "method": "mining.authorize", "params":["136029d4a40642de75bebca833ea150fc027e08fbedd29c943aab87810e77378", ""] }
-- {"error":null,"id":1,"result":true}
-- {"id":null,"method":"mining.set_target","params":["0000000000ffff00000000000000000000000000000000000000000000000000"]}
-- {"id":null,"method":"mining.notify","params":["425","0000000000000000f8292ccbb8ed050062ee1868a76a8dcab27205756faf64f675f719bf1abe4aaf3aa016c58bca78b703000600000059fdb247d287d818e205cb180d1990de6377953f5ced9a2f3c39baed1b209dda0b0000000614c98cabe4573ea0c10d35f842b72dfb878960131660c9a9bf26755c29b9e410000000d042253d5ab72315f2aa246aa391b997a596c7da52d086c56ecb097b3b1ef8f5d21327dc3d6a411db58e439434eab8d041c36315371f7ff73b000000000000009b8122e092a3ee286b0a6e054825cd61a10567c4525358cbc9384034959cce2a010000005820b5183128e3547b29000000000000000000000000000000000000000000002efe3000000000000500000045032236b8ed05000000000000000000",true]}
-- {"id":null,"method":"mining.notify","params":["438","000000000000000050d833cbb8ed05009cc3917479d76870c546e0d88691e70d963bef9f4ea8dcc8d41d9e9b3d7767d3030004000000d22584a2605b362cf57472e920e2daef770b7cb30066e17296b934e503e121a20600000059fdb247d287d818e205cb180d1990de6377953f5ced9a2f3c39baed1b209dda07000000b8cda94e11f8f19090986db5b9ef4f42124767f7c5071bee779851feff8d1a110f8ae2b41d2314ca286bb5876ffc3f4efa322cbdbf6aaeec3b000000000000002c032268b9e594faa613290ba5c1d49b8578c3604c040cc87265540fc93d7ed90900000058adf5dd455728697a29000000000000000000000000000000000000000000002efe3000000000000500000055e0a635b8ed05000000000000000000",true]}
-- {"id":null,"method":"mining.notify","params":["451","0000000000000000ec893bcbb8ed05009cc3917479d76870c546e0d88691e70d963bef9f4ea8dcc8d41d9e9b3d7767d3030004000000d22584a2605b362cf57472e920e2daef770b7cb30066e17296b934e503e121a20600000059fdb247d287d818e205cb180d1990de6377953f5ced9a2f3c39baed1b209dda07000000b8cda94e11f8f19090986db5b9ef4f42124767f7c5071bee779851feff8d1a110f8ae2b41d2314ca286bb5876ffc3f4efa322cbdbf6aaeec3b000000000000002c032268b9e594faa613290ba5c1d49b8578c3604c040cc87265540fc93d7ed90900000058adf5dd455728697a29000000000000000000000000000000000000000000002efe3000000000000500000055e0a635b8ed05000000000000000000",true]}
-- {"id":null,"method":"mining.notify","params":["454","00000000000000006f0d43cbb8ed05005edfece3f8b166b0084433dd2aacaa849b19d7650ad24ad93816af4cebe39a180300020000005fc46a2c71c2672d14cf5a58533cbe91df0c04c17f0f97be7a1098d01daf00cf0b0000000614c98cabe4573ea0c10d35f842b72dfb878960131660c9a9bf26755c29b9e40d0000004cff0d5516544015708da9202ebc5c162290b788166fe4fa8f823444a8009a46225e14118236c18aaac6780a1b2103b04d6f6a260952ed433c000000000000000ac24e695066907ff13c861240f973c80d42c1c4b315f22aa1f4a7ec7613e8280c000000fcc8d972caa977b36329000000000000000000000000000000000000000000002efe3000000000000500000041f14f37b8ed05000000000000000000",true]}
-- {"id":null,"method":"mining.notify","params":["467","000000000000000036bd4acbb8ed0500c204c9975e77ac13fdfe913112ff79ffdc2c1f3832427a78cee92c42447e17ed030000000000a020bb7a69c62664ddbbc45faa5532a71f3dd8b82e5f3bb6ff2e969d4dc7caad07000000b8cda94e11f8f19090986db5b9ef4f42124767f7c5071bee779851feff8d1a1108000000e9c616392da600c5d9d452490a8c2b6772999b9d026f071ec970954efeb31c62329e1465ff2d31987909a9bb96ffa1417d5e6739bc641cdd3b000000000000001c46e571fd4fc984c4a1014fd0331c237e35c14632b5aefddf8f6ee6d55eb3c005000000880047099442ae1a7a29000000000000000000000000000000000000000000002efe3000000000000500000064c14236b8ed05000000000000000000",true]}
-- {"id":null,"method":"mining.notify","params":["480","0000000000000000167a52cbb8ed05009cc3917479d76870c546e0d88691e70d963bef9f4ea8dcc8d41d9e9b3d7767d3030004000000d22584a2605b362cf57472e920e2daef770b7cb30066e17296b934e503e121a20600000059fdb247d287d818e205cb180d1990de6377953f5ced9a2f3c39baed1b209dda07000000b8cda94e11f8f19090986db5b9ef4f42124767f7c5071bee779851feff8d1a110f8ae2b41d2314ca286bb5876ffc3f4efa322cbdbf6aaeec3b000000000000002c032268b9e594faa613290ba5c1d49b8578c3604c040cc87265540fc93d7ed90900000058adf5dd455728697a29000000000000000000000000000000000000000000002efe3000000000000500000055e0a635b8ed05000000000000000000",true]}
-- {"id":null,"method":"mining.notify","params":["493","0000000000000000f5255acbb8ed0500a280506fc375d85e97cc8f0a65bf75ed26271f28b1de5e6cae62cbca2cca301b030000000000ed88eb6c75faede50680a13561a41938dfaf98a36d1e1f9359a1393b532fbb870e0000007c0360ade127803445c0d7dc99949f2af9ab8add3e220857f0aeacd1b69300e8100000003a78fd1ff11f3feed926993890fa1ef9e90628d1a25b0522a3a0d5b3197300b8104a109e2ca44a6929a0d2dc6d2130162c7b048aa864491c3c0000000000000005fc75ac3d13b3d3303132a71d31729076534968b203f17fe12dff53bc5b40fd0f000000662a1f088ff36ecb6329000000000000000000000000000000000000000000002dfe30000000000005000000a2f26c36b8ed05000000000000000000",true]}
-- {"id":null,"method":"mining.notify","params":["498","0000000000000000abf25ccbb8ed05009cc3917479d76870c546e0d88691e70d963bef9f4ea8dcc8d41d9e9b3d7767d3030004000000d22584a2605b362cf57472e920e2daef770b7cb30066e17296b934e503e121a20600000059fdb247d287d818e205cb180d1990de6377953f5ced9a2f3c39baed1b209dda07000000b8cda94e11f8f19090986db5b9ef4f42124767f7c5071bee779851feff8d1a110f8ae2b41d2314ca286bb5876ffc3f4efa322cbdbf6aaeec3b000000000000002c032268b9e594faa613290ba5c1d49b8578c3604c040cc87265540fc93d7ed90900000058adf5dd455728697a29000000000000000000000000000000000000000000002efe3000000000000500000055e0a635b8ed05000000000000000000",true]}
-- { "id":2, "method": "mining.subscribe", "params":["my-miner", null] }
-- {"error":null,"id":2,"result":[[["mining.notify","40591"],["mining.set_difficulty","40592"]],"4059",6]}
-- $ telnet 34.102.20.81 1917
-- Trying 34.102.20.81...
-- Connected to 81.20.102.34.bc.googleusercontent.com.
-- Escape character is '^]'.
-- { "id":1, "method": "mining.authorize", "params":["136029d4a40642de75bebca833ea150fc027e08fbedd29c943aab87810e77378", ""] }
-- {"result":[],"error":null,"id":1}
-- { "id":2, "method": "mining.subscribe", "params":["my-miner", null] }
-- {"result":[null,"38ba",6],"error":null,"id":2}
-- {"method":"mining.set_target","params":["00000000003fffffffffffffffffffffffffffffffffffffffffffffffffffff"],"id":null}
-- {"method":"mining.notify","params":["5d4","00000000000000005e0f4de0b8ed05001d7cf6289cebdbf3b7c156796d8a2f937792ef66abd53d0b1c78d7bd00e886270300070000008cd6b42c5d486651c209224efa1953486a6641e5fa6d4b3f96212a6676cb0cab0c000000ae6e012958c9ee703c3044aafc5dfd276ead2fdedbfe328f4d65f6990ac1e8bf1100000035971bf6d4deada7c06bfdec1865b68373158a604730224ec13585746642788d6ed88c9cfbe14532b99e7d5572d7498f932bc41662f941143c000000000000005fb2d766a58fc21f61b11f9e77f0ea77200198aef10154182e4a0c0217e3ce4c020000006e1e8bbe0c3f3cd07b29000000000000000000000000000000000000000000003afe300000000000050000008176e136b8ed05000000000000000000",true],"id":null}
-- {"method":"mining.notify","params":["5d5","0000000000000000d95e93e0b8ed050006608be04391857a960a7921209660b020ad62077e5cfadff66e70c1073e9f8c0300050000005a9d21a6bb03076830021840058fc5c96737bcd514da656bece52764c24d4aa70a00000037941034111e34be30306976d63912d1ccdfc538538d2f4c38c9d5ce19ae666a0f00000072e88ea4ffb8aa55ce46f9986a970a3d50fcf7e33d796eb1123313e5716256b55a175f9c36bdfe57c938d7b8d9b8eaab53eeb5adcc94b30c3c00000000000000fea5d9ffb9ea09369257d452cc132452cbae7039c7b812ce883ccaa8feb861c400000000db2ffb09fcbdab857b29000000000000000000000000000000000000000000003afe3000000000000500000099625c36b8ed05000000000000000000",true],"id":null}
-- {"method":"mining.notify","params":["5d6","0000000000000000d907b2e0b8ed0500ae6e012958c9ee703c3044aafc5dfd276ead2fdedbfe328f4d65f6990ac1e8bf0300020000001d7cf6289cebdbf3b7c156796d8a2f937792ef66abd53d0b1c78d7bd00e886270b000000dd1edf2d73e77956d324d3a0ff82e1c086bfb26f890e9ad2c711be558e280e6b0d00000028092a25fd071dcbece38875460d222707cf0e05a2cf56bd1aaa9c72cc3bd0c2225e14118236c18aaac6780a1b2103b04d6f6a260952ed433c000000000000000bc09afd942ff2bed160e404647cc9f4b5df50e6dc63a1be4f72f02199b2597e0c0000003ce58106702771e66329000000000000000000000000000000000000000000003afe3000000000000500000041f14f37b8ed05000000000000000000",true],"id":null}
-- {"method":"mining.notify","params":["5d7","0000000000000000f9940ce1b8ed0500850e84f9f0b5eaa7d95ed01a1b70302f0b04404335376cc81cc503aeaf838e02030002000000acb32cd9f5650670a8a1e38ec653e1dbc673e56ae9ca2fb462c4b52ab4c8a61a100000003e7ebc27d4af598daefb04dedee897e0aa9eacd538db87f6ebf792b472217423120000002db1e2ec9227c7da18eec7cdb384d0b76304f2913428781385cec4c0d5b538ee7ba55a342140fa689a4266949337723eb3591ffd694b3f053c00000000000000b799e1664c78958d44503032c50b256f5c2e2754d9061a7349f057a1229398a1110000004c285a3c7d86ebfd6329000000000000000000000000000000000000000000003bfe300000000000050000007014e235b8ed05000000000000000000",true],"id":null}