-
Notifications
You must be signed in to change notification settings - Fork 47
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
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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).
|
||
The sending node: | ||
|
||
* MUST send a new `tap_asset_proof` for each asset UTXO they wish to anchor in |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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`) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
|
||
1. Initialize an empty TAP asset tree dubbed `tap_asset_tree` | ||
|
||
2. For each `tap_input_leaf`: |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this 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_ |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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`] |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
blip-tap.md
Outdated
* [`32*byte`:`rfq_id`] | ||
* [`32*byte`:`asset_id`] | ||
* [`BigSize`:`asset_amt`] | ||
* [`BigSize`:`suggested_rate_tick`] |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Thanks for the review so far! Pushed up a fixup commit implementing changes/clarifications. |
There was a problem hiding this 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
blip-tap.md
Outdated
`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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
`amt_to_forward` field) targetting `tap_rfq_scid` eceeds the negotiated | |
`amt_to_forward` field) targetting `tap_rfq_scid` exceeds the negotiated |
blip-tap.md
Outdated
* `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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
blip-tap.md
Outdated
* `invoice_amt = 163305432 mSAT` | ||
* `invoice_amt = 163305 SAT` | ||
|
||
Always expressing the invoice amount in BTC/mSAT ensures that unpugraded |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this 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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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` |
There was a problem hiding this comment.
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` |
There was a problem hiding this comment.
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.
blip-tap.md
Outdated
* `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 |
There was a problem hiding this comment.
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?
blip-tap.md
Outdated
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`) |
There was a problem hiding this comment.
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?
blip-tap.md
Outdated
* [`BigSize`:`expiry_seconds`] | ||
* [`64*byte`:`rfq_sig`] | ||
|
||
TODO(roasbeef): tlv err where? |
There was a problem hiding this comment.
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?
blip-tap.md
Outdated
* `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`. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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`.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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`] |
There was a problem hiding this comment.
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.
blip-tap.md
Outdated
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`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. type: ?? (`tap_req_accept`) | |
1. type: ?? (`tap_rfq_reject`) |
blip-tap.md
Outdated
1. type: ?? (`tap_req_accept`) | ||
2. data: | ||
* [`32*byte`:`rfq_id`] | ||
* [`BigSize`:`accepted_rate_tick] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* [`BigSize`:`accepted_rate_tick] | |
* [`BigSize`:`accepted_rate_tick`] |
blip-tap.md
Outdated
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`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. type: ?? (`tap_req_accept`) | |
1. type: ?? (`tap_rfq_accept`) |
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
This commit introduces several changes to the RFQ p2p messages. This includes version fields, group key asset specifiers, in/out assets, max volume, replacing rate tick with fixed-point, and adding a transfer type field. This commit also modifies other RFQ sections such that they are in-line with these new RFQ message fields.
blip-29: update RFQ messages
Update fixed-point wire encoding/decoding to represent the coefficient as a variable-length byte sequence. This change enables support for encoding/decoding `big.Int` sized integers.
blip-29: use variable-length bytes for fixed-point coefficient encoding
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