How RGB Swaps Work on the Lightning Network
Lightning-native RGB swaps are easy to misunderstand if you approach them with the mental model of a classic atomic swap.
In a traditional cross-asset swap, you usually imagine two legs, two counterparties, and some protocol that ensures neither side can cheat. In the implementation discussed here, the story is different. The swap is not modeled as "one on-chain trade plus one off-chain trade," and it is not executed as two unrelated payments that happen to be coordinated. Instead, the swap is embedded into a single Lightning payment path and becomes a property of how one HTLC flow is constructed, intercepted, validated, and forwarded.
That design has several important consequences:
- The swap is executed inside one logical payment.
- The swap provider sits in the middle of the route, not outside it.
- Atomicity comes from one
payment_hashand one settlement condition. - RGB affects not only routing, but also commitment construction, HTLC state, and client-side transfer history.
This article explains that model in detail. It focuses on the engineering principles behind RGB swaps in a Lightning-based system built around rgb-lightning-node, a patched rust-lightning/LDK stack, and RGB wallet/runtime support.
Assumed background. This article assumes familiarity with basic Lightning channel mechanics: HTLCs, preimages, and commitment transactions. Readers who need a refresher on those topics should consult the Lightning Network specification (opens in a new tab) before continuing.
The Core Mental Model
The simplest way to understand an RGB swap is:
one payment, two route segments, one middle node where the asset conversion happens.
Suppose Alice wants RGB-USDT and Bob wants BTC. A naive explanation would say "Bob sends RGB to Alice, and Alice sends BTC to Bob." That statement is directionally true, but technically misleading. The actual implementation does not first send one asset and then separately send the other. Instead, the payer constructs a circular Lightning payment whose path goes:
maker -> ... -> taker -> ... -> makerThe maker starts the payment, the payment passes through the taker, and the route comes back to the maker. The taker sits at the turning point of the route. At that point, the taker can observe:
- what comes in on the inbound leg
- what is expected to go out on the outbound leg
- whether both sides match a pre-agreed quote
If the quote matches, the taker forwards. If not, the taker rejects the intercepted HTLC. Because the full route is still locked by the same payment_hash, the system gets atomic behavior from ordinary Lightning settlement machinery instead of from a separate bespoke swap protocol.
In practice, the route looks like this:
The Circular Route Topology
first_leg second_leg
Maker ------------------------------------------------------------> Maker
| ^
| |
+--> Hop A --> Hop B --> Taker / Swap Point --> Hop C --> Hop D ---+
inbound side at taker outbound side at taker
One logical payment:
- one payment_hash
- one settlement condition
- one route with two economic segmentsThe circular structure makes this a route-level swap rather than a dual-payment swap. Because the maker acts as both payer and payee, the same payment_hash locks both directions of the trade simultaneously, with no external coordinator needed.
A Minimal Algorithmic Description
Before diving into each component, the full mechanism can be summarized as a state transition system. The rest of the article expands on each step.
Inputs
- a quote
(maker_wants, taker_wants, expiry, contract_id) - an accepted taker allowlist entry for
payment_hash - a circular route with two segments
- per-hop BTC and optional RGB forwarding data
State
- channel HTLC state
- commitment construction state
- RGB payment metadata
- transaction transfer history
- swap lifecycle state
Transition
- The maker creates a
payment_hashand quote. - The maker shares the quote out of band as a
SwapString. - The taker accepts the quote and stores the
payment_hashin its swap allowlist. - The maker constructs a circular route and marks the swap point.
- The maker sets per-hop RGB amounts where needed and marks the payment as
swap_payment = true. - The payment enters the channel state machine as one HTLC flow.
- At the swap point, the taker intercepts the HTLC and validates inbound and outbound legs against the quote.
- If validation fails, the taker rejects the HTLC and the payment fails cleanly; both legs unwind under normal Lightning failure propagation, and no RGB state is transferred.
- If validation succeeds, the taker forwards the HTLC.
- The route settles under one preimage and one
payment_hash. - Channel state, commitment state, RGB transfer metadata, and swap lifecycle state are all updated consistently.
Roles: Maker, Taker, and the Quote
The swap starts with a quote created by the maker.
Conceptually, the quote says:
- what the maker wants to receive
- what the maker is willing to give
- when the quote expires
In the implementation, this is represented by a structure similar to:
from_assetandqty_from: what the maker wants from the takerto_assetandqty_to: what the maker gives to the takerexpiry: validity window
This directionality matters. It is easy to reverse it mentally and get confused later when reading validation logic.
If the maker wants RGB and gives BTC, then:
from_assetis the RGB assetqty_fromis the RGB amount the maker expects to receiveto_assetis BTCqty_tois the BTC amount the maker will pay
If the maker wants BTC and gives RGB, then the direction flips accordingly.
The quote is serialized into a SwapString, a compact string that can be copied, transmitted out-of-band, and accepted by the taker. The quote is not negotiated by Lightning nodes during route construction. The system assumes the price and trade terms were already agreed upon before execution begins.
Quote discovery and pricing live above the transport layer. The transport layer only enforces and executes a pre-agreed trade.
Why One Payment Hash Matters
The quote includes a payment_hash anchor. This is not an implementation detail; it is the thing that makes the design coherent.
The same payment_hash is used as:
- the identity of the swap in local persistent state
- the identifier the taker sees when intercepting the HTLC
- the settlement condition for the full circular payment
Because both route segments belong to one logical payment, the taker does not need a second settlement protocol to connect the two sides of the trade. The inbound and outbound sides are already coupled by Lightning's normal preimage-based success condition.
The Route Structure: Two Segments Inside One Payment
Execution begins when the maker turns the quote into a circular payment path.
The route is built as two concatenated segments:
first_leg: maker to takersecond_leg: taker back to maker
Each segment may carry different value semantics.
For example:
- the first segment may carry RGB amount information
- the second segment may carry only BTC amount semantics
Or the reverse:
- the first segment may be BTC-only
- the second segment may carry RGB forwarding data
Standard route construction assumes payment amounts evolve monotonically along the path as fees accumulate backward. A swap violates this assumption because the taker gains one asset on one side and loses a different asset on the other. To support this, the patched implementation separates the base payment amount carried by a hop from the accumulated forwarding fees, allowing the route to encode a non-monotonic value profile across the swap point.
How the Taker Knows a Payment Is a Swap
The maker marks the route so that the taker can recognize the swap at forwarding time.
One approach used in the patched stack is to reserve a marker bit in the short channel id. The first hop of the second leg is tagged with a special swap flag. When the payment reaches the taker and the taker examines the next hop, the node sees that the HTLC is not an ordinary forward but a swap-designated forward.
At that point, the Lightning node emits an interception event containing extended swap-relevant fields such as:
- inbound BTC amount
- expected outbound BTC amount
- inbound RGB amount
- expected outbound RGB amount
- the payment hash
- channel information for the relevant edges
Without this interception mechanism, the taker would be just another forwarding node; with it, the taker becomes a controlled exchange point embedded inside a Lightning route.
The Validation Rule at the Swap Point
When the taker intercepts the payment, the system checks whether the observed flow matches the accepted quote.
The taker verifies:
- the payment hash is on an allowlist of accepted swaps
- the inbound and outbound assets match the quote
- the inbound and outbound amounts match the quote
- the involved RGB channels carry the expected
contract_id - the quote has not expired
You can picture the decision point as one inbound HTLC leg being compared against one outbound HTLC leg under the same payment context:
HTLC Asset Conversion at Swap Point
inbound HTLC outbound HTLC
maker ---------------------------> Taker ---------------------------> maker
asset = BTC asset = RGB
amount = 5,000 msat amount = 100 USDT
same payment_hash
same preimage-based settlement
forward only if quote matchesFor the opposite direction, flip the assets:
maker ---------------------------> Taker ---------------------------> maker
asset = RGB asset = BTC
amount = 100 USDT amount = 5,000 msatRGB to BTC
The taker receives RGB on the inbound side and forwards BTC on the outbound side. The taker checks that the inbound RGB amount matches the quote, the channel carries the expected contract, and the BTC-side difference matches the quoted consideration.
BTC to RGB
The taker receives BTC on the inbound side and forwards RGB on the outbound side. The check is symmetric: outbound RGB amount, contract id, and BTC-side difference must all align with the quote.
RGB to RGB
The taker exchanges one RGB asset for another. The BTC delta should be zero except for routing fees, and both RGB contract ids and amounts must match the quote.
The broader principle is the same in every case: the taker does not discover the trade from market conditions; the taker checks whether the observed HTLC flow matches a pre-accepted trade template.
Why RGB Requires Extra Routing Data
A plain Lightning payment only needs BTC-denominated forwarding parameters. RGB swaps additionally require the route to describe, hop by hop, whether RGB value is moving and how much.
The patched implementation therefore introduces per-hop RGB forwarding information, expressed through a field such as rgb_amount_to_forward. The two route segments in a swap may carry entirely different RGB semantics: one leg transfers RGB, the other carries none, and in an RGB-to-RGB swap both legs carry RGB but for different contracts.
To handle this, the system uses a payment-scoped flag swap_payment = true. Without it, the sender-side logic would normalize all hops to the same RGB amount, which is correct for ordinary RGB payments, but fatal for swaps where the two legs intentionally differ. Setting swap_payment = true tells the stack to preserve per-hop RGB amounts as specified rather than collapsing them. If this flag were missing, the second leg would inherit the RGB amount from the first leg, causing the taker's validation to fail because the outbound RGB amount would not match the quote.
The Effect on Channel State
Once RGB enters the picture, the swap has a concrete impact on channel state.
Each affected channel records HTLC updates carrying RGB metadata in addition to BTC amounts, affecting at least three layers of channel behavior.
1. Pending HTLC State
When the payment is added, the channel records not only that an HTLC exists, but also the RGB amount attached, the contract id involved, and whether the payment belongs to a swap flow. This matters for routing correctness and restart recovery.
2. Commitment Transaction Construction
When commitment transactions are rebuilt, RGB-bearing HTLCs require special handling. The commitment builder must preserve enough information so that the RGB semantics are reflected in the commitment structure, including assigning RGB-relevant outputs and injecting anchor information such as OP_RETURN data via wallet-side PSBT processing.
3. HTLC Second-Level Transactions
If an HTLC ever needs to be resolved on chain, second-level transactions may also need RGB-aware coloring, linked to the parent commitment's transfer metadata so that the RGB asset state remains traceable along the descendant transaction path.
The Effect on Chain State
A successful off-chain swap does not cause a force-close, but the protocol must be prepared for the on-chain path regardless. On RGB-bearing legs, commitment and HTLC-related transactions may be colored with metadata linking them to RGB asset transfers, recording the transaction id, RGB contract id, and RGB amount. This transfer metadata allows child transactions to reconstruct the RGB semantics of their parent outputs, forming part of the protocol surface rather than mere bookkeeping.
The Effect on Client-Side History
A robust swap implementation must survive restarts, partial failures, and delayed settlement events. The client therefore persists swap-related state at multiple levels.
Swap Lifecycle State
The application tracks states such as waiting, pending, succeeded, and failed. After a restart, the client can determine whether a swap was merely quoted, partially forwarded, or fully settled.
RGB Payment Metadata
Payment-scoped records tie a payment_hash to RGB-specific execution details: contract id, RGB amount, direction, and whether the payment should preserve per-hop RGB amounts. This metadata allows the sender and channel manager to interpret the payment correctly during route construction, commitment updates, and recovery.
Transfer History
Per-transaction transfer records allow the RGB subsystem to reconstruct how asset state flowed through commitment and HTLC-related transactions. This is especially important when a later on-chain step needs to continue coloring child transactions.
A completed swap therefore updates three persistent histories simultaneously: payment history, channel-adjacent RGB metadata, and chain-anchored transfer history.
Why This Is Not "Just an Atomic Swap"
Calling this design an atomic swap is not wrong, but it is incomplete.
The exchange is encoded as the shape of one routed payment: the path is circular, the taker is the midpoint, the route carries heterogeneous asset semantics, and the state machine spans Lightning forwarding, RGB-aware commitments, and client-side persistence. This is a more Lightning-native design than coordinating two independent transfers at a higher layer.
Engineering Constraints and Open Edges
This architecture requires nontrivial extensions to the Lightning stack:
- swap-aware HTLC interception
- non-monotonic route amount handling
- per-hop RGB forwarding data
- RGB-aware commitment and HTLC coloring
- persistent metadata for restart-safe recovery
Some responsibilities remain outside the core transport: price discovery, quote negotiation, routing policy, fee policy, and force-close fee-bump coordination. That division is intentional. The transport layer enforces a pre-agreed trade; it does not negotiate one.
Conclusion
RGB swaps on Lightning are best understood as an execution pattern rather than a standalone exchange primitive.
The swap works because a maker constructs one circular payment, a taker validates the conversion point in the middle, and a patched RGB-aware Lightning stack preserves the right semantics across routing, commitments, HTLC resolution, and client history.
An RGB swap is one Lightning payment whose middle hop behaves like an exchange, while the surrounding stack ensures that channel state, chain-anchored RGB history, and client persistence all remain consistent with that exchange.