Skip to content

Components

Mihai Schiopu edited this page Jan 13, 2023 · 16 revisions

The Components of the Node

The REST API

The Node offers REST endpoints which provide information about its current status and Network overview, as well as endpoints which accept requests to submit Transactions to the Network. A specific group of endpoints are used for debugging SmartContracts, and will not be enabled in production Nodes connected to the Network.

REST Endpoint Group: /node

The endpoints in group /node provide status information about the Node itself. Note that /node (the group) is not an endpoint, so GET /node will return 404 Not Found.

/node/heartbeatstatus (GET JSON)

Return the time-sorted list of Heartbeat messages received by the Node from the entire Network. This information is used to model the overall status of the Network, as reported by the Nodes themselves. The returned list can grow long with longer uptime, as the Node constantly receives Heartbeat messages (every X seconds). Code that mitigates this growth is currently being worked on.

/node/statistics (GET JSON)

Return aggregate information calculated by the Node itself with respect to: total number of Nodes, total number of Shards, current Block Nonce, current Round number, average rate of Transactions per second and others.

/node/status (GET JSON)

Return a detailed information on the status of the Node at the instant of the request. Includes information on resource usage (CPU, memory, network), contents of the Data Pools, synchronization status, its public keys for signing Blocks and Transactions, application version and other aspects.

REST Endpoint Group: /address

The endpoints in group /address provide Account information, such as current Balance and current Nonce.

/address/:address (GET JSON)

Return information about the Account identified by :address, such as its current Nonce, Balance, the SmartContract code if any and the Root Hash of the SmartContract's storage trie (if the Account is a SmartContract).

/address/:address/balance (GET JSON)

Return the current balance of the Account identified by :address. Does not provide information about the Nonce, SmartContract code or any hashes corresponding to this Account (see endpoint /address/:address for this).

REST Endpoint Group: /transaction

The endpoints in group /transaction accept requests to submit Transactions and propagate them throughout the Network for execution.

/transaction/send (POST JSON, GET JSON)

Accept request to submit a single Transaction. The request must contain the following fields:

{
  "Sender"   : // string
  "Receiver" : // string
  "Value"    : // string
  "Data"     : // string
  "Nonce"    : // uint64
  "GasPrice" : // uint64
  "GasLimit" : // uint64
  "Signature": // string
}

On success, the Transaction will be submitted and propagated throughout the Network. Returns the hash of the submitted Transaction.

/transaction/send-multiple (POST JSON, GET JSON)

Accept request to send multiple Transactions. This endpoint behaves just like /transaction/send, but it accepts a JSON array of individual requests for a single Transaction. See the endpoint /transaction/send for details. Returns the number of valid Transactions that could be submitted and propagated throughout the Network.

/transaction/:txhash (GET JSON)

Not implemented yet.

REST Endpoint Group: /vm-values

The endpoints in group /vm-values are used to interrogate deployed SmartContracts and retrieve values from them. All endpoints have the same behavior - the only difference between them is how they format the retrieved value and return it to the user. These endpoints are only enabled for development purposes, as they present an opportunity for Denial-of-Service attacks. Note that /vm-values (the group) is not an endpoint, so GET /vm-values will return 404 Not Found.

The JSON request accepted by any of the endpoints in this group has the following form:

{
  "ScAddress" : // string
  "FuncName"  : // string
  "Args"      : // string array
}

If there is a SmartContract to be found at ScAddress, it will be loaded and its function FuncName executed with the arguments Args. The function FuncName must have been written to call special API functions provided by the MultiversX Environment Interface to be able to return values to the outside world.

The value returned by calling FuncName is handled differently by each of the following endpoints.

/vm-values/hex (POST JSON, GET JSON)

Return the requested value from the SmartContract by treating it as raw bytes, then writing it in base 16 as a string.

/vm-values/string (POST JSON, GET JSON)

Return the requested value from the SmartContract by treating it as a regular string.

/vm-values/int (POST JSON, GET JSON)

Return the requested value from the SmartContract by treating it as an integer. Large integers are supported using Go's big.Int type.

/vm-values/query (POST JSON, GET JSON)

Return the entire output produced by the Virtual Machine after executing the requested SmartContract function.

libp2p

The Node relies on the libp2p library (https://github.com/libp2p/go-libp2p) for its P2P communication needs, which we wrapped in the package p2p/libp2p. The file p2p/p2p.go contains the Messenger interface, implemented in p2p/libp2p/netMessenger.go on the functionality of libp2p.

Low-level communication handled by the libp2p library is automatically encrypted and authenticated by the library itself. The only piece of information provided to libp2p to generate its internal signing private key happens in functions NetworkComponentsFactory() and createNetMessenger() in the file cmd/node/factory/structs.go.

A higher-level authentication between Nodes is being developed, which is designed to create a mapping between the internal libp2p peer ID, the Node's BLS public key (also used to sign Blocks) and the Shard of the Node.

The Messenger

The Messenger is the component that handles all the P2P communication. It is an interface, defined in the file p2p/p2p.go. The actual functionality is provided by libp2p, and the libp2p-specific implementation of the Messenger interface is found in p2p/libp2p/netMessenger.go.

The Messenger relies on libp2p's Topic functionality extensively, effectively creating multiple abstract messaging networks, isolated from one another, on top of the physical Network. Nodes will register themselves to a multitude of predefined Topics, corresponding to intra-Shard or cross-Shard transmission of various data types such as Transactions and Block Headers.

The most important part of the Messenger is its RegisterMessageProcessor() method, which the Node uses to register a handler to be invoked when a Message arrives on an individual Topic. Interceptors and Resolvers are registered using this method. For this reason, Interceptors and Resolvers must implement the p2p.MessageProcessor interface:

type MessageProcessor interface {
	ProcessReceivedMessage(message p2p.MessageP2P) error
}

The Messenger interface defines methods for both P2P gossiping and P2P direct messages, with details below. For a higher-level overview of Gossiping and Direct Messaging, see the sections Gossip and Direct Messages respectively.

Cross-shard communication is cleanly handled without extra functionality, by simply defining Topics for both intra-Shard and cross-Shard Messages. The same code handles both - Interceptors and Resolvers are Sharding-agnostic (almost; see Communicating with the Metachain), since they've been instantiated to listen to predefined Topics. There is still functionality for choosing which cross-shard peers to connect to, found in the p2p/libp2p/networksharding package.

P2P Gossiping

Nodes propagate information throughout the Network by Gossip. They rely on the Messenger's Broadcast() method for that functionality. Broadcasting, like all P2P communication, is Topic-bound.

P2P Direct Messages

Nodes use the Messenger's SendToConnectedPeer() method to send individual Direct Messages to its Peers. Direct messaging, like all P2P communication, is Topic-bound.

An example of direct messaging between two nodes is found in dataRetriever/resolvers/topicResolverSender/topicResolverSender.go, function SendOnRequestTopic(), which sends a direct message to the peers of the Node asking them for a piece of information that the Node needs. The specific type of information requested depends on the Topic it is sent on (there are Topics for Transactions, Blocks, Block Headers etc, one intra-shard and multiple cross-shard for each type). The peers which received the request also send their replies as Direct Messages.

Interceptors

An Interceptor is the component that reacts to the arrival of a Message on a specific Topic. The main role of Interceptors is to add incoming information from the Network into the Data Pool. Note that Interceptors do not react to all Messages: they only react to valid Messages and to Message that arrive on Topics of interest. This means that no Interceptor will react to invalid or uninteresting Messages - such Messages will be automatically dropped.

The main purpose of the Interceptors is to save the information received from the Peers into the Data Pool, for later consumption by the Node.

Because Messages are propagated through the Network in a peer-to-peer manner, Interceptors are invoked by the Messenger whenever a Message arrives. This means that Interceptors must implement the interface p2p.MessageProcessor. The method Interceptor.ProcessReceivedMessage() must therefore perform all the handling required.

There are two major types of Interceptors:

  • Single-data Interceptors treat the received Message as containing the binary representation of a single serialized element (see process/interceptors/singleDataInterceptor.go)
  • Multi-data Interceptors treat the received Message as containing the binary representations of a set of serialized elements of the same type (see process/interceptors/multiDataInterceptor.go)

In order to construct useful information from the received Message, each Interceptor is initialized with a Data Factory instance (see the process.InterceptedDataFactory interface). A Data Factory will instantiate a real data type from the binary Message received by the Interceptor. Their individual implementation dictates what data types they instantiate. For example, the interceptedShardHeaderDataFactory will create an InterceptedHeader instance (see process/interceptors/factory/interceptedShardHeaderDataFactory.go for the Header Data Factory, and process/block/interceptedBlocks/interceptedBlockHeader.go for the intercepted Block Header instantiated by this factory).

Apart from the Data Factory, an Interceptor is also provided with an Interceptor Processor instance (see the process.InterceptorProcessor interface), which takes further action on the data instances created by the Data Factory. In short, the Interceptor Processor must be able to Validate() the received data, and then to Save() it in the correct Data Pool (see also function processInterceptedData() in process/interceptors/common.go)

Interceptor Containers

All Interceptors instantiated by the Node are held in an Interceptors Container (see the process.InterceptorsContainer interface), which is a wrapper around a HashMap object. When the Node starts, it uses an instance implementing process.InterceptorsContainerFactory to produce an Interceptors Container fit for either a normal Shard or for the Metachain, depending on where in the Network is the Node currently assigned to do work. This leads to a call either to shard.NewInterceptorsContainerFactory() or to metachain.NewInterceptorsContainerFactory() (but not both at the same time). Both of these factories have the Create() method, which returns an Interceptors Container with the Interceptors corresponding to Shard Nodes or to Metachain Nodes.

Consensus Interceptors

TODO

Resolvers

  • Resolvers: they send a reply back directly to the requesting Peer (the PeerID is in the Message containing the initial request)

The Consensus State Machine

  • Subround architecture
  • struct spos.Subround
  • common Subround code

SubroundStart

  • SubroundStart.Job()
  • SubroundStart.Check()
  • SubroundStart.Extend()

SubroundBlock

  • SubroundBlock.Job()
  • SubroundBlock.Check()
  • SubroundBlock.Extend()

SubroundSignature

  • SubroundSignature.Job()
  • SubroundSignature.Check()
  • SubroundSignature.Extend()

SubroundEnd

  • SubroundEnd.Job()
  • SubroundEnd.Check()
  • SubroundEnd.Extend()

NodesCoordinator

The Data Pool

  • Data Pool: clarify that there's one Data Pool, with multiple sub-pools
  • Notifier (inside Data Pools): reacts to the insertion of a certain piece of information into the Data Pool
  • Notifier (inside Data Pools): if the Node was awaiting a reply with a specific info, it sets a Notifier which saves the newly inserted info into a "Requested info" Data Pool

The Data Pool is a heterogeneous container, present in each Node, which stores information that has arrived through valid Messages from the Network and awaits processing (see how Nodes communicate using Messages). Note that before having their contents stored in the Data Pool, all Messages that arrive from the Network to the Node must have already been validated by Interceptors.

A Node's Data Pool contains not only the information that is vehiculated through the Network and has somehow arrived at the Node, but it also contains information that the Node has requested from its Peers (and they responded).

Here's an example of how a transaction ends up in the Data Pool of a Node, after being propagated to the network:

  1. some user in the world wants to call a SmartContract
  2. The user generates a Transaction with a sum of ERD and containing a call to a SmartContract method (see Executing SmartContracts)
  3. the Transaction is propagated through the Network by broadcasting it to as many Nodes as possible - every Node, ideally (see the details of peer-to-peer Propagation of information)
  4. every Node that hears about this Transaction will store it in its Data Pool, after its Interceptors validate the Message that contains it

There are two types of Data Pools, which contain different types of information, depending on what kind of Node created it. These two types of Data Pools are Sharded Data Pool and Meta Data Pool. See below.

Sharded Data Pool

It is used by Nodes that are part of normal Shards (as opposed to Metashard Nodes).

type PoolsHolder interface {
  Transactions() ShardedDataCacherNotifier
  UnsignedTransactions() ShardedDataCacherNotifier
  Headers() storage.Cacher
  HeadersNonces() Uint64SyncMapCacher
  MiniBlocks() storage.Cacher
  PeerChangesBlocks() storage.Cacher
  MetaBlocks() storage.Cacher
}

Meta Data Pool

It is used by Nodes that are part of the Metashard (as opposed to Nodes in normal Shards)

  type MetaPoolsHolder interface {
    MetaChainBlocks() storage.Cacher
    MiniBlockHashes() ShardedDataCacherNotifier
    ShardHeaders() storage.Cacher
    HeadersNonces() Uint64SyncMapCacher
  }