Building Pendulum: framework, Stellar bridge, and nodes

Since receiving the R&D grant from the Stellar Development Foundation, we have been hard at work building the Pendulum Chain prototype. As we near the completion of the prototype, we want to share our progress, some of our learnings, as well as plans for the future.

Framework: Substrate

In the early days of the project, we decided to use a blockchain framework instead of building a blockchain from scratch. Ultimately, the choice was between Polygon SDK and Substrate. While we expect great things of the Polygon SDK in the future, it was still immature at the time and it made sense to go for a more robust option. Substrate, on which Polkadot is built, has a broader feature set and has been battle-tested on Kusama.

As a framework, Substrate comes with a wide range of features for creating and deploying a blockchain system, which can be customized according to preference. A few features that made the framework attractive were the WebAssembly runtime, the forkless upgrades with built-in upgrade coordination, and the modular, component-based approach.

Runtime

The Substrate runtime can be executed in WebAssembly, which means blockchain logic can be written in any of the many languages to which Wasm can compile, including C/C++ and Rust. This will enable Pendulum developers to write smart contracts in the language of their choice and use existing tools, which flattens the learning curve for the Pendulum community and decreases implementation costs for network participants, who want to build services on Pendulum.

Consensus

The Pendulum consensus mechanism is based on existing Substrate pallets. As most Substrate projects, it relies on Proof of Stake. For block creation, it uses BABE in combination with GRANDPA for block finalization. BABE selects the block creation node based on Nominated Proof-of-Stake. It has the benefit that it is not publicly predictable which validator node will be selected for block production, which mitigates DDoS attacks on validator nodes. The exact properties of the consensus system (e.g. the slashing mechanisms and incentive schemes) will be discussed in future blog posts.

The Bridge to Stellar

A key component of the Pendulum Chain is its bridge to the Stellar network. In the following section, we will dig a little deeper into our plans.

The bridge is composed of a number of Pendulum nodes with additional bridging functionality (we use “bridge nodes”, “relayers”, ”relay nodes” interchangeably) and an escrow account on the Stellar network that the relayers co-sign.

The flow is illustrated below:

The Stellar escrow account holds and releases funds on the Stellar network. It is an m-out-of-n multi-signature account signed by a subset of Pendulum nodes that act as relayers. The relayers monitor the escrow account and sign the appropriate transactions. After deposit, the escrow account will hold the Stellar funds until a withdrawal is successfully executed, releasing the funds accordingly.

Similarly, the bridge nodes on Pendulum mint and burn (i.e. create or remove) the appropriate tokens according to the deposits and withdrawals. Using this infrastructure, users can deposit funds from Stellar to Pendulum, as well as withdraw from Pendulum to Stellar.

The relayers form a decentralized subnetwork with its own consensus, so no single relayer can execute deposits and withdrawals alone. The multi-signature setup (signers, key weights, and threshold) of the Stellar escrow account determines how the relayers reach consensus about bridging events. So any action they take must be signed by a sufficient number of relayers so that the signatures would authorize a transaction of the escrow account.

This is enforced by the nodes running the Pendulum network. In order to ensure that non-relay nodes are also able to verify consensus among the relayers, the Stellar escrow account’s multi-signature properties and every update to these are stored on the Pendulum chain as well. Relay nodes also need to stake tokens and risk that their tokens are slashed if they misbehave.

Deposit

The transfer of funds from Stellar to Pendulum is shown in the below diagram:

  1. To initiate a deposit, the user transfers the funds to the Stellar escrow account. The user must create a claimable balance transaction. A claimable balance is a feature of the Stellar protocol, which allows the beneficiary (“claimant”) to receive the funds at some later stage after the funds have already been moved out of the sender’s account.
  2. Relayers watch the escrow account and recognize claimable balances.
  3. Relayers check whether the escrow account has the required trustline (i.e. whether the account can hold the asset being deposited). The relayers will set the trustline on the escrow account before claiming the balance (refer to signature coordination diagram) if required. Both the trustline and the claim operations are required to be signed collectively by the bridge nodes.
  4. Relayers cooperate to trigger the creation of an asset in Pendulum that mirrors the Stellar asset and creates the funds on Pendulum that have been deposited on Stellar. The newly minted funds are sent to the Pendulum account with the same address as the Stellar account that deposited these funds.

Withdrawal

The transfer of funds from Pendulum to Stellar is described in the diagram below:

  1. To initiate the withdrawal, the Pendulum user sends funds to a bridge-owned withdrawal account on the Pendulum Chain.
  2. Relayers watch the withdrawal account.
  3. The relayers release funds from the Stellar escrow and transfer them to the designated Stellar recipient.

Bridge nodes

The bridge nodes are run by a substrate mechanism called an off-chain worker. These workers run in their own environment outside the Substrate runtime and allow execution of long-running and possibly non-deterministic tasks (e.g. operations that take more time than a block or may regularly fail like fetching data from a remote source). They watch the Stellar network for deposits to the escrow account to mint new funds on Pendulum and, conversely, watch the Pendulum bridge to initiate Stellar escrow release transaction.

During both withdrawal and deposit processes, the relay nodes in the bridge must coordinate the signing of the relevant transactions for the escrow account in the Stellar network. For this, the relayers can use the Pendulum network to do multi-signature coordination (signature collection) by wrapping the Stellar transaction and signatures into Pendulum transactions. The coordination process is illustrated in diagram below:

  1. Relayers watch for new Pendulum-wrapped Stellar transactions.
  2. When relayer (A) creates a Stellar transaction, it signs it and submits a transaction on the Pendulum network that wraps the Stellar transaction.
  3. Other relayers pick up the partially signed transaction, verify its content, then submit a similar Pendulum transaction containing their Stellar transaction signature.
  4. Once a relayer has sufficient signatures for the Stellar transaction, the transaction can be submitted to Horizon (the Stellar network’s API servers).
  5. Relayers await the successful submission of the transaction.

The Pendulum prototype: a look at the code

To start with a testable version of the above processes, we first built a prototype of a single bridge node that handles all the transactions across both blockchains. We will give a few code examples below to illustrate how it works.

During a deposit — a transfer from Stellar to Pendulum — the bridge node watches Stellar transactions of interest. When a transaction is a deposit to the escrow account, then another transaction is replicated in Pendulum. We need this transaction to be feeless because the bridge is executing it. In Substrate, the way to achieve this is to send an unsigned transaction with signed payload, from the off-chain worker to the Pendulum network.

// The main execution point of an offchain-worker
fn offchain_worker(_n: T::BlockNumber) {
// Fetch transactions, and for each one of them, decode it
let tx_xdr = base64::decode(tx.envelope_xdr);
let tx_envelope = stellar::TransactionEnvelope::from_xdr(tx_xdr);

if let stellar::TransactionEnvelope::EnvelopeTypeTx(env) = tx_envelope {
// Unwrapping the actual content of a transaction to process it
// If the transaction matches our criteria, then we replicate it
if let Some((_, res)) = signer.send_unsigned_transaction(
|acct| DepositPayload {
currency_id: currency_id,
amount: amount,
destination: destination.clone(),
signed_by: acct.public.clone(),
},
Call::submit_deposit_unsigned_with_signed_payload,
) {...}
}
// ...
}

The code has been simplified to illustrate a few important parts. As you can see, there are a few references to a stellar crate (a crate is a Rust package). Since Stellar has its own encoding mechanism to send transaction data (using XDR), we needed some specific logic to be able to use the data inside a transaction. Since we could not find something suitable, we have built our own Stellar SDK for Substrate.

You can also see that there’s a special line: Call::submit_deposit_unsigned_with_signed_payload. Here we make use of something called extrinsics. This line won’t actually execute anything in the context of the offchain worker. It’s more like a callback function: once a block is minted and this transaction processed, then this code will be called on-chain and perform the actual deposit.

We do something similar for withdrawals (removing the asset from circulation in Pendulum and unlocking them on Stellar), but in a different fashion. Here, the trigger of the extrinsic is not a watcher of the Stellar transactions, like we do for the deposit, but the user itself using an explorer like polkadot{.js} or a Pendulum wallet. When the user calls the extrinsic, the transaction is queued in the off-chain worker’s storage using off-chain indexing. This mechanism uses the consensus protocol to determine the index and thus makes sure that all the nodes have the same copy of the storage.

#[pallet::weight(100000)]
pub fn withdraw_to_stellar(
origin: OriginFor<T>,
currency_id: CurrencyIdOf<T>,
amount: BalanceOf<T>,
) -> DispatchResultWithPostInfo {
// Build and queue Pendulum transaction}

As you can see at the beginning of the function, there’s a weight of 100000 units. That means that every time you call this function from the explorer, a fee represented by this weight will be charged. Once the function is called, the withdrawal transaction is built and queued to be consumed later.

Making use of the same entry point as before, and after processing the new deposits, we handle the queued withdrawal transactions.

fn offchain_worker(_n: T::BlockNumber) {
// … Process new deposits …
// Pop next withdrawal from queue
let submission_result = (|| {
Self::pop_queued_withdrawal()
.map(|maybe_withdrawal| match maybe_withdrawal {
Some(withdrawal) => Self::execute_withdrawal(withdrawal),
None => Ok(()),
})
})();
}

Here we will use the same pattern as in the deposits. We need to execute the withdrawal on-chain. So we will call an extrinsic with a signed payload, and put that back on the chain. Then, some blocks later, the withdrawal operation will be actually executed on-chain. Only when this is successful, the funds will be released back to Stellar.

It is important to stress that the operations that alter the network state (deposit and withdrawal) always occur on-chain, and the selection of valid ones always depends on the consensus protocol of the Pendulum network.

Future Developments of the Bridge

In the above, we looked at the foundational scheme of the Pendulum-Stellar bridge and the state of the single node prototype. The next stage of development is to build a multi-node bridge and network.

While we are building this alpha version of the Pendulum testnet, we will work in parallel on improving the planned bridge concept in order to make the bridge even more secure and less prone to misbehavior or negative effects of, e.g., collusion among relayers. The goal is to build a decentralized, trustless bridge with a fair governance model. From where we are today, we consider several approaches, among which are:

  • Using the bridge only for token swaps: instead of, e.g., transferring funds from Stellar to Pendulum in a unidirectional fashion, one can only swap tokens in Stellar with tokens in Pendulum. These swaps can be done in a completely trustless way using conditional transfers. A unidirectional deposit can be simulated by a swap by issuing a token in Stellar that represents the deposited token in Pendulum. The user can then swap back that token for the Pendulum token to simulate a withdrawal.
  • Letting relay nodes act as oracles: relayers watch the Stellar network and provide relevant information to Pendulum. Instead of a special purpose relayer network, one can employ a service such as Chainlink, a popular decentralized oracle network. Chainlink’s level of decentralization and reliability carries over to the Pendulum-Stellar bridge.

Another more immediate challenge for creating a decentralized Stellar bridge is that the Stellar protocol only allows for 20 co-signers on a given account, which puts a ceiling on the number of bridge participants (as each relayer must also co-sign the escrow account). We will accept this limit for the alpha version of the testnet, but must find a solution before mainnet launch.

Next steps

With the successful completion of the Pendulum Chain prototype, we begin the journey towards an alpha testnet deployment. The high-level goals of the testnet is to complete the technical infrastructure, ensure all the incentive dynamics of node operators and other network participants are aligned and get external protocol builders to join the Pendulum network to create awesome things! Therefore we are very happy to hear from entrepreneurs and project developers who want to build on Pendulum Chain.

Sign up for our newsletter and follow us on social media channels to be up-to-date on all our latest developments:

Twitter: @pendulum_chain
Medium: @pendulum_chain
Telegram: t.me/pendulum_chain
Discord: discord.gg/ACVmQ4hg

Connecting Fiat to DeFi