Dymension Kaspa Bridge Design Details

Dymension Kaspa Bridge Design Details

We (Dymension team) have built a token bridge between Kaspa and Dymension L1 (frontend, explorer). It is a trust minimized decentralized multisig mint-and-burn bridge which takes user deposits on Kaspa and mints a 1-1 representation on Dymension. In the reverse direction the representation is burned on Dymension and released back to a user on Kaspa from a multisig escrow address. The multisig keys are held by a diverse subset of Dymension chain validators.

Dymension is a Cosmos SDK–based CometBFT proof of stake chain that supports an ecosystem of L2s (’rollapps’) - chains capable of arbitrary logic and smart contracts. The L1 provides core DeFi functionalities, including a token launchpad and bridging infrastructure.

The Kaspa<>Dymension bridge is novel, we developed it ourselves, but we closely mimic the design philosophy of the Hyperlane bridge ecosystem (docs, explorer), and used some of the code of that ecosystem too (main repo, cosmos plugin A, cosmos plugin B).

The Gist

The bridge has four components: Kaspa chain, Dymension chain, off-chain validators and an off-chain relayer. To elaborate:

A decentralized set of independent ‘validator’ actors run an off-chain process (code). Each validator has two private keys. One private key allows them to sign a (m-of-n) multisig to mint wKas on Dymension, and the other key allows them to sign a Kaspa TX which releases KAS from a (m-n) multisig address on Kaspa. A relayer is responsible for liveness, and gathers signatures from validators for relevant operations. The relayer is entirely untrusted: it does not control funds, and validators verify everything against their own data sources.

To bootstrap, each validator generates two keypairs and registers the public keys for inclusion in the bridges initial configuration which is stored on Dymension chain state. One keypair controls release of KAS on Kaspa and the other is for minting a wKAS representation on Dymension.

The list of pub keys derives a permanent Kaspa address to escrow user KAS funds. Technically, its a OP_CHECKMULTISIG based P2SH, using Schnorr keys. Each validator also creates their own Ethereum style key pair (ECDSA secp256k1) and Dymension has logic on-chain to verify an m-of-n signature set against these pub keys (code). In this way, the validators also control wKAS funds on Dymension. The Dymension configuration state is wholly controlled by Cosmos style on-chain governance voting, and not by any team or individual.

Users send regular Kaspa transactions, containing some metadata in the payload field to the Kaspa escrow address. A relayer monitors this and constructs a transaction targeted to Dymension L1 which will mint wKAS to the user’s target destination address on Dymension. The relayer then queries each validator to get a signature, which is required for the TX to pass, and forwards to Dymension once enough have been collected. The complement is true in the other direction, a relayer constructs a PSKT using SIG_HASH_ANY_ONE_CAN_PAY and collects signatures from each validator before forwarding to the Kaspa network.

The protocol guarantees user safety: a 1-1 correspondence between escrowed KAS and minted wKAS. The only security assumption is a lower bound on the number of trusted validators. No other participant is trusted. Modulo a code bug, or a majority of validator misbehaviour, loss of funds is impossible.

The Details

Hyperlane

Dymension operates Hyperlane (HL) bridges between Dymension and Ethereum/Solana/Base/Binance. Hyperlane is a generic cross-chain message passing tech adopted by 130+ major chains. It does not prescribe a security mechanism, but in practice m-of-n multisig based bridges are a first class citizen of the ecosystem. Moreover, their design philosophy is quite simple: they only rely on a quorum of trusted validators, and do not trust third parties like relayers. Moreover, their feature set is slimline: there are no message timeouts, refunds or error conditions.

While HL does not out of the box support Kaspa (their codebase requires smart contracts) we adopt many of their concepts, and approximately half our Kaspa bridge stack uses their code (much of the Dymension code and the off-chain process code). A lot of our work was forking their code to make it work for Kaspa. The simplicity of the design philosophy simplified the problem, and also reduces the surface area.

The Protocol

Now we present the validation protocol itself, and argue safety.

The protocol runs forever, but requires an initial bootstrap phase. The bootstrap phase consists of initial keypairs generation followed by a seed TX to the escrow address. Concretely, after creating the m-of-n P2SH, the escrow address requires a first seed deposit to create an initial UTXO. Dymension chain governance designates this outpoint the ‘anchor’ and stores it in bookkeeping state. This marks the end of the bootstrapping of the bridge, and afterwards it is ready to process user actions.

Let’s analyse the two directions of user actions:

Kaspa → Dymension: escrow KAS and mint wKAS

Step 1: user initiation

A user uses any normal Kaspa wallet that supports populating the TX payload field to send money to the escrow address controlled by validators (in practice Dymension supplies a frontend to do this for the user). The payload contains a HL message containing information such as the address to mint wKAS to on Dymension.

Step 2: observation

The untrusted relayer monitors the escrow address and notices the deposit. It constructs a Dymension TX to process the HL message contained within. Dymension won’t accept the TX unless it has m-of-n validator signatures. The relayer queries the validators to gather these.

Step 3: validation and signing

On receiving a signing request (code) the validator checks against his chosen Kaspa RPC and indexer that the deposit indeed exists at the escrow address, and is confirmed with at least 1000 confirmations (code) (concretely that the virtual blue score of the network is at least 1000 higher than the blue score of the accepting block of the transaction), which guarantees a negligible probability of mistake.

If valid, the validators compute and sign a digest which is over the full data of the deposit.

Step 4: relaying

The relayer forwards the signed TX to Dymension chain which mints the wKAS to the user and stores the transfer digest in a permanent set. The chain logic prevents a replay by only allowing one mint action for this digest.

Dymension → Kaspa: burn wKAS and un-escrow KAS

This direction is more nuanced, and consists of a first step to actually facilitate the user action, and a second necessary step for bookkeeping that is not visible to the user.

Part 1: withdraw to user destination

Step 1: user initiation

A user initiates by sending a withdrawal TX to Dymension. Dymension burns wKAS from the user balance and writes a HL message to the outbox area of the chain state, with status ‘pending’. The user populates the Kaspa destination address in this message.

Step 2: observation

An untrusted relayer monitors this outbox and constructs a Kaspa PSKT to spend the escrowed KAS. Of course, Kaspa network will not accept the PSKT without m-of-n signatures from validators. Crucially, the relayer constructs the PSKT to embed the HL message ID of the withdrawal in the payload, and to spend the designated anchor outpoint recorded in the Dymension chain state.

Step 3: validation and signing

On receiving a signing request (code) the validator checks against this chosen Dymension RPC to atomically (in one action) check that:

  1. The anchor outpoint spent by the PSKT is indeed the anchor on Dymension

  2. The PSKT has a change output back to the escrow address (using the initial seed KAS)

  3. The message ID being processed has status ‘pending’ (not ‘complete’).

At this stage there are some other trivial validations such as making sure the PSKT spend amount and address matches the HL message amount and address etc.

Step 4: relaying

The relayer forwards the signed TX to Kaspa network which releases escrowed KAS to the users specified transfer destination address.

Part 2: bookkeeping

After a withdrawal PSKT is accepted to the network and the user’s specified recipient gets their KAS, the protocol must update Dymension with a new anchor outpoint: the change output outpoint from the processed PSKT.

Again, a relayer tracks completed withdrawals and again requests signatures from validators for a second Dymension TX type. This TX (code) does an atomic compare and swap operation to replace the anchor outpoint currently in state with the one in the message. At the same time, the TX updates the set of ‘complete’ withdrawals (status change from ‘pending’ to ‘complete’).

On receiving a signing request for this TX type validators check that the PSKT which spent the old anchor and created the new one had exactly the message ID in its payload which is being marked as ‘complete’. Again, the validator checks this against his Kaspa RPC/indexer of choice, and ensures 1000 confirmations on the PSKT.

Observations

In this way, the set of PSKTs that are accepted to Kaspa network for user withdrawals forms a linear sequence, with each element spending the output of the previous and with each spent output being the designated anchor on Dymension chain state at some time. Moreover, the union of message IDs contained in the payloads is exactly the set of withdrawals marked as completed on Dymension.

In this model, observe, it’s impossible to double spend. The reasoning is:

Suppose a fault: that there were two withdrawals on Kaspa for one on Dymension. For two withdrawals to have been accepted on Kaspa, there must have been two PSKTs spending a disjoint set of UTXOs. Remember, a validator only signs a PSKT if it spends an anchor outpoint on Dymension. Therefore without loss of generality one PSKT must spend an anchor outpoint AO0 from observation time t0 and the other must spend an anchor outpoint A01 from observation time t1 with t0 < t1. Also, both PSKTs must contain the same message ID M in the payload. In order to update the Dymension chain state from <t0, AO0> to <t1, AO1> Dymension must also have updated the set of ‘complete’ messages to contain the message ID. i.e. <t0, AO0, {M}> and <t1, AO1, {M}> However, the validator would not then have signed the second PSKT because it only does so if the message ID M is pending. Contradiction.

Optimisations and Practical Concerns

Throughput, batching

The protocol fulfill multiple user withdrawals in a single PSKT, because each PSKT can have multiple outputs, and the payload can fit more than one message ID. Moreover, the relayer can create a linear sequence of PSKTs and ask the validators to sign them all. That sequence is linked by each TX after the first spending the emphemeral anchor of the previous. In this way, the protocol is guaranteed without having to roundtrip through Dymension state updates so often.

Configuration will use a 5-of-9 multisig for PSKT signing and our tests show the system can do dozens of transfers per minute in both directions with this arrangement.

PSKT construction

Partial signatures: each PSKT input contains a partial_sigs: HashMap<PublicKey, Signature> field. When validator V signs, they use an input filter to select only escrow inputs. For each such input calc_schnorr_signature_hash(..) computes a digest committing to: transaction metadata (version, lock_time, subnetwork_id, gas, payload), all outputs (amounts, script_pubkeys), and the single input being signed (previous_outpoint, amount, script_pubkey). Validator V’s secp256k1 keypair signs this digest via Schnorr sign_schnorr, producing a signature which is inserted into partial_sigs[V.pubkey] = signature. Each validator independently populates their entry in this map. The relayer similarly signs fee inputs, populating their pubkey entry. The protocol uses SIGHASH_ALL | ANYONECANPAY to let the relayer pay network fee and give itself change without allowing unwanted spending.

Finalization: the relayer finalization logic makes the unlocking signature_script for each input. For escrow inputs it take the first m (of n) sigs, and constructs the prefix for the stack machine. These signature_scripts unlock their respective inputs when Kaspa executes the stack machine: for P2SH, it hashes the provided redeem_script to verify the P2SH commitment, then it executes the redeem_script’s OP_CHECKMULTISIG with the m signatures on the stack, which verifies m schnorr signatures against the m-of-n pubkeys embedded in the script.

RPC and man-in-the-middle, validator set choice

The validator reads Kaspa network state and Dymension chain state. It requires both a Kaspa node websocket RPC (canonical codebase) connection, and also a Kaspa indexer backed REST server (indexer,server), and basic Dymension RPC. Our team instructs each validator to run their own instances.

6 Likes

Excellent detailed review.

Highly recommended for those looking for more technical context.

2 Likes