https://github.com/graninas/Node is the main repository of the project (Enecuum does not support it anymore). The framework is in the frozen state but it can be resumed once you'll need it and we're agree on the form of maintainance.
Node Framework allows to build network actors and blockchain protocols, console applications, work with KV database and cryptography. Current features include:
- Framework to build stateful multithreaded applications
- Config management tools
- Testing environment
- TCP, UDP, JSON-RPC for client and server side
- Parallel network requests processing
- Concurrent state based on Software Transactional Memory
- Parallel computations and processes
- Concurrent in-memory data graph of arbitrary structure
- KV-database support
- Embeddable console client to build interactable CLIs
- Basic cryptography and random numbers generation
- Logging
- Time, file system, and other possibilities
The Node project contains:
- Haskell Node Framework
- Sample nodes with configs
- Testing environment
- Tests (functional, integration)
This tutorial contains more info.
- Source code located in ./src/
- Configs for sample nodes located in ./configs/
- Test code located in ./test/
The framework represents a set of embedded monadic languages organized hierarchically. The languages are divided to core languages responsible for common subsystems and framework languages responsible for network and actors behavior.
- HGraphL - Working with generic any structure graph (concurrently).
- StateL - Working with concurrent state variables. Represents a wrapper around native STM.
- DatabaseL - Raw KV database interface. RocksDB is the implementation currently.
- LoggerL - Logging possibilities.
- FileSystemL - Working with file system.
- RandomL - Random generation and crypto methods.
- CryptoL - Subset to work with crypto methods.
- TimeL - Getting current time.
- ControlFlowL - Controlling the flow of the evaluation.
- NodeDefinitionL - Language to define servers, APIs for node, command line methods. Provides methods for parallel process forking (forkIO essentially).
- NodeL - Allows to work with connections (TCP, UDP), create graphs and databases, evaluate scripts in core languages. Also, has methods to evaluate database and state scripts.
- Install Haskell stack
curl -sSL https://get.haskellstack.org/ | sh
- If needed, add the path to your profile
sudo nano ~/.profile
and append export PATH=$PATH:$HOME/.local/bin
at the end.
sudo apt update && sudo apt install librocksdb-dev libgd-dev libtinfo-dev -y
- Clone repo:
git clone [email protected]:graninas/Node.git && cd Node
- Build & install
stack build
- Run tests (optional)
Run all tests:
stack test
Run fast tests:
stack build --test --test-arguments "-m Fast"
Run slow and unreliable tests:
stack build --test --test-arguments "-m Slow"
enq-test-node-haskell
is a single executable for sample nodes.
-
GraphNode Transmitter
- Controllable from the Client node.
- Accepts K-blocks and microblocks.
- Works with blockchain graph and ledger.
- Answers for balance requests.
- Has a wide API, can answer to many different requests.
stack exec enq-test-node-haskell singlenode ./configs/tst_graph_node_transmitter.json
-
GraphNode Receiver
- Works with blockchain graph and ledger.
- Polls the Transmitter node to synchronize with it. Implements a basic synchronisation scenario.
stack exec enq-test-node-haskell singlenode ./configs/tst_graph_node_receiver.json
-
Gen PoW
- Controllable from the Client node.
- By the command from Client, generates KBlocks organized in a chain, but without hash complexity (does not do any mining).
- Sends KBlocks to GraphNode Transmitter.
stack exec enq-test-node-haskell singlenode ./configs/tst_gen_pow.json
-
Gen PoA
- Polls the Transmitter and generates a microblock for an empty KBlock found.
- Fills the microblock by random transactions for random wallets (5 wallets are hardcoded).
stack exec enq-test-node-haskell singlenode ./configs/tst_gen_poa.json
-
Console client
- Has console API.
- Allows to create wallets, send transactions, ask balance.
- Sends commands to nodes.
stack exec enq-test-node-haskell singlenode ./configs/tst_client.json
- Server logic: Enecuum.Samples.Assets.Nodes.TstNodes.PingPong.PingServer
- Client logic: Enecuum.Samples.Assets.Nodes.TstNodes.PingPong.PongClient
- Configs:
In this sample, two nodes interact via network sending UDP messages.
-
Ping server node
- Listens UDP port for
Ping
messages. - Sends
Pons
message back to the client. - Manages a concurrent internal state (counter of pings).
stack exec enq-test-node-haskell singlenode ./configs/tst_ping_server.json
- Listens UDP port for
-
Pong client node
- Sends
Ping
messages to the server periodically. - Accepts
Pong
messages from the server.
stack exec enq-test-node-haskell singlenode ./configs/tst_pong_client1.json
stack exec enq-test-node-haskell singlenode ./configs/tst_pong_client2.json
- Sends
-- Messages
newtype Ping = Ping Text deriving (Generic, ToJSON, FromJSON)
newtype Pong = Pong Int deriving (Generic, ToJSON, FromJSON)
-- Ping server node unique tag.
data PingServerNode = PingServerNode
-- Ping server node config.
data instance NodeConfig PingServerNode = PingServerNodeConfig
{ stopOnPing :: Int
, servingPort :: PortNumber
}
-- Ping server node definition type.
instance Node PingServerNode where
data NodeScenario PingServerNode = PingServer
getNodeScript PingServer = pingServerNode
getNodeTag _ = PingServerNode
-- Handling Ping messages.
acceptPing
:: D.StateVar D.NodeStatus
-> D.StateVar Int
-> Int
-> Ping
-> D.Connection D.Udp
-> L.NodeL ()
acceptPing status pingsCount threshold (Ping clientName) conn = do
pings <- L.atomically $ do
L.modifyVar pingsCount (+1)
L.readVar pingsCount
let done = pings + 1 >= threshold
when done $ do
L.close conn
L.writeVarIO status D.NodeFinished
L.logInfo $ "Pings threshold reached: " +|| threshold ||+ ". Finishing."
unless done $ do
L.send conn (Pong pings)
L.logInfo $ "Ping #" +|| pings ||+ " accepted from " +|| clientName ||+ "."
-- Ping server definition node.
pingServerNode :: NodeConfig PingServerNode -> L.NodeDefinitionL ()
pingServerNode cfg = do
let threshold = _stopOnPing cfg
let port = _servingPort cfg
pingsCount <- L.newVarIO 0
status <- L.newVarIO D.NodeActing
-- Starting a separate process for serving on UDP port.
L.serving D.Udp port $
L.handler $ acceptPing status pingsCount threshold
L.awaitNodeFinished' status
-- Pong client node unique tag.
data PongClientNode = PongClientNode
deriving (Show, Generic)
-- Pong client node config.
data instance NodeConfig PongClientNode = PongClientNodeConfig
{ _clientName :: Text
, _pingDelay :: Int
, _pingServerAddress :: D.Address
}
deriving (Show, Generic)
-- Pong client node definition type.
instance Node PongClientNode where
data NodeScenario PongClientNode = PongClient
deriving (Show, Generic)
getNodeScript _ = pongClientNode'
getNodeTag _ = PongClientNode
-- Accepting pong responses from the server.
acceptPong :: Pong -> connection -> L.NodeL ()
acceptPong (Pong pingsCount) _ =
L.logInfo $ "Pong accepted from server. Pings count: " <> show pingsCount
-- Sending pings to the server.
pingSending :: D.StateVar D.NodeStatus -> NodeConfig PongClientNode -> D.Connection D.Udp -> L.NodeL ()
pingSending status cfg conn = do
L.delay $ _pingDelay cfg
L.logInfo "Sending Ping to the server."
eSent <- L.send conn (Ping $ _clientName cfg)
case eSent of
Right () -> pingSending status cfg conn
Left _ -> do
L.logInfo "Server is gone."
L.close conn
L.writeVarIO status D.NodeFinished
-- Pong client definition node.
pongClientNode :: NodeConfig PongClientNode -> L.NodeDefinitionL ()
pongClientNode cfg = do
status <- L.newVarIO D.NodeActing
-- Connecting to the server.
mbConn <- L.open D.Udp (_pingServerAddress cfg) $
L.handler acceptPong
case mbConn of
Nothing -> L.logError "Ping Server not found"
Just conn -> do
-- Forking separate process of periodical pings.
L.process (pingSending status cfg conn)
-- Waiting when the node is finished.
L.awaitNodeFinished' status