Building Gateways for the XRPL Community

Zeeshan Sarwar | Blockchain Technology Specialist - Protocol

Overview

Wallet support is crucial for any blockchain ecosystem. Wallets serve as the primary interface through which users engage with the blockchain, with this in mind alongside increasing demand for cross-chain interoperability, it has become essential to provide a robust mechanism for transacting across diverse blockchain ecosystems.

In this blog, we’ll look at how The Root Network has been looking at ways to improve the user experience for the XRP community, through pallet-xrpl, enabling messages/transactions signed using the XRPL-based Xaman wallet, to be accepted by The Root Network.

The Xaman wallet is a mobile wallet that supports the XRP Ledger, our goal was to enable Xaman wallet holders to interact seamlessly, allowing them to sign and dispatch transactions directly to The Root Network. This integration ensures users can engage with The Root Network ecosystem without the need to switch wallets.

With the increasing demand for cross-chain interoperability, it has become essential to provide a robust mechanism for transacting across diverse blockchain ecosystems. pallet-xrpl is our answer to this demand, which would enable Xaman signed messages/transactions to be accepted by The Root Network.

In the following sections, we will share insights from our development journey, outline our high-level integration strategy, dissect the core components of pallet-xrpl, and discuss the challenges we faced along with the innovative solutions we devised.

Stay tuned as we unpack the intricate details of integrating the Xaman XRPL wallet with The Root Network.

Why support XRPL transactions on TRN?

Given the strong ties with the XRPL and the use of XRP as the underlying gas token, it was imperative for us to meet our users where they are. The Xaman wallet, being one of the most popular wallets in the XRP community, was a natural choice for integration. By supporting the Xaman wallet, we provide a seamless experience for XRP users to interact with The Root Network. This integration not only enhances user experience but also fosters cross-chain interoperability, expanding the reach of our ecosystem. It opens up new possibilities for users, enabling them to transact seamlessly across different blockchain ecosystems.

Problem statement

To enable XRPL users to interact with The Root Network, we needed to develop a mechanism that would allow users to sign XRPL messages and submit them to The Root Network. These messages would contain data such as the extrinsic to be executed (or the hash of it) - which would effectively allow an XRPL account to perform actions - i.e. state changes, on The Root Network. We identified the Xaman wallet, a popular mobile wallet that supports the XRP Ledger, as a good place to start.

The challenge was to design a solution that would facilitate the signing of transactions on The Root Network, using schemes supported by the Xaman wallet, then submitting these signed messages/transactions on our network, while maintaining the security and integrity of the transactions. Since these signed messages are not natively supported on The Root Network - i.e. these are not substrate extrinsics; we had to devise a way to support custom Xaman signed messages.

To integrate with the XRPL via the Xaman wallet, we must first understand the architecture of how a request to sign an XRPL message is initiated, the key entities/services involved in this process, and how the signed message/transaction is sent to The Root Network.

Architecture of the Xaman wallet integration

To understand how we could allow an Xaman wallet to sign transactions on The Root Network, we need to first understand how signing requests are sent to the Xaman wallet and then how signed payloads are relayed back to the caller.

  1. In the first step, the client (generally a frontend/dapp) sends a signed request to the Xaman wallet by utilizing the Xaman SDK and subscribes for the signed payload response over a WebSocket connection

  2. The SDK is a wrapper around the REST API, hence forwards the request to the Xaman backend infrastructure/servers; a QR code is generated and a link to that is returned to the client

  3. The client then displays the QR code, and scans the QR code - which opens the Xaman wallet app

  4. The user reviews the transaction details and signs the transaction using the Xaman wallet. The payload and signature are encoded - using the XRPL binary codec (more details below), the result is sent to the server - which then forwards it to the subscribed client

  5. The client receives the signed payload and submits it to The Root Network

Note: The Xaman wallet does not sign extrinsics, but instead signs XRPL messages - which contain data similar to that of an extrinsic. These messages are then submitted to The Root Network as self-contained calls (more on this below).

From signing to submission: A deep dive into the integration flow

