Skip to content

Building a TLV extension app - a field report #3407

@jarret

Description

@jarret

As a challenge, I (with some help from others) built https://onion.studio to explore how the createonion, sendonion and generally the TLV stuff can be used for building apps. Also, maybe help spread awareness of the associated concepts out there. The full code is all at: https://github.com/jarret/onionstudio

First, thanks to all of you in the C-Lightning project for building this stuff, exposing it to be used from the outside and performing the handful of public talks to explain it. Very cool stuff that I am excited to follow the future of.

Here are some of my thoughts and feedback about some of the bumpy parts I encountered in my arc of building. A lot of it is along the lines of "other app developers will likely have need for the same stuff I wrote, so shared plugin/platform code that makes these operations easier would be a benefit". This type of thing also might be premature to action on without additional feedback from others trying to build apps.

I hope this account is useful info to help refine going forward. I apologize in advance for verbosity:

The documentation for createonion refers to BOLT 4 for how to encode a payload. That was a rabbit hole for me to go down and I ended up having to implement the stuff in BOLT 1 in Python in order to build the TLV payloads myself.

This library is somewhat generalized such that it could be used by a different Python app and has four main modules - bigsize.py, tlv.py, namespace.py and hop_payload.py which build on each other (in that order).

For my app-specific extension TLVs, I extend a generated hop payload with my extension module.

When writing this, I was thinking about how C-Lightning has the invoice and decodepay commands which, in a way, go together as a pair for creating and decoding a BOLT 11. invoice also is able to prevent the creation of an incorrect BOLT 11 with the benefit of context. The net effect is that the application developer doesn't have to think much about the details of BOLT 11 encodings, so maybe that is similarly achievable for the BOLT 4 payload encodings.

SUGGESTION 1:
It might be good to provide an 'on rails' payload creator that can to all the TLV encoding/decoding according to spec with the main codebase such that encoders/decoders don't have to be written in N different languages. Also, it is imaginably straightforward to include optional parameters for extension TLVs to be appended after.

Since a) there is the hard cap of 1300 bytes for the payload + HMAC data in the onion packet and b) the data is all variably-length encoded for compactness and c) The quantity of hops to get to the destination (and back) can also be fluid, it makes it difficult to determine how much data is ultimately available in the packet for application extension data.

My solution for Onion Studio is a fudgy estimate starting point and a couple iteration of finding hops and attempting an encoding, and reducing the amount of payload pixels I encode until it fits under the 1300 byte limit.

Also, when playing with this stuff I hit the two crashing bugs: #3377 #3370 which were additional pain for trying to use the createonion command before I had a proper clue about what I was doing.

Overall, this was kind of gnarly to get done, but I now have some solution logic written that might be helpful to others.

SUGGESTION 2a:
Aside from fixing the crashes and giving nice error messages when you give wrong input, it might be good to provide a fuller-featured onion packet creator functionality that is more aware of the content and give you less rope to hang yourself with. Application-side fudgy estimates like mine are doomed to be less accurate and maintained than the internal code for creating onions doing validation, so it seems to make sense to provide a better service.

SUGGESTION 2b:
Perhaps in conjunction with SUGGESTION 1, the workflow for such an onion creator tool might have two phases. First, a phase for creating the routing instructions as desired since that is the most important thing. Second, the first phase might tell you how many 'spare' bytes are left for non-routing/extension concerns and then lets you fill them in.

Onion Studio's client sends the pixel data via a circular route and pays for the pixels by 'overpaying' the routing fee at that hop. The route creation uses getroute with a fuzzpercent setting of 0.0. It uses the fromid to compute the reverse route in the same way, which is can be identical to the outgoing route. It has to be deterministic simply because the algorithm need to hold the route relatively constant as it iterates on the onion construction to fit the pixel payload and appropriate payment using the rest of the space (and changing the payment amount also can influence the chosen route, changing the available space, changing the number of pixels in the payload, changing the payment, etc.).

This implementation trickiness resulted in me abandoning the desire to construct a 'good' circular route that would prefer the outgoing and returning route to be fuzzy and avoid node overlaps. The result is an implementation that is perhaps sub-par for the purpose of passing a maximally-discreet message.

The chosen circular scheme is also convenient for sending data without some out-of-band negotiation between the source in the destination. An alternative for a similar app might be the "Key Send" scheme to make it a unidirectional send of a payment + extension data. However, choosing between the two schemes for a particular style of app is a complex discussion that I don't have a clear formed opinion on.

However, I observe that the circular scheme might be useful for obscuring the real destination of a 'TLV RPC call' among the set of participants in the circular route.

Also, I observe that if there are schemes designed (I expect this to be inevitable, if not already happened) for 'TLV RPC calls' where there is a call out with a payload to a node, and a call back with a payload to the source, this might look exactly like a circular operation, even though it might be two different unidirectional sub-operations in implementation. Having both schemes around and supported might be a good platform for privacy for the variety of possible operations.

SUGGESTION 3:
A "get circular route" operation might be a good plugin/command to provide as a common utility. Also, given that the 'real' payment might be in the form of a forwarding fee somewhere along the route, it would be a nice feature if that amount could be optionally be selected as a criteria.

A noted fact of the extension TLVs is that they are delivered to their intended recipient before the preimage is revealed to trigger the associated payment. Onion Studio's design is such that the pixels don't draw until the payment is received.

On the application side, it has to catch the 'payload' field upon a call from the htlc_accepted hook notification and hold on to the data it until getting a forward_event notification. Also, the application needs to have a scheduled 'pruning' step to cull the stored payload data from forwarding HTLCs that never get paid.

SUGGESTION 4a:
Possibly consider a design for notifications to the plugin to give notification of the extension TLVs specifically along their lifecycle such that the application can get a notification upon 1) HTLC accepted 2) HTLC fulfilled or 3) HTLC expired. This way the extension TLV might not need to be held separately by the application

SUGGESTION 4b:
One (perhaps half-baked) thought is that there might be value in the protocol supporting encrypted extended TLVs that can only be decrypted with knowledge of the preimage. This would be such that the recipient doesn't know what the requested extension operation is until the payment is received. This would give sender the option to avoid revealing the content of request until the sender has definitively decided to go ahead with the payment/operation.

With Onion Studio, typically only ~250 encoded pixels fit in the onion packet. Unless they are tiny .png images, they have to be split up into many separate payments. For example, a 200x200 pixel image will require approximately 160 payments of 250 satoshis each totalling 40,000 satoshis to transmit all the pixels.

There is a challenge in estimating channel capacity for this kind of payment because it isn't quite "I wish to route 40k sats to the destination", it is "I wish to route many small payments totaling 40k sats to the destination which can go via different routes". Ideally, we would want to determine for the user whether it looks like that can realistically be done to completion before deciding to proceed. Right now, the approach of Onion Studio is to allow failure if there is a capacity exhaustion event midway, but gives the user the option to manually resume the transmission from the point it left off later. However, this might be frustrating user experience to have to babysit the operation and restart.

I imagine this is a similar problem for other data-publishing apps where it doesn't really deliver full value to only get a partial set of payments through to their destination.

SUGGESTION 5:
This probably needs a chunk of science done to figure out good algorithms for this, but I can perhaps spot a similarity between this and the routing considerations needed for AMP. In this specific concern, we want the full message transmission to be the concern for atomicity. It might make sense to include this problem for consideration in that discussion.

~

FIN

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions