Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

blip-tap: initial bLIP draft for Taproot Asset Protocol channels #29

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

Roasbeef
Copy link
Contributor

@Roasbeef Roasbeef commented Sep 6, 2023

This bLIP describes a variant on the "simple taproot channels" proposal that also supports holding an transferring assets created by the Taproot Assets Protocol. As Taproot Assets are built on top of the taproot itself, from the PoV of the taproot channel format, Taproot Assets manifests entirely as an extra tapscript sibling placed in the tapscript tee of relevant outputs. A set of asset-specific balances (in the form of taproot asset tree commitments) are maintained as an overlay layer on top of the normal initiator+responder balances of Lightning channels. For channel state transitions and eventual on-chain contract claims, in addition to normal taproot witnesses, a set of taproot asset level witnesses are also exchanged, encumbered by a nested iteration of the current Tapscript VM, the Taproot Assets VM.

In order to facilitate multi-hop payments of the existing LN using Taproot Assets edge liquidity, an RFQ (Request For Quote) last-mile negotiation scheme is used to lock in an exchange rate for both incoming and outgoing payments by liquidity providers. Tendered quotes (asset_id, volume, price) are identified by a cryptographic hash and scid-like sequence number, and ephemerally expire in order to reduce exchange rate risk. The existing BOLT 11 invoice format is used verbatim, in a manner that allows a receiver to accept an taproot asset without burdening the sender with up to date knowledge of exchange rates. As no effective changes to the invoice scheme re required, support for BOLT 12 invoices is readily available.

See also the TAP BIPs: bitcoin/bips#1489

Copy link

@guggero guggero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome read so far!
Checkpointing my review, will continue later.

I'm fixing typos directly in a branch (https://github.com/guggero/blips/tree/taproot-assets-blip-typos), feel free to pull in directly (to reduce the amount of nit related chatter on the PR).

blip-tap.md Outdated Show resolved Hide resolved
blip-tap.md Show resolved Hide resolved

The sending node:

* MUST send a new `tap_asset_proof` for each asset UTXO they wish to anchor in
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is simply for the case where the funding transaction would merge multiple asset inputs of the same asset ID into one? But the channel funding output would only commit to a single (merged) asset UTXO, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is simply for the case where the funding transaction would merge multiple asset inputs of the same asset ID into one?

Yeah, this is the phase where the initiator proves that the assets actually exist, and if they're merging N assets into a single UTXO. Correct that it'll still be a single merged UTXO.

This is also has a play for the eventual addition of dual funding as well.

Each `tap_asset_proof` declares an `asset_id`, an `amt`, and finally a
serialized existence proof for the Taproot Asset.

1. type: ?? (`tap_asset_proof`)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if there can be multiple asset inputs, would the sending node send multiple tap_asset_proof objects? Or should we add a num_proofs to this type to allow multiple proof files to be sent? Because the requirement of all sharing the same asset_id would be enforced by that.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I only now saw that tx_asset_proof is its own message not part of the open_channel message... What's the reason for that? Couldn't the open_channel message just contain a list of proofs? Or are there overall message size limits?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if there can be multiple asset inputs, would the sending node send multiple tap_asset_proof objects?

Yeah if we have 3 inputs of the same asset, we send 3 proofs. If we have 2 diff assets, we send two proofs, etc.

Or should we add a num_proofs to this type to allow multiple proof files to be sent?

I made them each a single message so we wouldn't have to worry about overflowing the default 65 KB message size. At least in the early days. The idea here is this is just the anchor proof of the final resting place, and not the entire history proof. Using this, then the verifier would use a universe to fetch the full history or up to w/e check point.

What's the reason for that? Couldn't the open_channel message just contain a list of proofs? Or are there overall message size limits?

I had them a distinct messages due to size limits, and also to minimize the changes needed for open_channel.


The receiving node:

* MUST verify the incoming partial signature against the constructed TAP
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to mention that the receiving node MUST check that all anchor transactions referenced by the proofs are actually present as inputs to the funding transaction?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in single funder land, the initiator never actually see the full funding transaction. This would be something done once the funding transaction has been confirmed, or if we modified things to just have the initiator send directly to the responder.