What we want to achieve is to allow users to sign The Root Network transactions using their XRPL-based wallet, in this case through Xaman wallet integration. So how would we achieve this? First of all, what is The Root Network transaction?

A Root Network transaction is an extrinsic call that is submitted to the network. The user signs This extrinsic call using their private key via a wallet, where it is validated and executed by the validator(s).

In the case of the Xaman wallet, it cannot directly sign extrinsics; instead, it must sign messages - that too are of a specific format.

The following is a basic payload which the SDK would send to the Xaman wallet for signing:

const created = await xumm.payload?.create({
  txjson: {
    TransactionType: 'SignIn',
  },
  custom_meta: {
    instruction: "Sign Futurepass balance transfer extrinsic",
  },
});

The message/transaction is a special transaction type - SignIn; which is an Xaman wallet app-specific transaction (pseudo transaction). XRPL supports multiple transaction types - but we resort to using SignIn since this is Xaman specific and hence transactions of this type cannot be submitted to the XRPL ledger (since Xaman can also sign XRPL transactions for XRPL chain).

The custom_meta.instruction field is a custom field that is used to specify the instruction to the user - this is displayed in the Xaman wallet app when the user is signing the transaction.

The payload returned by this request looks like the following:

{
  "AccountTxnID": "...",
  "SigningPubKey": "...",
  "TxnSignature": "...",
  "Account": "rhLmGWkHr59h9ffYgPEAqZnqiQZMGb71yo",
}
  • AccountTxnID is a unique identifier for the transaction

  • SigningPubKey is the public key of the account that signed the transaction

  • TxnSignature is the signature of the transaction

  • Account is the derived (from the public key) XRPL account that signed the transaction

