Skip to content

Latest commit

 

History

History
201 lines (162 loc) · 9.53 KB

File metadata and controls

201 lines (162 loc) · 9.53 KB

the API infrastructure Brenthy provides to blockchain types To learn how BrenthyAPI is used by WalytisAPI to allow applications to interact with Walytis, see WalytisAPI.

BrenthyAPI

BrenthyAPI is the infrastructure (coded in api_terminal and brenthy_api) which Brenthy provides to blockchain types to allow them to communicate with their API libraries. The brenthy_api module of the brenthy_tools library (upon which blockchain APIs such as walytis_api are built) communicates with api_terminal, a module of the core Brenthy software.

brenthy_tools is a package that contains utilities common to both its brenthy_api sub-package and Brenthy's core software. You can install the brenthy_tools python library and use brenthy_api to do some very basic interaction with the installed Brenthy instance yourself, such as looking up Brenthy's version.

Here's some Python code for that. Brenthy needs to be running for it to work.

from brenthy_tools import brenthy_api
print(brenthy_api.get_brenthy_version())

Blockchain APIs

Application Requests

In the above code example in which we looked up Brenthy's version, we used brenthy_api to make a request to Brenthy itself. BrenthyAPI's main purpose however is to forward requests and responses between a blockchain's API library (used by applications) and the blockchains running in Brenthy.

When sending a request from a blockchain's API library to Brenthy, brenthy_api encodes the blockchain library's blockchain type into the BrenthyAPI requests so that Brenthy's api_terminal knows which blockchain to forward the request to.

Here's that encoding being done in brenthy_api's send_request function:

# from Brenthy/brenthy_tools_beta/brenthy_api.py

def send_request(
    blockchain_type: str, payload: bytearray | bytes
) -> bytearray:
	...
	request = blockchain_type.encode() + bytearray([0]) + payload

Let's have a look at how a blockchain API library might use brenthy_api.send_request. Here's the _send_request function from the Walytis blockchain's walytis_api library:

# from Brenthy/blockchains/Walytis_Beta/walytis_beta_api/walytis_beta_interface.py

def _send_request(function_name: str, payload: bytearray | bytes) -> bytearray:
	# compose request to Walytis, encoding the library's version, remote function to call, and payload
	request = (
        encode_version(WALYTIS_BETA_API_PROTOCOL_VERSION) + bytearray([0])
        + function_name.encode() + bytearray([0])
        + payload
    )
	# use brenthy_api to send this request to Walytis
	reply = brenthy_api.send_request("Walytis_Beta", request)

brenthy_api sends requests in binary. This gives the blockchain full flexibility in the encoding it wants to use, e.g. plain packet-size efficient binary like Walytis does, JSON, or XML.

When communicating to Brenthy itself instead of one of its blockchains, "Brenthy" is encoded into the message in the place of the blockchain type. Here's that being done in brenthy_api's send_brenthy_request function:

# from Brenthy/brenthy_tools_beta/brenthy_api.py

def send_brenthy_request(function_name: str, payload: bytearray) -> bytearray:
	request = function_name.encode() + bytearray([0]) + payload
	return send_request("Brenthy", request)

In summary, each BrenthyAPI message is tagged with a blockchain type or with "Brenthy", so that Brenthy's api_terminal, the module which receives BrenthyAPI requests, knows whom to pass the requests on to. We can see this decision making coded in the request_router function in Brenthy's api_terminal module:

# from Brenthy/api_terminal/api_terminal.py
def request_router(request: bytearray, blockchain_type: str) -> bytearray:

    if blockchain_type == "Brenthy":
        return bytearray([1]) + brenthy_request_handler(request)
    for blockchain_module in blockchain_manager.blockchain_modules:
        if blockchain_module.blockchain_type == blockchain_type:
            reply = blockchain_module.api_request_handler(request)
            # bytearray([1]) signals success
            return bytearray([1]) + reply
	
    # bytearray([0]) signals failure
    return bytearray([0]) + json.dumps({
        "success": False,
        "error": UNKNOWN_BLOCKCHAIN_TYPE,
        "blockchain_type": blockchain_type,
    }).encode()

Now we've seen how with BrenthyAPI requests, applications can perform Blockchain operations (RPCs - Remote Procedure Calls), or simply ask for a certain piece of information. In both cases the requestee (the blockchain or Brenthy) responds with a reply: a report on whether the operation succeeded in the case of the RPC, or with the requested information in the latter case.

Blockchain Publications

When using BrenthyAPI requests, the application decides when an operation should be performed or when it wants to get a piece of information. When things happen on the blockchain that are outside the application's control, it is the blockchain running in Brenthy that must initiate the communication to inform the application of the event in a timely fashion.

To do this, BrenthyAPI uses a publish-subscribe system (pubsub for short). On Brenthy's side, the api_terminal takes the role of publisher and creates a pubsub socket. On the blockchain API library's side, brenthy_api is used to subscribe to that socket. On Brenthy's side, a blockchain can publish a message on the pubsub socket and it will be received by any application that has subscribed to it.

The blockchain also specifies so-called topics in the messages, by which the subscribers can filter. This allows the same publishing socket to be used for different types of messages, while the subscribers can configure their subscribing sockets to only receive messages published under topics relevant to their functionality.

Let's look at how the Walytis blockchain publishes messages about new blocks being received by the Blockchain object in walytis_api:

In walytis.Blockchain.download_and_process_block():

# from Brenthy/blockchains/Walytis_Beta/walytis_beta.py

# inform applications about the new block
walytis_beta_api_terminal.publish_event(
	self.blockchain_id,
	message={"block_id": bytes_to_string(block.short_id)},
	topics="NewBlocks",
)

download_and_process_block passes the event message and topics to walytis_beta_api_terminal.publish_event along with its blockchain ID. walytis_beta_api_terminal.publish_event encodes the blockchain ID into the topic, before passing on the event to api_terminal.publish_event, along with its blockchain type:

# Brenthy/brenthy_tools_beta/brenthy_api.py
def publish_event(
    blockchain_id: str, message: dict, topics: list[str] | str | None = None
) -> None:
	...
	api_terminal.publish_event(
        "Walytis_Beta",
        message,
        topics=[f"{blockchain_id}-{topic}" for topic in topics],
    )

In api_terminal.publish_event, the blockchain type is encoded into the topics:

# from Brenthy/api_terminal/api_terminal.py

def publish_event(
    blockchain_type: str, payload: dict, topics: list | None = None
) -> None:
	...
	for topic in topics:
		data = {"topic": f"{blockchain_type}-{topic}"}
		data.update(payload)
		publish_on_all_endpoints(data)

The actual code dealing with the pubsub sockets is still nested a few functions deeper inside, via publish_on_all_endpoints() which calls the publish functions of all the BrenthyAPI protocol modules (see BrenthyAPI-Protocol for details). This extra layer of complexity is due to Brenthy's backward-compatibility-guarantee system.

In summary, we have the same nested encapsulation for BrenthyAPI's event publication as for its requests.

Let's go through the process backwards, and look at the decapsulation of received events:

brenthy_api' provides the EventListener class for listening to to events published by a specific blockchain type. We see in its event-handler function for processing received messages how it removes the encoded blockchain-type from the topics before passing on the event to the blockchain-type's event-handler:

# from Brenthy/brenthy_tools_beta/brenthy_api.py
class EventListener:
	def __init__(
        self,
        blockchain_type: str,
        eventhandler: FunctionType,
        topics: str | list[str],
    ):
	    ...
		brenthy_topics = [
            f"{self.blockchain_type}-{topic}" for topic in topics
        ]
        ...
	
    def _handler(self, message: dict, topic: str) -> None:
        """Process an event from Brenthy Core."""
        topic = topic.strip(f"{self.blockchain_type}-")
        ...
        self.users_eventhandler(message, topic)

Rising up from the depths of BrenthyAPI's infrastructure, let's look at how a blockchain API library uses the brenthy_api's EventListener class to subscribe to publications from its blockchain. Again, we see its event-handler remove the encoded blockchain ID from the topic, completing the decapsulation:

# from Brenthy/blockchains/Walytis_Beta/walytis_beta_api/walytis_beta_interface.py

class BlocksListener:
    event_listener = None
    
	def __init__(
        self,
        blockchain_id: str,
        eventhandler: Callable[[Block, str], None] | Callable[[Block], None],
        topics: list[str] | None = None,
    ):
	    ...
		self.event_listener = brenthy_api.EventListener(
            "Walytis_Beta", self._eventhandler, f"{blockchain_id}-NewBlocks"
        )
    
    def _eventhandler(self, data: dict, topic: str) -> None:
        blockchain_topic = topic.strip(f"{self.blockchain_id}-")