This article is part of Gravity documentation that pertains to the technical aspects of the protocol and network. It provides an all-encompassing, example-based overview that demonstrates what a developer may encounter when integrating a new blockchain into the Gravity crosschain network, using Ethereum & BSC integrations as an example.
In order to extend the Gravity Protocol by adding support for a new blockchain, a sequence of several commits is expected into Gravity-Tech/gravity-core and a few other Github repositories mentioned throughout this article. These source modifications can be divided into four parts:
- Gravity Ledger Node logic (adaptor)
- Gravity Oracle logic
- Gravity Core contracts (Gravity-Tech/gravity-core/contracts)
- Gravity Deployer (Gravity-Tech/gateway-deployer)
We highly recommend opening pull requests in the specific repositories mentioned in the guide while implementing your solution, which should minimize the rewriting of the existing codebase. If your aim is to support application-layer logic on top of the new integrated chain, several changes need to be introduced to Gravity-Tech/gateway. That part of logic also includes writing an appropriate USER-SC (user smart contract).
The first alpha product built on top of Gravity is SuSy, a protocol and interface for cross-chain gateways. It currently connects Waves as origin and Binance Smart Chain as the destination chain. SuSy mainnet alpha is live at susy.gravity.tech and provides a frontend interface that allows users to swap USDN tokens from Waves into BSC. USDNs on BSC incorporate an identical functionality as on Ethereum, including auto-staking, elastic supply, and staking rewards that are automatically distributed among all holders.
The SuSy protocol is based on trust in the oracle, which is an intermediary in the transfer of information from one blockchain to another. From a technical standpoint, when implementing the oracle as a trustless decentralized system, which is what the Gravity protocol does, cross-chain gateways on top of it inherit the trustlessness. Another feature of the SuSy protocol implementation over the Gravity oracle protocol is the presence of useful high-level abstractions and services.
Furthermore, to conduct a cross-chain swap of a token from one blockchain to another, no additional tokens are required, except for the native tokens of the corresponding blockchain networks.
Below is the description of SuSy cross-chain transfer algorithm. It shows a transfer of a token from ORIGIN-CHAIN to DESTINATION-CHAIN, where it will be issued as a swT (wrapped) token and sent to the recipient R in DESTINATION-CHAIN.
A user (S) interacts with the LU-PORT smart contract by transferring an amount (A) of the T token to it and specifying the recipient’s public address in DESTINATION-CHAIN. The gateway smart contract automatically creates a unique SWAP-ID and sets the registered status. The received funds are blocked on the LU-PORT smart contract.
Information about this event is handled by extractors, the Gravity network’s services that process the received data and communicate it to Gravity. From the Gravity framework, the oracle moves hashed data about the new SWAP-ID and swap directions to the verification contract (NEBULA-SC), in which the signatures of the Gravity network validators and the legitimacy of the transferred context are verified.
Upon verification, the SEND-DATA-TX transaction is called, containing a set of data and instructions for issuing and sending swT tokens to the recipient (R).
Likewise, all data about this event is handled by Gravity network oracles, and, contingent upon successful execution, the “processed” status is set. After reaching a certain number of blocks at which the likelihood of a fork is minimal, it may be necessary to set the finalized status.
In the opposite direction, for transferring the swT token from DESTINATION CHAIN to ORIGIN CHAIN and unlocking T on the LU PORT contract, the procedure is similar. The only difference is in the final transactions, that is, the burning of the swT token on IB PORT and unlocking the T token on LU PORT, are reversed.
As regards the extension of SuSy dApp onto other blockchains, USER-SC plays the role of IB (Issue-Burn) and LU (Lock-Unlock) ports. USER-SC methods are meant to be invoked by NEBULA-SC. So, in order to have a working application on top of the protocol, one needs to:
- Provide new NEBULA-SC & SYSTEM-SC (Gravity contracts) inside the Gravity-Tech/gravity-core/contracts.
- Create a custom USER-SC, which is consistent with your custom NEBULA-SC.
Recommendations on extending the Gravity Core
To ensure compatibility with a new chain, a set of separate smart contracts is required. More importantly, there are two types of contracts: those used by the core of Gravity and application-specific contracts (e.g. SuSy).
Examples of an IB Port and a LU Port for Ethereum and Waves can be found here:
Two new smart contracts should be implemented in the new blockchain network with the following functionality:
- The smart contract should support an external call of the attachEventData function with the following parameters: Token ID (optional, since different tokens will have their own gateways), Amount, Receiver
- Only one of 5 admin accounts should be able to call this method.
- After attachEventData, Receiver must receive the wrapped tokens
- Holders of wrapped tokens should be able to send tokens to the gateway address, which should trigger API (RPC) calls.
For all statements 1–4, an open and public API must be implemented.
After creating the contracts, they need to be compiled into bytecode. The compiled bytecode files should be put into the “/abi” directory. Consider checking the “contracts” directory inside the Gravity-Tech/gateway repository to see examples of gateway contracts’ source code.
In order to add an implementation for any new chain, it should first be supported by the Gravity Network itself. After completing the smart contracts, it is necessary to implement core adaptors within the gravity-core repository. The architecture of Gravity Core application is modular, without any tight coupling. This guarantees the ease of extending separate components. In principle, using any programming language or library is acceptable, however it is generally preferable to extend the existing Go source code by providing implementations for required methods rather than rewriting from scratch.
To get a clearer idea of the required changes to the Gravity core, review this “BSC support” pull request for Gravity Ledger Node extension. Later on in this article, we are going to illustrate each portion of the changes described in the pull request above.
Key points to keep in mind:
- Take notice of the BlocksInterval parameter for the specific chain (required for a correct network “pulsation”).
- Include an implementation of the adaptors, IBlockchainAdaptor interface, which is the most descriptive part of each specific chain.
- Try starting a node & deploying contracts in a customnet to test out your newly written adapter. After testing, create a pull request to Gravity-Tech/gravity-core.
Overview of the required changes based on the BSC & Ethereum integrations
In the createApp() function, a constructor for instantiation of a new adaptor should be provided:
Consider storing your adaptor implementation in common/adaptors/[chain_name].go.
An important part that relates to communication with a specific chain is the IBlockchainAdaptor interface:
Let’s review how the Ethereum adaptor meets the interface’s requirements.
First, we declare blockchain-specific types and a struct:
Ethereum implementation contains:
- WithEthereumGravityContract() function that enables instantiation based on GravityContractAddress. It is invoked in the createApp function we mentioned earlier.
- EthAdapterWithGhClient() function, used for launching a Gravity Oracle:
It is also necessary to implement a constructor function, such as NewEthereumAdaptor() below:
Let’s explore the GetHeight() and Sign() methods. The first one retrieves blockchain height and the second one implements bytes signing:
WaitTx must block the thread. A simple for-select pattern allows for handling cases where waiting for a transaction is required. Be cautious: queryTicker is used here to prevent a goroutine leak, so the 3 seconds timeout is hardcoded:
PubKey() resolves the adaptor’s public key. ValueType() instantiates a Nebula-SC using a corresponding ABI. Next, it calls DataType() in order to resolve the value type that the Nebula works with (Gravity supports Byte, Int, String types).
The AddPulse() implementation contains many technical details. Most importantly, this method performs:
- Checking whether the Nebula pulse count and the BFT value are persisted to smart contract state. If any of the values does not exist, the method returns an empty string.
- Checking whether a correct number of validators (>= BFT value) have signed the message.
SendValueToSubs() method is responsible for sending values to Nebula’s subscribers. Nebula is in one-to-many relation to subscribers.
SetOraclesToNebula() updates oracles of the Nebula. Signing and public key verification is mandatory:
SendConsulsToGravityContract() is similar, but it is used for consuls and in the context of Gravity contracts.
SignConsuls(), as well as SignOracles(), is responsible for signing the updated consuls/oracles. Both methods use practically the same algorithm.
LastPulseId() resolves the id of the last pulse of the Nebula, which is the last action’s ID performed by Nebula oracles. This method is crucial for Gravity oracles, because it is commonly used in iterators which compare current and previous pulses.
LastRound() determines the latest action in Gravity network. A round, essentially, is a specific index of blockchain height, where Gravity network mutated the state either through oracles/consuls update, score update, or pulse sending.
RoundExist() verifies whether a certain round exists.
In this file, a constant should be instantiated. In other places, where existing constants are mentioned, this new constant should be added as well:
This module is responsible for Nebula logic.
Here, a chain behaviour that relates to account address instantiation is specified.
This module is responsible for parsing of consul keys.
The Oracle component
A crucial part of Gravity Oracle. node.go, as well as the entire oracle/node module, describes existing implementations and sets up oracle constraints.