The basic payload does not have any transaction data, it is a simple sign request. So how can we encode the extrinsic data in the message? By looking at other transactional fields - which can be included in the request, there is an option to include arbitrary data via utilization of the [Memos](https://xrpl.org/docs/references/protocol/transactions/common-fields#memos-field) field. This field (Memos) allows for the inclusion of arbitrary hex data in the request - up to a maximum of 1Kb in size.

Given this knowledge, we could update the payload to include the extrinsic data in the Memos field like so:

const created = await xumm.payload?.create({
  txjson: {
    TransactionType: 'SignIn',
    Memos: [
      {
        Memo: {
          MemoType: stringToHex("extrinsic"),
          MemoData: stringToHex(`${extrinsic.toHex()}`),
        },
      },
    ],
  },
  custom_meta: {
    instruction: "Sign Futurepass balance transfer extrinsic",
  },
});

Which would produce the following signed response:

{
  "AccountTxnID": "...",
  "SigningPubKey": "...",
  "TxnSignature": "...",
  "Account": "rhLmGWkHr59h9ffYgPEAqZnqiQZMGb71yo",
  "Memos": [
    {
      "Memo": {
        "MemoType": "65787472696e736963",
        "MemoData": "..."
      }
    }
  ]
}

It must be noted that this response is encoded in the XRPL binary format. This format is specific to XRPL and is not natively supported on The Root Network. One can say, ok now we have a signed payload, we can submit this to The Root Network as an extrinsic payload. But there is a big problem with this, how do you even wrap this in the extrinsic? Who is the extrinsic signer? and how would the extrinsic be signed? Since we are not using a substrate/Ethereum wallet - if this payload were to be included in a traditional extrinsic we cannot sign extrinsics.

How can we support XRPL transactions on The Root Network?

The core of EVM transaction support on The Root Network is the SelfContainedCall, a special type of extrinsic call that encapsulates the transaction data and validation parameters within the call itself. It is possible to use the same approach to support XRPL transactions on The Root Network (after making a minor change to the frontier's SelfContainedCall trait).

To support this, we would need to expose an entry point for the self-contained call - this is where we introduce the new pallet - pallet-xrpl. The self-contained call, i.e. Xaman's signed message, must be submitted as an unsigned extrinsic call.

One could imagine the following extrinsic:

pub fn transact(
  origin: OriginFor<T>,
  encoded_msg: BoundedVec<u8, T::MaxMessageLength>,
) -> DispatchResult {

The encoded_msg would be the signed XRPL message, which would contain the extrinsic to be executed by the pallet. But before committing to this, we realized the hard limit of 1KB for the memo data. It is possible for large extrinsics, especially batch extrinsics to easily exceed the memo data limit. To address this, we can hash the extrinsic data and include the hash in the memo data field. while including the call to be executed as another parameter in the transact extrinsic above.

This would change the extrinsic to:

pub fn transact(
  origin: OriginFor<T>,
  encoded_msg: BoundedVec<u8, T::MaxMessageLength>,
  call: Box<<T as Config>::RuntimeCall>,
) -> DispatchResult {

Since this is an unsigned extrinsic, it must be validated on-chain. All signed extrinsics (normal extrinsics) are validated via executing signed extensions, these are core extrinsic validators. The signed extensions for The Root Network are:

pub type SignedExtra = (
	frame_system::CheckNonZeroSender<Runtime>,
	frame_system::CheckSpecVersion<Runtime>,
	frame_system::CheckTxVersion<Runtime>,
	frame_system::CheckGenesis<Runtime>,
	frame_system::CheckEra<Runtime>,
	frame_system::CheckNonce<Runtime>,
	frame_system::CheckWeight<Runtime>,
	pallet_maintenance_mode::MaintenanceChecker<Runtime>,
	pallet_transaction_payment::ChargeTransactionPayment<Runtime>,
);

We need to perform the same or similar validations for unsigned extrinsics by utilizing the data from the signed XRPL message. This implies that, in addition to the extrinsic hash, we must also include the validation parameters in the memo data. To validate similar unsigned extrinsics, the payload is updated to include the validation parameters in the memo data field.

const created = await xumm.payload?.create({
  txjson: {
    TransactionType: 'SignIn',
    Memos: [
      {
        Memo: {
          MemoType: stringToHex("extrinsic"),
          MemoData: stringToHex(`${genesisHash}:${nonce}:${maxBlockNumber}:${priorityFee}:${hashedExtrinsic}`),
        },
      },
    ],
  },
  custom_meta: {
    instruction: "Sign Futurepass balance transfer extrinsic",
  },
});
  • genesisHash is the genesis hash of the chain so that the same message cannot be replayed on another chain (i.e. a testnet message cannot be replayed on mainnet)

  • nonce is the nonce of the account, to ensure that the message is not replayed by the same account

  • maxBlockNumber is the maximum block number until the message is valid

  • priorityFee is an optional fee that the user can specify to priortize their transaction

  • hashedExtrinsic is the hash of the extrinsic call - to be executed within the transact extrinsic

Are we ready to submit XRPL transactions on TRN?

The response retrieved from the Xaman wallet is different from the JSON shown above (which was decoded for the sake of understanding). The response is encoded in the XRPL binary format. This format is specific to XRPL and is not natively supported on The Root Network. Why is this a problem? Why can we not submit the decoded payload in the unsigned extrinsic?

This is because the signature of the payload (the TxnSignature field) is actually signing the encoded message (as shown in the app source code here). Since we must validate the message on-chain (verify the signature is signed by the signing account/address); the encoded message must be submitted as is - to be decoded and verified against the provided signature on-chain. This was not as simple as it sounds; for 2 main reasons:

  • The Root Network protocol code is developed in Rust; there isn't an official Rust SDK to decode XRPL binary messages

  • The Root Network protocol code runs in a no_std environment; which means even if we were to find open-source rust code to decode XRPL messages, there is a high chance it would not work in a no_std environment

To address this, we had to re-implement the XRPL binary decoding logic in Rust. This was a non-trivial task, as the XRPL binary format is somewhat complex. Rather than implementing the decoding logic from scratch, we decided to first look towards open-source solutions; 2 main Rust libraries were found:

  • https://github.com/sephynox/xrpl-rust

  • https://github.com/gmosx/xrpl-sdk-rust

Supporting ed25519 signed transactions

Now that we can decode the XRPL binary message on-chain, verify the signature against the signed payload and validate the parameters in the memo data against common signed extensions for the self-contained-call; there is still an additional complexity that needs to be addressed.

The Xaman wallet supports multiple signing algorithms - ed25519 and secp256k1. In the initial implementation, we only accounted for secp256k1 signed messages since TRN only supports secp256k1 signatures - because ECDSA accounts (supported by Ethereum and TRN) support the secp256k1 curve.

However, to support any/all Xaman wallet accounts, we must provide support for ed25519 signed messages as well.

To address this, we devised a bespoke solution to generate an Ethereum address from the ed25519 public key:

impl TryFrom<ed25519::Public> for AccountId20 {
	type Error = &'static str;

	fn try_from(public: ed25519::Public) -> Result<Self, Self::Error> {
		let account =
			H160(keccak_256(&public.0)[12..].try_into().map_err(|_| "Invalid account id")?);
		Ok(Self(account.0))
	}
}

and in typescript:

const eoa = Web3.utils.toChecksumAddress(
  // remove "ED" prefix from public key to compute EOA
  // keccak hash produces 32 bytes (64 chars) - take last 20 bytes (40 chars)
  // get last 20 bytes of the keccak hash output (12 bytes = 24 chars; + `0x` = 26 chars)
  "0x" + keccak256(hexToU8a(`0x${publicKey.slice(2)}`)).slice(26),
);

Here, similar to how Ethereum addresses are generated from the public key, we generate an Ethereum address from the ed25519 public key by hashing the ed25519 public key and taking the last 20 bytes. This bespoke solution works for us, and we have incorporated this in our SDKs now. The resulting address from this operation is used to represent an ed25519 signed account on TRN (PR here).

Supporting XRPL accounts

With the support for ed25519 signed messages, we can now support all Xaman wallet accounts on The Root Network. Since the Xaman signed message provides the public key of the account that signed the message, we can use the same public key to derive the TRN account (based on the public key prefix - ED for ed25519 and 02or 03 for secp256k1). The same public key can also be used to derive the XRPL account addresses as well. To link a Root Network account to the XRPL account, a successful execution of the transact self-contained extrinsic emits an event with the following signature:

XRPLExtrinsicExecuted {
  public_key: XrplPublicKey,
  caller: T::AccountId,
  r_address: String,
  call: <T as pallet::Config>::RuntimeCall,
},

Indexers can then listen for this event and map the XRPL account to the TRN account based on the public key.

Looking further

As we look ahead into the future, we envision tighter integrations between the XRPL ecosystem (tooling, SDKs, etc.) and The Root Network.

Supporting the Rust XRPL SDK(s)

During the development of pallet-xrpl we have also made significant contributions to the unofficial Rust XRPL SDK.

We added support for decoding XRPL messages in addition to providing no_std compilation target for the core crates. Both these additions essentially allow any Rust project to decode XRPL messages/transactions on no_std environments, which are commonly on-chain.

As we further integrate with XRP-based tooling, we will likely need to rely more heavily on open-source Rust XRPL SDKs to provide XRPL-specific functionalities. In such cases, we might extend these SDKs or make generic improvements that could be upstreamed to the open-source repositories. These improvements would make Rust ↔ XRPL integrations much easier for future development work by anyone wanting to use Rust for working with XRPL.

First class support for additional XRPL wallets

Currently pallet-xrpl only supports messages/transactions signed by the Xaman wallet which specify the special Xaman SignIn pseudo transaction type. To support additional XRP wallets, we’d need to potentially support their respective transaction types or resort to integrating/accepting a more common transaction type (which is not submittable/acceptable by XRPL ledger itself).

Such wallet support would primarily be derived from user demand; which is how we decided to provide support for Xaman wallet in the first place.

The Root Network as an XRPL compliant RPC

One potential avenue for exploration is the native support for XRPL transactions on The Root Network. We already have half of the work done - we can decode XRPL binary messages on-chain and verify the signature against the signed payload via utilizing self-contained calls; similar to how Ethereum transactions are supported on The Root Network.

However, we can take this a step further by adding additional RPCs, potentially enabling users to natively transact on The Root Network using XRPL transactions. This would maximise the interoperability between the XRPL and The Root Network ecosystems.

Last updated

© 2023 -> ♾️