blip-tap.md Outdated Show resolved Hide resolved

1. Initialize an empty TAP asset tree dubbed `tap_asset_tree`

2. For each `tap_input_leaf`:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by for each tap_input_leaf here? Wouldn't there only be a single one to reduce complexity? Meaning that even if there are multiple inputs, they would be merged into a single asset level UTXO?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct that in the default case (only one UTXO in the TAP tree), we'd only need one. This is for the case of multiple assets in the funding output. If it's just a single asset, even if we send in multiple inputs, then correct that we'll only have a single item in the TAP tree of the funding output.

blip-tap.md Show resolved Hide resolved
Copy link

@guggero guggero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great stuff! I really love the elegance of the quote system and the SCID alias use. Really makes it easy to do exchange rates at either or both ends of a payment.

Will need a second pass to catch all the low-level details, but at least think I have a decent understanding of the flow and data exchanged to make this work.

or the highest element in the tree. The spec of `bip-tap.mediawiki` elaborates
on this.

The `tap_to_local_script_root` is itself, a nested instance of the _existing_
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds a bit confusing or unclear. You're meaning to say that tap_to_local_script_root is the root of two levels of MS-SMT trees with a leaf at the very bottom that has the same to_delay_script_root construction as its script_key, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean here is that we basically replicate all the scripts on the TAP layer. Tacked on an extra sentence to help clarify a bit.

