mirror of https://github.com/voidlizard/hbs2
822 lines
26 KiB
Haskell
822 lines
26 KiB
Haskell
{-# Language TemplateHaskell #-}
|
||
{-# Language UndecidableInstances #-}
|
||
{-# Language AllowAmbiguousTypes #-}
|
||
module Main where
|
||
|
||
import HBS2.Prelude
|
||
import HBS2.Defaults
|
||
import HBS2.Merkle
|
||
import HBS2.Hash
|
||
import HBS2.Clock
|
||
import HBS2.Base58
|
||
import HBS2.OrDie
|
||
import HBS2.Data.Types.Refs
|
||
import HBS2.Net.Proto.Types
|
||
import HBS2.Actors.Peer
|
||
import HBS2.Net.Proto.RefChan
|
||
import HBS2.Net.Proto.AnyRef
|
||
import HBS2.Data.Types.SignedBox
|
||
import HBS2.Net.Messaging.Unix
|
||
import HBS2.Net.Proto.Definition
|
||
import HBS2.Data.Bundle
|
||
import HBS2.Net.Auth.Credentials
|
||
import HBS2.Data.Detect
|
||
import HBS2.Actors.Peer.Types()
|
||
|
||
import HBS2.Storage.Simple
|
||
|
||
import HBS2.System.Logger.Simple
|
||
|
||
import QBLF.Proto
|
||
|
||
import Demo.QBLF.Transactions
|
||
import Data.Config.Suckless
|
||
import Data.Config.Suckless.KeyValue
|
||
|
||
import Data.Ord
|
||
import Control.Monad.Trans.Maybe
|
||
import Codec.Serialise
|
||
import Control.Monad.Reader
|
||
import Data.ByteString(ByteString)
|
||
import Data.ByteString.Char8 qualified as BS
|
||
import Data.ByteString.Lazy qualified as LBS
|
||
import Data.Functor
|
||
import Data.List qualified as List
|
||
import Lens.Micro.Platform hiding ((.=))
|
||
import Options.Applicative hiding (info)
|
||
import Options.Applicative qualified as O
|
||
import System.Directory
|
||
import Data.HashSet (HashSet)
|
||
import Data.HashSet qualified as HashSet
|
||
import Data.HashMap.Strict (HashMap)
|
||
import Data.HashMap.Strict qualified as HashMap
|
||
import Data.Maybe
|
||
import Data.Word
|
||
import System.Random
|
||
import UnliftIO
|
||
|
||
import Data.Time.Clock.POSIX (getPOSIXTime)
|
||
|
||
import Data.Aeson hiding (json)
|
||
import Web.Scotty hiding (request,header)
|
||
import Web.Scotty qualified as Scotty
|
||
import Network.HTTP.Types.Status
|
||
import Data.Monoid (mconcat)
|
||
import Data.Cache (Cache)
|
||
import Data.Cache qualified as Cache
|
||
|
||
import Control.Monad.Except
|
||
|
||
|
||
import Streaming.Prelude qualified as S
|
||
|
||
{- HLINT ignore "Use newtype instead of data" -}
|
||
|
||
-- TODO: config
|
||
-- сделать конфиг, а то слишком много уже параметров в CLI
|
||
|
||
data HttpPortOpt
|
||
data RefChanOpt
|
||
data SocketOpt
|
||
data ActorOpt
|
||
data DefStateOpt
|
||
data StateRefOpt
|
||
|
||
data QBLFRefKey
|
||
type MyRefKey = AnyRefKey QBLFRefKey HBS2Basic
|
||
|
||
instance Monad m => HasCfgKey HttpPortOpt (Maybe Int) m where
|
||
key = "http"
|
||
|
||
|
||
instance {-# OVERLAPPING #-} (HasConf m, HasCfgKey HttpPortOpt (Maybe Int) m) => HasCfgValue HttpPortOpt (Maybe Int) m where
|
||
cfgValue = val <$> getConf
|
||
where
|
||
val syn = lastMay [ fromIntegral e
|
||
| ListVal @C (Key s [LitIntVal e]) <- syn, s == key @HttpPortOpt @(Maybe Int) @m
|
||
]
|
||
|
||
instance Monad m => HasCfgKey RefChanOpt (Maybe String) m where
|
||
key = "refchan"
|
||
|
||
instance Monad m => HasCfgKey SocketOpt (Maybe String) m where
|
||
key = "socket"
|
||
|
||
instance Monad m => HasCfgKey ActorOpt (Maybe String) m where
|
||
key = "actor"
|
||
|
||
instance Monad m => HasCfgKey DefStateOpt (Maybe String) m where
|
||
key = "default-state"
|
||
|
||
instance Monad m => HasCfgKey StateRefOpt (Maybe String) m where
|
||
key = "state-ref"
|
||
|
||
class ToBalance e tx where
|
||
toBalance :: tx -> [(Account e, Amount)]
|
||
|
||
tracePrefix :: SetLoggerEntry
|
||
tracePrefix = toStderr . logPrefix "[trace] "
|
||
|
||
debugPrefix :: SetLoggerEntry
|
||
debugPrefix = toStderr . logPrefix "[debug] "
|
||
|
||
errorPrefix :: SetLoggerEntry
|
||
errorPrefix = toStderr . logPrefix "[error] "
|
||
|
||
warnPrefix :: SetLoggerEntry
|
||
warnPrefix = toStderr . logPrefix "[warn] "
|
||
|
||
noticePrefix :: SetLoggerEntry
|
||
noticePrefix = toStderr . logPrefix "[notice] "
|
||
|
||
infoPrefix :: SetLoggerEntry
|
||
infoPrefix = toStdout . logPrefix ""
|
||
|
||
silently :: MonadIO m => m a -> m ()
|
||
silently m = do
|
||
setLoggingOff @DEBUG
|
||
setLoggingOff @INFO
|
||
setLoggingOff @ERROR
|
||
setLoggingOff @WARN
|
||
setLoggingOff @NOTICE
|
||
void m
|
||
|
||
withLogging :: MonadIO m => m a -> m ()
|
||
withLogging m = do
|
||
|
||
-- setLogging @TRACE tracePrefix
|
||
setLogging @DEBUG debugPrefix
|
||
setLogging @INFO infoPrefix
|
||
setLogging @ERROR errorPrefix
|
||
setLogging @WARN warnPrefix
|
||
setLogging @NOTICE noticePrefix
|
||
|
||
m
|
||
|
||
setLoggingOff @DEBUG
|
||
setLoggingOff @INFO
|
||
setLoggingOff @ERROR
|
||
setLoggingOff @WARN
|
||
setLoggingOff @NOTICE
|
||
|
||
data MyEnv =
|
||
MyEnv
|
||
{ mySelf :: Peer UNIX
|
||
, myFab :: Fabriq UNIX
|
||
, myChan :: RefChanId UNIX
|
||
, myRef :: MyRefKey
|
||
, mySto :: AnyStorage
|
||
, myCred :: PeerCredentials HBS2Basic
|
||
, myHttpPort :: Int
|
||
, myFetch :: Cache HashRef ()
|
||
}
|
||
|
||
newtype App m a = App { fromApp :: ReaderT MyEnv m a }
|
||
deriving newtype ( Functor
|
||
, Applicative
|
||
, Monad
|
||
, MonadIO
|
||
, MonadUnliftIO
|
||
, MonadReader MyEnv
|
||
, MonadTrans
|
||
)
|
||
|
||
runApp :: (MonadIO m, PeerMessaging UNIX) => MyEnv -> App m a -> m a
|
||
runApp env m = runReaderT (fromApp m) env
|
||
|
||
instance Monad m => HasFabriq UNIX (App m) where
|
||
getFabriq = asks myFab
|
||
|
||
instance Monad m => HasOwnPeer UNIX (App m) where
|
||
ownPeer = asks mySelf
|
||
|
||
instance Monad m => HasStorage (App m) where
|
||
getStorage = asks mySto
|
||
|
||
data ConsensusQBLF
|
||
|
||
data StateQBLF = StateQBLF { fromStateQBLF :: HashRef }
|
||
|
||
data MyError =
|
||
DeserializationError | SignatureError | TxUnsupported | SomeOtherError
|
||
deriving stock (Eq,Ord,Show)
|
||
|
||
check :: MonadIO m => MyError -> Either e a -> ExceptT MyError m a
|
||
check w = \case
|
||
Right x -> ExceptT $ pure (Right x)
|
||
Left{} -> ExceptT $ pure (Left w)
|
||
|
||
fiasco :: MonadIO m => MyError -> ExceptT MyError m a
|
||
fiasco x = ExceptT $ pure $ Left x
|
||
|
||
ok :: MonadIO m => a -> ExceptT MyError m a
|
||
ok x = ExceptT $ pure $ Right x
|
||
|
||
|
||
type ForConsensus m = (MonadIO m, Serialise (QBLFMessage ConsensusQBLF))
|
||
|
||
instance Serialise (QBLFMerge ConsensusQBLF)
|
||
instance Serialise (QBLFMessage ConsensusQBLF)
|
||
instance Serialise (QBLFAnnounce ConsensusQBLF)
|
||
instance Serialise (QBLFCommit ConsensusQBLF)
|
||
|
||
instance Monad m => HasTimeLimits UNIX (RefChanNotify UNIX) (App m) where
|
||
tryLockForPeriod _ _ = pure True
|
||
|
||
instance (ForConsensus m, MonadUnliftIO m) => IsQBLF ConsensusQBLF (App m) where
|
||
type QBLFActor ConsensusQBLF = Actor L4Proto
|
||
type QBLFTransaction ConsensusQBLF = QBLFDemoToken L4Proto
|
||
type QBLFState ConsensusQBLF = DAppState
|
||
|
||
qblfMoveForward _ s1 = do
|
||
env <- ask
|
||
fetchMissed env s1
|
||
pure True
|
||
|
||
qblfNewState (DAppState h0) txs = do
|
||
sto <- asks mySto
|
||
chan <- asks myChan
|
||
self <- asks mySelf
|
||
creds <- asks myCred
|
||
let sk = view peerSignSk creds
|
||
let pk = view peerSignPk creds
|
||
|
||
-- основная проблема в том, что мы пересортировываем весь state
|
||
-- однако, если считать его уже отсортированным, то, может быть,
|
||
-- все будет не так уж плохо.
|
||
-- так-то мы можем вообще его на диске держать
|
||
|
||
root <- if List.null txs then do
|
||
pure h0
|
||
else do
|
||
hashes <- liftIO $ mapM (putBlock sto . serialise) txs <&> catMaybes
|
||
|
||
current <- readLog (getBlock sto) h0
|
||
|
||
let new = HashSet.fromList ( current <> fmap HashRef hashes )
|
||
|
||
let pt = toPTree (MaxSize 256) (MaxNum 256) (HashSet.toList new)
|
||
|
||
-- пробуем разослать бандлы с транзакциями
|
||
runMaybeT do
|
||
ref <- MaybeT $ createBundle sto (fmap HashRef hashes)
|
||
let refval = makeBundleRefValue @L4Proto pk sk (BundleRefSimple ref)
|
||
r <- MaybeT $ liftIO $ putBlock sto (serialise refval)
|
||
lift $ request self (ActionRequest @UNIX chan (RefChanAnnounceBlock (HashRef r)))
|
||
|
||
r <- makeMerkle 0 pt $ \(hx,_,bs) -> do
|
||
th <- liftIO (enqueueBlock sto bs)
|
||
debug $ "WRITE TX" <+> pretty hx
|
||
|
||
request self (ActionRequest @UNIX chan (RefChanAnnounceBlock (HashRef r)))
|
||
|
||
pure (HashRef r)
|
||
|
||
debug $ "PROPOSED NEW STATE:" <+> pretty root
|
||
pure $ DAppState root
|
||
|
||
qblfCommit s0 s1 = do
|
||
debug $ "COMMIT:" <+> pretty s0 <+> pretty s1
|
||
sto <- asks mySto
|
||
chan <- asks myChan
|
||
ref <- asks myRef
|
||
|
||
debug $ "UPDATING REF" <+> pretty ref <+> pretty s1
|
||
liftIO $ updateRef sto ref (fromHashRef (fromDAppState s1))
|
||
pure ()
|
||
|
||
qblfBroadCast msg = do
|
||
self <- asks mySelf
|
||
creds <- asks myCred
|
||
chan <- asks myChan
|
||
|
||
let sk = view peerSignSk creds
|
||
let pk = view peerSignPk creds
|
||
nonce <- randomIO @Word64 <&> serialise <&> LBS.toStrict
|
||
let box = makeSignedBox @UNIX pk sk (LBS.toStrict (serialise msg) <> nonce)
|
||
let notify = Notify @UNIX chan box
|
||
request self notify
|
||
|
||
case msg of
|
||
QBLFMsgAnn _ (QBLFAnnounce _ _) -> do
|
||
-- TODO: maybe-announce-new-state-here
|
||
pure ()
|
||
|
||
_ -> none
|
||
|
||
-- TODO: optimize-qblf-merge
|
||
-- будет нормально работать до десятков/сотен тысяч транз,
|
||
-- а потом помрёт.
|
||
-- варианты:
|
||
-- 1. перенести логику в БД
|
||
-- 2. кэшировать всё, что можно
|
||
qblfMerge s0 s1 = do
|
||
chan <- asks myChan
|
||
self <- asks mySelf
|
||
|
||
creds <- asks myCred
|
||
let sk = view peerSignSk creds
|
||
let pk = view peerSignPk creds
|
||
|
||
debug $ "MERGE. Proposed state:" <+> pretty s1
|
||
|
||
sto <- asks mySto
|
||
let readFn = liftIO . getBlock sto
|
||
|
||
tx1 <- mapM (readLog readFn) (fmap fromDAppState s1) <&> mconcat
|
||
tx0 <- readLog readFn (fromDAppState s0) <&> HashSet.fromList
|
||
|
||
let txNew = HashSet.fromList tx1 `HashSet.difference` tx0
|
||
|
||
if List.null txNew then do
|
||
pure s0
|
||
else do
|
||
debug $ "READ TXS" <+> pretty s1 <+> pretty (length tx1)
|
||
|
||
r <- forM tx1 $ \t -> runMaybeT do
|
||
|
||
-- игнорируем ранее добавленные транзакции
|
||
guard (not (HashSet.member t tx0))
|
||
|
||
bs <- MaybeT $ liftIO $ getBlock sto (fromHashRef t)
|
||
|
||
tx <- MaybeT $ pure $ deserialiseOrFail @(QBLFDemoToken L4Proto) bs & either (const Nothing) Just
|
||
|
||
case tx of
|
||
Emit box -> do
|
||
(pk, e@(EmitTx a q _)) <- MaybeT $ pure $ unboxSignedBox0 @(EmitTx L4Proto) box
|
||
guard ( chan == pk )
|
||
debug $ "VALID EMIT TRANSACTION" <+> pretty t <+> pretty (AsBase58 a) <+> pretty q
|
||
pure ([(t,e)], mempty)
|
||
|
||
(Move box) -> do
|
||
(_, m@(MoveTx _ _ qty _)) <- MaybeT $ pure $ unboxSignedBox0 @(MoveTx L4Proto) box
|
||
|
||
guard (qty > 0)
|
||
debug $ "MOVE TRANSACTION" <+> pretty t
|
||
pure (mempty, [(t,m)])
|
||
|
||
let parsed = catMaybes r
|
||
|
||
let emits = foldMap (view _1) parsed
|
||
|
||
let moves = foldMap (view _2) parsed & List.sortOn fst
|
||
|
||
bal0 <- balances (fromDAppState s0)
|
||
|
||
-- баланс с учётом новых emit
|
||
let balE = foldMap (toBalance @L4Proto . snd) emits
|
||
& HashMap.fromListWith (+)
|
||
& HashMap.unionWith (+) bal0
|
||
|
||
let moves' = updBalances @L4Proto balE moves
|
||
|
||
let merged = fmap fst emits <> fmap fst moves'
|
||
|
||
let pt = toPTree (MaxSize 256) (MaxNum 256) (HashSet.toList (tx0 <> HashSet.fromList merged))
|
||
|
||
root <- makeMerkle 0 pt $ \(_,_,bs) -> do
|
||
void $ liftIO (putBlock sto bs)
|
||
|
||
let new = DAppState (HashRef root)
|
||
|
||
-- FIXME: garbage-collect-discarded-states
|
||
|
||
async $ void $ balances (fromDAppState new)
|
||
debug $ "MERGED" <+> pretty new
|
||
|
||
pure new
|
||
|
||
|
||
instance (HasConf (ReaderT Config IO)) where
|
||
getConf = ask
|
||
|
||
instance HasStorage (ReaderT AnyStorage IO) where
|
||
getStorage = ask
|
||
|
||
|
||
|
||
instance ToBalance e (EmitTx e) where
|
||
toBalance (EmitTx a qty _) = [(a, qty)]
|
||
|
||
instance ToBalance e (MoveTx e) where
|
||
toBalance (MoveTx a1 a2 qty _) = [(a1, -qty), (a2, qty)]
|
||
|
||
balances :: forall e s m . ( e ~ L4Proto
|
||
, MonadIO m
|
||
, HasStorage m
|
||
-- , FromStringMaybe (PubKey 'Sign s)
|
||
, s ~ Encryption e
|
||
, ToBalance L4Proto (EmitTx L4Proto)
|
||
, ToBalance L4Proto (MoveTx L4Proto)
|
||
)
|
||
=> HashRef
|
||
-> m (HashMap (Account e) Amount)
|
||
|
||
balances root = do
|
||
sto <- getStorage
|
||
|
||
let pk = SomeRefKey (HashRef "6ChGmfYkwM6646oKkj8r8MAjdViTsdtZSi6tgqk3tbh", root)
|
||
|
||
cached <- runMaybeT do
|
||
rval <- MaybeT $ liftIO $ getRef sto pk
|
||
val <- MaybeT $ liftIO $ getBlock sto rval
|
||
MaybeT $ deserialiseOrFail @(HashMap (Account e) Amount) val
|
||
& either (const $ pure Nothing) (pure . Just)
|
||
|
||
case cached of
|
||
Just bal -> pure bal
|
||
Nothing -> do
|
||
|
||
txs <- readLog (liftIO . getBlock sto) root
|
||
|
||
r <- forM txs $ \h -> runMaybeT do
|
||
blk <- MaybeT $ liftIO $ getBlock sto (fromHashRef h)
|
||
tx <- MaybeT $ pure $ deserialiseOrFail @(QBLFDemoToken L4Proto) blk & either (const Nothing) Just
|
||
|
||
case tx of
|
||
Emit box -> do
|
||
(_, emit) <- MaybeT $ pure $ unboxSignedBox0 @(EmitTx L4Proto) box
|
||
pure $ toBalance @e emit
|
||
|
||
Move box -> do
|
||
(_, move) <- MaybeT $ pure $ unboxSignedBox0 @(MoveTx L4Proto) box
|
||
pure $ toBalance @e move
|
||
|
||
let val = catMaybes r & mconcat & HashMap.fromListWith (+)
|
||
|
||
runMaybeT do
|
||
checkComplete sto root >>= guard
|
||
rv <- MaybeT $ liftIO $ putBlock sto (serialise val)
|
||
liftIO $ updateRef sto pk rv
|
||
|
||
pure val
|
||
|
||
|
||
-- TODO: optimize-upd-balances
|
||
-- можно сгруппировать по аккаунтам
|
||
-- и проверять только те транзакции, которые относятся
|
||
-- к связанной (транзакциями) группе аккаунтов.
|
||
-- то есть, разбить на кластеры, у которых отсутствуют пересечения по
|
||
-- аккаунтам и проверять независимо и параллельно, например
|
||
-- причем, прямо этой функцией
|
||
--
|
||
-- updBalances :: HashMap (Account L4Proto) Amount
|
||
-- -> [(tx, b)]
|
||
-- -> [(tx, b)]
|
||
|
||
updBalances :: forall e a tx . (ForRefChans e, ToBalance e tx)
|
||
=> HashMap (Account e) Amount
|
||
-> [(a, tx)]
|
||
-> [(a, tx)]
|
||
|
||
updBalances = go
|
||
where
|
||
go bal [] = empty
|
||
|
||
go bal (t:rest) =
|
||
|
||
if good then
|
||
t : go nb rest
|
||
else
|
||
go bal rest
|
||
|
||
where
|
||
nb = HashMap.unionWith (+) bal (HashMap.fromList (toBalance @e (snd t)))
|
||
good = HashMap.filter (<0) nb & HashMap.null
|
||
|
||
|
||
fetchMissed :: forall e w m . ( MonadIO m
|
||
, Request e (RefChanNotify e) m
|
||
, e ~ UNIX
|
||
, w ~ ConsensusQBLF
|
||
)
|
||
=> MyEnv
|
||
-> QBLFState w
|
||
-> m ()
|
||
|
||
fetchMissed env s = do
|
||
let tube = mySelf env
|
||
let chan = myChan env
|
||
let cache = myFetch env
|
||
let sto = mySto env
|
||
|
||
let href = fromDAppState s
|
||
|
||
here <- liftIO $ hasBlock sto (fromHashRef href) <&> isJust
|
||
wip <- liftIO $ Cache.lookup cache href <&> isJust
|
||
|
||
when here do
|
||
liftIO $ Cache.delete cache href
|
||
|
||
unless (here || wip) do
|
||
debug $ "We might be need to fetch" <+> pretty s
|
||
liftIO $ Cache.insert cache href ()
|
||
request @UNIX tube (ActionRequest @UNIX chan (RefChanFetch (fromDAppState s)))
|
||
|
||
runMe :: ForConsensus IO => Config -> IO ()
|
||
runMe conf = withLogging $ flip runReaderT conf do
|
||
|
||
debug $ "runMe" <+> pretty conf
|
||
|
||
kr <- cfgValue @ActorOpt @(Maybe String) `orDie` "actor's key not set"
|
||
chan' <- cfgValue @RefChanOpt @(Maybe String) `orDie` "refchan not set"
|
||
sa <- cfgValue @SocketOpt @(Maybe String) `orDie` "socket not set"
|
||
pno <- cfgValue @HttpPortOpt @(Maybe Int) <&> fromMaybe 3011
|
||
ds <- cfgValue @DefStateOpt @(Maybe String)
|
||
|
||
ref <- ( cfgValue @StateRefOpt @(Maybe String)
|
||
<&> maybe Nothing fromStringMay
|
||
) `orDie` "state-ref not set"
|
||
|
||
sc <- liftIO $ BS.readFile kr
|
||
creds <- pure (parseCredentials @HBS2Basic (AsCredFile sc)) `orDie` "bad keyring file"
|
||
|
||
chan <- pure (fromStringMay @(RefChanId L4Proto) chan') `orDie` "invalid REFCHAN"
|
||
|
||
here <- liftIO $ doesFileExist sa
|
||
|
||
when here do
|
||
liftIO $ removeFile sa
|
||
|
||
server <- newMessagingUnix True 1.0 sa
|
||
|
||
abus <- async $ runMessagingUnix server
|
||
|
||
let tube = fromString sa
|
||
|
||
-- FIXME: fix-default-storage
|
||
xdg <- liftIO $ getXdgDirectory XdgData defStorePath <&> fromString
|
||
sto' <- simpleStorageInit @HbSync [StoragePrefix xdg]
|
||
|
||
let sto = AnyStorage sto'
|
||
sw <- liftIO $ replicateM 4 $ async $ simpleStorageWorker sto'
|
||
|
||
-- FIXME: fix-hardcoded-timeout
|
||
fetches <- liftIO $ Cache.newCache (Just (toTimeSpec (TimeoutSec 30)))
|
||
|
||
let myEnv = MyEnv tube
|
||
(Fabriq server)
|
||
chan
|
||
ref
|
||
sto
|
||
creds
|
||
pno
|
||
fetches
|
||
|
||
let dss = ds >>= fromStringMay
|
||
|
||
s0 <- readOrCreateStateRef dss sto ref
|
||
|
||
debug $ "STATE0:" <+> pretty s0
|
||
|
||
-- получить голову
|
||
-- из головы получить акторов
|
||
|
||
headBlk <- getRefChanHead @L4Proto sto (RefChanHeadKey chan) `orDie` "can't read head block"
|
||
|
||
let self = view peerSignPk creds & Actor @L4Proto
|
||
|
||
let actors = view refChanHeadAuthors headBlk
|
||
& HashSet.toList
|
||
& fmap (Actor @L4Proto)
|
||
|
||
runApp myEnv do
|
||
|
||
-- FIXME: timeout-hardcode
|
||
let w = realToFrac 5
|
||
|
||
-- FIXME: use-actors-asap
|
||
qblf <- qblfInit @ConsensusQBLF self actors (DAppState (HashRef s0)) w
|
||
|
||
consensus <- async do
|
||
pause @'Seconds 0.5
|
||
qblfRun qblf
|
||
|
||
|
||
-- FIXME: web-port-to-config
|
||
web <- async $ liftIO $ scotty (fromIntegral (myHttpPort myEnv)) $ do
|
||
post "/tx" $ do
|
||
|
||
r <- runExceptT do
|
||
|
||
bin <- lift body
|
||
let hBin = hashObject @HbSync bin
|
||
|
||
debug $ "GOT TX" <+> pretty hBin
|
||
|
||
tok <- check DeserializationError =<< pure (deserialiseOrFail @(QBLFDemoToken L4Proto) bin)
|
||
|
||
tx <- case tok of
|
||
(Emit box) -> do
|
||
(sign, tx) <- maybe (ExceptT $ pure $ Left SignatureError) pure $ unboxSignedBox0 box
|
||
|
||
if sign == chan then
|
||
pure hBin
|
||
else
|
||
fiasco SignatureError
|
||
|
||
(Move box) -> do
|
||
(sign, tx) <- maybe (ExceptT $ pure $ Left SignatureError) pure $ unboxSignedBox0 box
|
||
pure hBin
|
||
|
||
qblfEnqueue qblf tok
|
||
pure hBin
|
||
|
||
case r of
|
||
Left SignatureError -> do
|
||
err $ viaShow SignatureError
|
||
status status401
|
||
|
||
Left e -> do
|
||
err $ viaShow e
|
||
status status400
|
||
|
||
Right tx -> do
|
||
debug $ "TX ENQUEUED OK" <+> pretty tx
|
||
status status200
|
||
|
||
link web
|
||
|
||
runProto $ List.singleton $ makeResponse (myProto myEnv qblf chan)
|
||
|
||
void $ waitAnyCatchCancel $ [abus] <> sw
|
||
|
||
where
|
||
|
||
myProto :: forall e m . ( MonadIO m
|
||
, Request e (RefChanNotify e) m
|
||
, e ~ UNIX
|
||
)
|
||
=> MyEnv
|
||
-> QBLF ConsensusQBLF
|
||
-> RefChanId e
|
||
-> RefChanNotify e
|
||
-> m ()
|
||
|
||
myProto _ qblf _ (ActionRequest{}) = do
|
||
pure ()
|
||
|
||
myProto env qblf chan (Notify _ msg) = do
|
||
|
||
let sto = mySto env
|
||
let tube = mySelf env
|
||
|
||
let coco = hashObject @HbSync $ serialise msg
|
||
void $ runMaybeT do
|
||
(_, wrapped) <- MaybeT $ pure $ unboxSignedBox0 @ByteString @UNIX msg
|
||
qbmess <- MaybeT $ pure $ deserialiseOrFail @(QBLFMessage ConsensusQBLF) (LBS.fromStrict wrapped)
|
||
& either (const Nothing) Just
|
||
|
||
states <- case qbmess of
|
||
QBLFMsgAnn _ (QBLFAnnounce s0 s1) -> do
|
||
pure [s0, s1]
|
||
|
||
QBLFMsgHeartBeat _ _ s0 _-> do
|
||
pure [s0]
|
||
|
||
_ -> do
|
||
pure mempty
|
||
|
||
-- FIXME: full-download-guarantee
|
||
lift $ forM_ states (fetchMissed env)
|
||
|
||
qblfAcceptMessage qblf qbmess
|
||
-- debug $ "RefChanQBLFMain(3)" <+> "got message" <+> pretty (AsBase58 chan) <+> pretty coco
|
||
|
||
readOrCreateStateRef mbDs sto ref = do
|
||
debug $ "MyRef:" <+> pretty (hashObject @HbSync ref)
|
||
fix \spin -> do
|
||
mbref <- liftIO $ getRef @_ @HbSync sto ref
|
||
case mbref of
|
||
Nothing -> do
|
||
debug "STATE is empty"
|
||
maybe1 mbDs none $ \ds -> do
|
||
debug $ "UPDATE REF" <+> pretty (hashObject @HbSync ref) <+> pretty (HashRef ds)
|
||
liftIO $ updateRef sto ref ds
|
||
|
||
pause @'Seconds 0.25
|
||
|
||
spin
|
||
|
||
Just val -> do
|
||
pure val
|
||
|
||
type Config = [Syntax MegaParsec]
|
||
|
||
main :: IO ()
|
||
main = join . customExecParser (prefs showHelpOnError) $
|
||
O.info (helper <*> globalOptions)
|
||
( fullDesc
|
||
<> header "refchan-qblf-worker"
|
||
<> progDesc "for test and demo purposed"
|
||
)
|
||
where
|
||
|
||
globalOptions = applyConfig <$> commonOpts <*> cli
|
||
|
||
applyConfig :: Maybe FilePath -> (Config -> IO ()) -> IO ()
|
||
applyConfig config m = do
|
||
maybe1 config (m mempty) $ \conf -> do
|
||
top <- readFile conf <&> parseTop <&> either (pure mempty) id
|
||
m top
|
||
|
||
commonOpts = optional $ strOption (long "config" <> short 'c' <> help "Config file")
|
||
|
||
cli = hsubparser ( command "run" (O.info pRun (progDesc "run qblf servant" ) )
|
||
<> command "gen" (O.info pGen (progDesc "generate transcation") )
|
||
<> command "post" (O.info pPostTx (progDesc "post transaction") )
|
||
<> command "check" (O.info pCheckTx (progDesc "check transaction") )
|
||
<> command "balances" (O.info pBalances (progDesc "show balances") )
|
||
)
|
||
|
||
pRun = do
|
||
pure runMe
|
||
|
||
pGen = hsubparser
|
||
( command "tx-emit" ( O.info pGenEmit (progDesc "generate emit") )
|
||
<> command "tx-move" ( O.info pGenMove (progDesc "generate move") )
|
||
)
|
||
|
||
pGenEmit = do
|
||
kr <- strOption ( long "keyring" <> short 'k' <> help "keyring file" )
|
||
amnt <- option @Amount auto ( long "amount" <> short 'n' <> help "amount" )
|
||
dest <- strArgument ( metavar "ADDRESS" )
|
||
pure $ const $ silently do
|
||
sc <- BS.readFile kr
|
||
creds <- pure (parseCredentials @HBS2Basic (AsCredFile sc)) `orDie` "bad keyring file"
|
||
let pk = view peerSignPk creds
|
||
let sk = view peerSignSk creds
|
||
acc <- pure (fromStringMay @(RefChanId L4Proto) dest) `orDie` "bad account address"
|
||
tx <- makeEmitTx @L4Proto pk sk acc amnt
|
||
LBS.putStr $ serialise tx
|
||
|
||
pGenMove = do
|
||
kr <- strOption ( long "wallet" <> short 'w' <> help "wallet (keyring) file" )
|
||
amnt <- option @Amount auto ( long "amount" <> short 'n' <> help "amount" )
|
||
dest <- strArgument ( metavar "ADDRESS" )
|
||
pure $ const $ silently do
|
||
sc <- BS.readFile kr
|
||
creds <- pure (parseCredentials @HBS2Basic (AsCredFile sc)) `orDie` "bad keyring file"
|
||
let pk = view peerSignPk creds
|
||
let sk = view peerSignSk creds
|
||
acc <- pure (fromStringMay @(RefChanId L4Proto) dest) `orDie` "bad account address"
|
||
tx <- makeMoveTx @L4Proto pk sk acc amnt
|
||
LBS.putStr $ serialise tx
|
||
|
||
pCheckTx = do
|
||
kr <- strOption ( long "keyring" <> short 'k' <> help "keyring file" )
|
||
pure $ const do
|
||
sc <- BS.readFile kr
|
||
creds <- pure (parseCredentials @HBS2Basic (AsCredFile sc)) `orDie` "bad keyring file"
|
||
let pk = view peerSignPk creds
|
||
let sk = view peerSignSk creds
|
||
|
||
tx <- LBS.getContents <&> deserialise @(QBLFDemoToken L4Proto)
|
||
|
||
case tx of
|
||
Emit box -> do
|
||
void $ pure (unboxSignedBox0 @(EmitTx L4Proto) @L4Proto box) `orDie` "bad emit tx"
|
||
|
||
Move box -> do
|
||
void $ pure (unboxSignedBox0 @(MoveTx L4Proto) @L4Proto box) `orDie` "bad move tx"
|
||
|
||
pure ()
|
||
|
||
pPostTx = pure $ const do
|
||
error "not supported anymore / TODO via http"
|
||
-- rc <- strArgument ( metavar "REFCHAN" )
|
||
-- sa <- strArgument ( metavar "UNIX-SOCKET" ) <&> fromString
|
||
-- pure $ withLogging do
|
||
-- rchan <- pure (fromStringMay @(RefChanId L4Proto) rc) `orDie` "bad refchan"
|
||
-- print "JOPA"
|
||
-- -- FIXME: wrap-client-boilerplate
|
||
-- inbox <- newMessagingUnix False 1.0 sa
|
||
-- wInbox <- async $ runMessagingUnix inbox
|
||
-- let env = MyEnv (fromString sa) (Fabriq inbox) rchan
|
||
-- msg <- (LBS.getContents <&> deserialiseOrFail) `orDie` "transaction decode error"
|
||
-- runApp env do
|
||
-- request (mySelf env) (msg :: QBLFDemoTran UNIX)
|
||
-- pause @'Seconds 0.1
|
||
-- cancel wInbox
|
||
|
||
pBalances = do
|
||
state <- strArgument ( metavar "STATE" )
|
||
pure $ const $ withLogging do
|
||
|
||
xdg <- liftIO $ getXdgDirectory XdgData defStorePath <&> fromString
|
||
sto' <- simpleStorageInit @HbSync [StoragePrefix xdg]
|
||
|
||
let sto = AnyStorage sto'
|
||
sw <- liftIO $ replicateM 4 $ async $ simpleStorageWorker sto'
|
||
|
||
root <- pure (fromStringMay @HashRef state) `orDie` "Bad STATE reference"
|
||
|
||
flip runReaderT sto $ do
|
||
debug $ "calculating balances for" <+> pretty root
|
||
bal <- balances root
|
||
|
||
forM_ (HashMap.toList bal) $ \(acc, qty) -> do
|
||
liftIO $ print $ pretty (AsBase58 acc) <+> pretty qty
|
||
|