blip-tap.md Outdated
2. data:
* [`32*byte`:`rfq_id`]
* [`BigSize`:`accepted_rate_tick]
* [`BigSize`:`expiry_seconds`]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need BigSize for seconds? Or does that just mean VarInt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's basically just a VarInt, but big-endian, as the Bitcoin varint is little-endian.

* [`32*byte`:`rfq_id`]
* [`32*byte`:`asset_id`]
* [`BigSize`:`asset_amt`]
* [`BigSize`:`suggested_rate_tick`]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to add the short channel ID of the channel to the quote request/response?
It could be that we have two USD backed channels, would the quote be valid for both?
Because on the last hop we only have this fake short channel ID that binds to the quote. But if there are multiple channels that could satisfy the quote, will the last hop just choose one of them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to add the short channel ID of the channel to the quote request/response?

Hmm, good question. I think for the MPP case, we'd want to treat the set of USD backed channels as a single unit? Since here we're concerned with accepting a certain price for a a given volume (amt) transferred.

Today we have "non-strict forwarding" by default, so the last hop can always choose which channels to actually send over, and this is the default implementation in the switch.

I think this is something we'll want to come back to though.

blip-tap.md Show resolved Hide resolved
blip-tap.md Outdated Show resolved Hide resolved
blip-tap.md Outdated Show resolved Hide resolved
@Roasbeef
Copy link
Contributor Author

Thanks for the review so far! Pushed up a fixup commit implementing changes/clarifications.

Copy link

@GeorgeTsagk GeorgeTsagk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great write-up. Spotted a bunch of typos, also have a few Qs 🥕

network routers with a new revenue source. One such potential asset includes
stablecoins, which at the time of writing have a market cap of nearly $100
billion. By enabling stablecoins to be sent and received at the edge of the
network, the utility of the Lightning Network increase, as LN effectively

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
network, the utility of the Lightning Network increase, as LN effectively
network, the utility of the Lightning Network increases, as LN effectively

A merkle-sum sparse merkle tree builds on the SMT data structure with the
addition of augmented leaves and branches. In addition to a key-location, and a
value, each leaf also contains a _sum value_. When creating the parent of two
leaf nodes, the _sum_ of the accumulator values for both leaf node is also

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
leaf nodes, the _sum_ of the accumulator values for both leaf node is also
leaf nodes, the _sum_ of the accumulator values for both leaf nodes is also

addition of augmented leaves and branches. In addition to a key-location, and a
value, each leaf also contains a _sum value_. When creating the parent of two
leaf nodes, the _sum_ of the accumulator values for both leaf node is also
included in the hash digest. This enables a prover to prove to a verify

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
included in the hash digest. This enables a prover to prove to a verify
included in the hash digest. This enables a prover to prove to a verifier

assets held by an output. A single taproot output may hold up to `2^256-1`
individual assets.

The Taproot Assets commitment is stored in _unique_ location within the

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The Taproot Assets commitment is stored in _unique_ location within the
The Taproot Assets commitment is stored in _unique_ locations within the


The asset TLV of a taproot asset includes a special `script_key` field. This
`script_key` is derived according to the rules defined in BIP-341 and 342. In
other woods, the initial version of the Taproot Assets VM is actually a

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
other woods, the initial version of the Taproot Assets VM is actually a
other words, the initial version of the Taproot Assets VM is actually a

`expiry_seconds` value

- MUST reject the entire HTLC set if at anytime, the sum of HTLCs (the
`amt_to_forward` field) targetting `tap_rfq_scid` eceeds the negotiated

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`amt_to_forward` field) targetting `tap_rfq_scid` eceeds the negotiated
`amt_to_forward` field) targetting `tap_rfq_scid` exceeds the negotiated

* `amt_to_forward = (9999999 / 10_000)`
* `amt_to_forward = 999.9`
* Note that all assets internally are accounted in a unit of a `tick`
(1/1000th) of an asset. When convering back to the main asset, the value

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(1/1000th) of an asset. When convering back to the main asset, the value
(1/1000th) of an asset. When converting back to the main asset, the value

blip-tap.md Outdated
passed on as an additional last-hop fee.

When creating an invoice, the creator MUST ensure that the invoice expiry value
is set exactly to the `expiry_seconds` value of the accepted RFQ.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense that both need to expire fast, but why on the exact same time?
e.g what if quote expired 10ms before invoice or vice versa

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What use is the quote if the invoice has expired? Same with the other way around.

Or is your idea that quotes can span multiple invoices? As is, they're considered to be 1:1

* `invoice_amt = 163305432 mSAT`
* `invoice_amt = 163305 SAT`

Always expressing the invoice amount in BTC/mSAT ensures that unpugraded

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Always expressing the invoice amount in BTC/mSAT ensures that unpugraded
Always expressing the invoice amount in BTC/mSAT ensures that unupgraded

senders will be able to send over these asset channels.


## Universality

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this section going to be about?

Copy link

@guggero guggero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some drive-by comments for stuff I noticed while implementing a mini-PoC.

* MUST store the received partial signature to later be able to broadcast a
force close transaction with the commitment transaction

#### `funding_accepted` Extensions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this called funding_signed?

* MUST store the received partial signature to later be able to broadcast a
force close transaction with the commitment transaction

#### Funding Output Construction
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we also need to send a next_local_nonce in the channel_ready message.

MUST send an `open_channel` message with the same `temporary_channel_id`, which
includes the new TLV extensions defined below:

1. `tlv_stream`: `open_channel_tlvs`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we also need next_local_nonce for each distinct asset ID that we're going to commit to the channel, because each asset ID will reside in its own leaf, meaning its own musig2 signing session.
Don't think it's safe to re-use the nonce we use at the BTC level.

includes the TAP asset root they arrive at. This allows the initiator to verify
that the responder has constructed the same asset root.

1. `tlv_stream`: `accept_channel_tlvs`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here re next_local_nonce per unique asset ID.

* `expiry_seconds` is the amount of seconds to use for the expiry of both the
quote and the invoice

* `rfq_sig` is a signature over the serialized contents of the message
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this signature is necessary. Is the purpose of this field to facilitate distributing the message among peers?

If it can, then it should send `tap_rfq_accept` that returns the quote amount
the edge node is willing to observe to move `N` units of asset `asset_id`:

1. type: ?? (`tap_req_accept`)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In choosing a type number we might want to consider doing the following:

Custom message types start at 32768. We can specify a taproot-assets specific offset from that starting number by concatenating the alphabetical index positions of the letters "t" (20), "a" (1), and "p" (16), which gives 20116. And then the tap offset is TapMessageTypeBaseOffset = 32768 + 20116.

Using that offset, the quote request message can have type TapMessageTypeBaseOffset + 0, quote accept with TapMessageTypeBaseOffset + 1, and quote reject with TapMessageTypeBaseOffset + 2.

What do you guys think?

* [`BigSize`:`expiry_seconds`]
* [`64*byte`:`rfq_sig`]

TODO(roasbeef): tlv err where?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we can include an optional error field in the reject response message?

Comment on lines +943 to +948
* `suggested_rate_tick` is the internal unit used for asset conversions. A tick
is 1/10000th of a currency unit. It gives us up to 4 decimal places of
precision (0.0001 or 0.01% or 1 bps). As an example, if the BTC/USD rate was
$61,234.95, then we multiply that by 10,000 to arrive at the `usd_rate_tick`:
`$61,234.95 * 10000 = 612,349,500`. To convert back to our normal rate, we
decide by `10,000` to arrive back at `$61,234.95`.
Copy link

@ffranr ffranr Jan 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think suggested_rate_tick should be renamed to suggested_scaled_exchange_rate. I think the word "tick" has a particular meaning in finance that confuses what we're trying to explain here.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use the word pip instead, for example:

`suggested_scaled_exchange_rate` is the internal representation used for asset
conversions. A pip, in this context, is 1/10000th of a currency unit, providing
up to 4 decimal places of precision (0.0001). For example, if the BTC/USD
exchange rate is $61,234.95, we multiply it by 10,000 to determine the
`scaled_exchange_rate`: `$61,234.95 * 10000 = 612,349,500`.  To convert back to
our normal rate, we divide by `10,000` to arrive back at `$61,234.95`.

Copy link

@ffranr ffranr Jan 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might also want to also consider adding a scaling exponent field (uint8) so that we can support variable precision. Otherwise it might be complicated to increase precision in a later release and maintain backwards compatibility.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alternative naming could be characteristic lightninglabs/taproot-assets#763

Copy link

@ffranr ffranr Feb 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dstadulis I think the characteristic discussion/idea is separate from the value stored in the suggested_rate_tick field.

As I understand it: the value in the suggested_rate_tick field is a (scaled) exchange rate. Whereas the characteristic concept comes in handy when the smallest unit of a tap stablecoin asset does not map exactly in value to the smallest unit of its fiat counter currency.

TLDR: we need an exchange rate for non-stablecoin tap assets.

blip-tap.md Outdated
2. data:
* [`32*byte`:`rfq_id`]
* [`BigSize`:`accepted_rate_tick]
* [`BigSize`:`expiry_seconds`]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't we're representing expiry optimally. Right now, expiry_seconds is the number of seconds until the quote expires. But if there's any kind of delay in sending/receiving the message, then that expiry will be interpreted inaccurately. Further, the message wouldn't be "self contained", we need to track the receive time somehow.

I think we should have a field called expiry_unix_timestamp with type uint64. It's value would be the unix timestamp after which the quote is no longer valid.

be rejected if the channel cannot accommodate the proposed volume, or if the
edge node is unwilling to carry any HTLCs for that `asset_id`.

1. type: ?? (`tap_req_accept`)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. type: ?? (`tap_req_accept`)
1. type: ?? (`tap_rfq_reject`)

1. type: ?? (`tap_req_accept`)
2. data:
* [`32*byte`:`rfq_id`]
* [`BigSize`:`accepted_rate_tick]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* [`BigSize`:`accepted_rate_tick]
* [`BigSize`:`accepted_rate_tick`]

If it can, then it should send `tap_rfq_accept` that returns the quote amount
the edge node is willing to observe to move `N` units of asset `asset_id`:

1. type: ?? (`tap_req_accept`)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. type: ?? (`tap_req_accept`)
1. type: ?? (`tap_rfq_accept`)

ffranr and others added 2 commits April 4, 2024 00:51
This commit changes the RFQ accept message's expiry field to represent a
Unix timestamp instead of a duration in seconds until expiration. The
prior approach, which used a relative time format, introduced potential
inaccuracies for clients based on message processing, forwarding, and
handling times. This modification enhances precision in expiry handling.
blip-29: use Unix timestamp for RFQ accepted quote expiry
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants