Neutrino payments


TLDR; – Neutrino payments (ν-payments) is a “Lightning-style” protocol of zero-fee Universa-guaranteed microtransactions, requiring the (regular Universa) fee only to establish and to close the series of micropayments.

Abstract

Reading this article you’ll refresh your knowledge about the Lightning Network for Bitcoin (its benefits for Bitcoin as well as its basic architecture),.. and find out how easy it is to implement the concepts of Lightning Network on top of Universa smart contracts, without writing even a single line of code.

Lightning Network benefits

A well-known recent improvement to Bitcoin network is the Lightning Network, the off-chain micropayments extension that utilizes the legacy data and features of core Bitcoin network, but builds its own, not (immediately) blockchain-registered methods of value transfer, trying to compensate for many existing Bitcoin flaws:

  • Bitcoin network broadcast may be slow, and it may take a long time before the receiver even knows a value transfer has even been attempted – Lightning Network uses direct peer-to-peer connection which provides some (practically sufficient) guarantees of transfer execution, and provides almost immediate notification.
  • Bitcoin network may take unpredictable long time to accept the transaction into some block. Of course you can increase the transaction fee in a desperate hope for it to be included into a mined block earlier; but that’s a different question. Without going too generous, it may take minutes, hours, even days to mine it. On the other hand, in a Lightning Network, the Bitcoin amount locked in the Lightning engine can be spent virtually immediately.
  • As mentioned above, the speed of getting the transaction registered depends a lot on the “generosity” of the transaction sender. Ultimately this ends up in high transaction costs, from dozens of cents and sometimes even reaching some dollars/euros for a single transaction. On the other hand, the transaction in Lightning Network, not being registered in the main blockchain (and reaching the blockchain later, during finalizing a number – probably a larger one – of transactions), doesn’t require the Bitcoin-level fees (it still has some fees, but rather tiny). This makes Lightning Network a true “microtransaction” framework, ready for lots of tiny payments without dealing with expensive fees for them.
  • The transactions in Bitcoin network are eventually consistent rather than finally consistent; some blocks which you may have considered “accepted”, may actually be “rolled back” from the blockchain and become orphan, if the network finds “a longer chain” not including them. Therefore even after seeing a block containing some transaction, people may want to wait for a while, for several “confirmations” (i.e. a number of blocks mined after the block containing the transaction in question) until considering the transaction accepted. With the Lightning Network, even though the transaction data is not registered in the primary blockchain, each microtransaction itself can still be considered “done” and accepted immediately.

Of course, many of these flaws just don’t exist in Universa network:

  • The Universa network is significantly faster, with more than 20 000 TPS (comparing to 15 TPS of Bitcoin), and usually sub-second time of transaction broadcasting over the whole network. And this sub-second time is not just for the transaction being broadcasted, but even for the transaction being recorded in the distributed ledger!
  • The transactions in Universa are ultimately consistent, so a concept of “orphan blocks” or “number of confirmations” is not applicable to Universa; if you have received a final payment to you, it won’t be rolled back by the network.
  • High transaction fees are not so much of a problem with Universa, either; the resource usage quantization is tuned in such a way that a typical payment transaction needs just 1 U for processing; and the cost of U is bound to €0.01; making the Universa network one of the cheapest networks, with a cost of €0.01 for a typical token transfer transaction. Nevertheless, the microtransactions could still be an extremely interesting case, e.g. for scenarios when some low-cost service is provided with the payment being taken on the fly (even to the point of some candies being bought by weight, with every extra gram or ounce of candies being immediately paid).

So, let’s construct a micropayment framework over Universa. To distinguish the Universa implementation from Bitcoin one, let’s call it… say, “neutrino payments” – cause neutrinos are incredibly fast and incredibly lightweight, and they can traverse the whole Universe(a), so why not? Here come the neutrino payments.

Neutrino payments: architecture

Imagine Alice and Bob.

Alice is a prominent video blogger and streamer on Twitch; during the stream, she accepts donations – they are usually tiny in amount, but there may be a number of them from the same viewer, to encourage her to stream more and longer.

Bob is a dedicated fan and viewer of Alice’s streams, and donates her regularly, being sure that his donations are important for her providing the quality contents. He doesn’t like to donate more than €10 per stream, though; but each donation may be from €0.01 up to even €0.5.

When Alice starts another stream, Bob establishes a Lightning Network channel with Alice. This is basically just a multi-signature “wallet” controlled by the keys of both Alice and Bob. At this moment, Bob registers it in the Bitcoin blockchain (that’s one of just two transactions with the Bitcoin network; all the other operations won’t require updating the Bitcoin blockchain), so the network knows some funds are “locked down” in this channel. In Bob’s case, he locks down just €10 amount of his cryptocurrency, for future Lightning Network payments to Alice.

Whenever Bob makes a donation to Alice, this happens actually over the Lightning Network only. He submits a new transaction with a new distribution of funds in the multisig wallet of the Lightning Network channel, signed with just his own key. First it is a “€0.01 to Alice, €9.99 to Bob”, then that is “€0.02 to Alice, €9.98 to Bob” and so on.

As the €10 balance is locked in the channel already (and that is recorded in the Bitcoin blockchain), Alice knows the funds are “confirmed”. And seeing a transaction signed by Bob, she may be sure the funds in this transaction are already “hers” (the funds are locked; the transaction is signed by 1 key of the two, now it is only her who needs to sign it to receive them fully; nothing can go wrong here). If the donations on her stream involve any paid “fan service” for those who donate, she could consider the payment happened, even though it is not – yet – registered in Bitcoin blockchain.

When the channel being closed, Alice just signs the last received transaction with her own key, completing the multi-sig requirements. And then the payment is registered in the Bitcoin network, being the second of just the two real network transactions and completing the “micropayments sequence”. None of the intermediate transactions involved any cost to either Alice or Bob.

Architecture: Universa network way

Preliminary architecture

Can the scenario above be executed in Universa? Sure, and so trivially that the introduction before may be larger than the actual Universa’s implementation as a smart contract!

Each smart contract in Universa is a structured document, containing some mandatory fields with one of them being “state.owner”. state.owner field is usually assigned a SimpleRole containing just the key address of the current owner. So “sending” any token usually means just “reassigning” the owner field of the underlying smart contract (potentially, with splitting some smart contract in multiple, having the same total amount; or joining multiple compatible contracts, similarly keeping the total amount invariant).

How to make a “multi-signature wallet” in Universa then? Easy. The owner (state.owner) is reassigned not to a SimpleRole, but to a ListRole containing multiple key addresses. Each ListRole is created in one of multiple modes, like Mode.ALL or Mode.ANY.

“Establishing a channel”

To emulate the Lightning Network channel, Bob should make a new revision of his €10-worth token contract, where the new state.owner is assigned with a ListRole in Mode.ALL, and put there two key addresses: of Bob’s key and of Alice’s key. After this he registers this ownership change in the Universa network,.. yes, this is the 1st of the 2 transactions touching the blockchain; and this is equivalent to “establishing a channel” in real Bitcoin Lightning network:

...
state: 
  owner: 
    mode: ALL
    name: owner
    roles: 
      - alice: 
          name: Alice
          type: simple
          addresses: 
            - __type: KeyAddress
              uaddress: 
                __type: binary
                base58: "<base58 of Alice’s key address>"
      - bob: 
          name: Bob
          type: simple
          addresses: 
            - __type: KeyAddress
              uaddress: 
                __type: binary
                base58: "<base58 of Bob’s key address>"
    type: list
...

Note he doesn’t create any new smart contract. He doesn’t need the original smart contract of his token be prepared somehow. Any regular “token”-style smart contract (and not even just a token; but more on this later) in Universa will support it, as the only what happens with it, in terms of Universa, is ownership change!

Micropayments

Similarly to the Lightning network transfers, after the initial revision of the contract all the subsequent mitransactions happen off-chain.

When Bob wants to “make a donation micropayment” to Alice, he takes the registered contract revision, and creates a new transaction pack. The transaction pack contains new and revoked sections; in the revoked section, he puts the registered contract revision. In the section, he puts two new revision of this smart contract. The sum of “amounts” in both revisions should be equal to the amount field in the original token; one revision will contain the amount being “donated” to Alice (like, “0.01”), and the other revision will contain the amount remaining in Bob’s ownership (like, “9.99”).

Then he signs this transaction pack with his own key, and sends it to Alice via any off-chain method.

But what Bob doesn’t do, contrary to regular Universa usage, is he doesn’t attempt to register this contract in Universa network. He could not do it anyway (do you remember that the current owner is ListRole(mode=ALL, parties=(Alice, Bob))? – this syntax is high-level and not reflects the real smart contract syntax), as the signature from Alice is needed for contract to be registered. But he doesn’t wait for Alice signing her document either,.. he just sits and watches the Alice’s stream further.

Alice, in her turn, doesn’t sign the document immediately. She does accept the donation though; for her, it is basically happened. If she checks the existing token contract (the one with the amount=10 and owner being a ListRole), she’ll see it is in APPROVED state (so, “the money are locked in the Lightning channel”). She sees the ownership in one of the contracts being changed to herself, indefinitely; and Bob signed this transaction pack. The only remaining is her signature, nothing can go wrong – it is a real donation just waiting for her signature to be finalized.

...

As the Twitch stream goes, Bob decides to make one more donation. He just rebuilds the same transaction pack, but now with the new amount donated to Alice (now it will be “0.02” probably), and the new remainder in his ownership (9.98). He signs the transaction pack again, and sends it to Alice (again, via any direct p2p connection, without touching the Universa network).

Alice, seeing a new transaction pack from Bob, compares it with the previous one received from him. She sees a new ownership token contract owned by her key address, and the amount increased since the previous Bob’s operation. She notices it has increased by 0.01, and, happily, publicly announces on her Twitch stream that a new donation, 0.01, has been received from her regular fan, Bob. Bob, you are the best!

Again, she doesn’t need to sign a new transaction pack yet, she just keeps it for longer, waiting if Bob wants to make another microdonation.

...

One may wonder, what happens if Bob, in one of his next “microtransactions”, decreases the amount donated to Alice? Like, the previous amount donated to Alice was 1.25, and in the next transaction pack signed by him, he gives her just 1.1? Yes he can make such a transaction pack; but Alice already has a better transaction pack, and it is signed by Bob already (and just waiting for her own signature). So she just ignores any new transaction pack which is worse (for her) than what she has got already. No “negative microdonation” is possible, she just waits until Bob is in the better mood, and his donation is higher than 1.25.

“Closing the channel”

As soon as the Twitch stream is over – or when Alice and Bob mutually decide the channel could be closed; like, when Bob announces he is not going to donate to Alice in this stream for a while while – Alice gets the latest (and the best) transaction pack, containing the new splitting of token ownership between herself and Bob. Then she signs it with her own key, and registers in in Universa Mainnet (as, with the previous Bob's signature, the transaction pack has now everything to register it properly). This is the 2nd of the two Universa blockchain transactions, the final one.

Since this moment, the actual token transfer has occurred and registered. Only two Universa network registrations (think about it as “€0.02 spent”) were required, no matter how many – dozens, hundreds, thousands – of intermediate microtransactions occured.

Real-world architecture

For real implementation, there are several more considerations to be done.

Uncooperative Alice: handling channel timeouts

After “opening the channel”, the native “state.owner = ListRole(mode=ALL, parties=(Alice, Bob))” approach would be rather dangerous for Bob. What happens if, during the Twitch stream, Bob has donated about €0.05 of token value, and should receive €9.95 of remaining tokens; but Alice doesn’t sign the transaction pack at all? She won’t receive her own tokens, of course; but the €9.95 Bob’s tokens are “held to ransom” by her, until she signs the transaction pack.

Such an “uncooperative Alice” behaviour should be avoided; luckily, we can update our procedure very easily, to restrict her from blocking the funds:

When “making a channel”, that is, setting the owner of a token to the ALL-mode ListRole of Alice’s and Bob’s key, we should, in fact, consider two different scenarios. One is “regular flow”, when one of the roles is ListRole(mode=ALL, parties=(Alice, Bob)). Another scenario should handle the situation of “uncooperative Alice”, and allow Bob to complete the transaction on his own, if Alice hasn’t completed her part during some time.

What do we mean by “hasn’t completed her part”? Most obvious approach is to consider the “Lightning channel” be built for some specific duration, like “4 hours” (but the time may vary and be negotiable between Alice and Bob); so if Alice hasn’t completed the interaction and not signed the transaction pack, after 4 hours from the start (from creation of the channel) Bob can control the token single-handedly again.

High-level overview

How to implement this? Very high-level view (not the actual syntax) would look like this. Instead of:

state.owner = ListRole(mode=ALL, parties=(
  SimpleRole(Alice),
  SimpleRole(Bob)
))

, Bob should create a new revision like this:

state.owner = ListRole(mode=ANY, parties=
  ListRole(mode=ALL, parties=(
    SimpleRole(Alice),
    SimpleRole(Bob)
  ),
  SimpleRole(Bob, reference="now > this.transactional.data.cancelableSince")
)

and, in transactional.data.cancelableSince, he should put the actual time of the “channel timeout”; in our case, the time of transaction creation plus 4 hours.

Implementation

How would the real implementation look? Let’s describe it in uniclient-compatible YAML form. For simplicity, let’s think the channel starts on 12.03.2020, 12:34 UTC, and is planned to last for 4 hours.

  1. In a new revision of the contract, which acts as a “Lightning channel” (locking the funds), in its transactional section, let’s create the constant containing till what moment the channel should stay alive (or, alternately thinking – since when the channel may be canceled):

    transactional:
      data:
        cancelableSince: "2020-03-12 16:34:00" # 12:34 is start time; 16:34 is start time + 4 hours
    

    By the way – why transactional section, rather than (more “popular”) definition or state one? Easy: because you cannot edit the definition section after the initial creation; and for the state section, people may or may not have sufficient rights (defined by the contract permissions) to edit the particular fields; but for the transactional section, people may edit it because it is guaranteed to survive only for the nearest transaction.

  2. Let’s explicitly create a new reference. It will be used for the nearest transaction only, hence it is created in the transactional section as well:

    transactional:
      references:
        - reference:
            name: lightning_channel_cancelable
            where:
              all_of:
                - now > this.transactional.data.cancelableSince::ZonedDateTime
    

    Note that this reference will be tested not when you set for the new owner (i.e. on the “lightning channel creation”), but when the actual ownership (containing this reference) is tried, i.e. on the “lightning channel close”.

  3. As the new owner of the contract, you set the combination of ANY-mode ListRole and ALL-mode ListRole, as described above:

    state:
      owner:
        type: list  # or __type: ListRole
        name: owner
        mode: ANY
        roles:
          - lightning_channel:
              name: 'Lightning channel'
              type: list
              mode: ALL 
              roles:
                - roleAlice:
                    name: 'Alice'
                    type: simple  # or __type: SimpleRole
                    addresses:
                      - __type: KeyAddress
                        uaddress:
                          __type: binary
                          base58: <base58 of Alice’s key address>
                - roleBob:
                    name: 'Bob'
                    type: simple
                    addresses:
                      - __type: KeyAddress
                        uaddress:
                          __type: binary
                          base58: <base58 of Bob’s key address>
          - uncooperativeAlice:
              name: 'Uncooperative Alice'
              type: simple
              addresses:
                - __type: KeyAddress
                  uaddress:
                    __type: binary
                    base58: <base58 of Bob’s key address>
              required:  # here goes the timeout-defining reference
                ALL_OF:
                  - lightning_channel_cancelable
    

That is it! Such smart contract should already be sufficient to prevent any obstructions Alice may impose.

Transaction fees

Another topic to discuss is: the fees for “establishing the channel” will definitely be paid by Bob, as it is him who is controlling the tokens at that moment. But who should pay the fees for closing the channel?

The most expected answer is: Alice should. She is the last who signs the transaction pack; so she takes the final pack, ready to be sent to Universa, and obviously registers it.

Of course, she may send it back to Bob, expecting him to pay the fees and register it. But in such case, considering the “uncooperative Alice” timeout (defined and described above), Bob will be tempted to not register it during the channel lifetime. Why he should do it, if he can wait for the channel timeout to happen, get back the sole control of the token, and cancel all the donations – like if Alice has never signed it?

So it seems that Alice is doomed to spend her own UTNs/Us to actually register the transaction, if she wants the donation… is she?

Actually, no. It is possible to enhance this protocol in such a way that Alice will receive the microdonations, without spending a dime.

For it to happen, Bob must send not just the transaction pack containing the new revisions of the contracts, the previous revision being revoked, and his signature. He must sent the actual prepaid parcel which includes both his U fee (and his signature permitting the fee to be paid), and the still-incomplete transaction pack. Alice, when receives the microdonation (the new parcel), should check that this is indeed the prepaid parcel, with a sufficient fee included, and that the transaction pack inside is formed properly as she expects.

Some future enhancements of Universa smart contracts may (or may not) disable the “prepaid parcels” where the payment is not tied to the sealed transaction pack; but for now it is a valid scenario.

Caching the contracts

Some caution should be taken when following this protocol though. Whenever Bob sends any new transaction pack to Alice, he must keep in mind that she has all the transaction packs he sent her before. For example, in our scenario, if he sent three €0.01-worth donations to her, this means he actually sent her three different transaction packs (or parcels):

  • {alice: owns 0.01, bob: owns 9.99}
  • {alice: owns 0.02, bob: owns 9.98}
  • {alice: owns 0.03, bob: owns 9.97}

It is reasonably expected that after this protocol, Alice registers the third pack (most profitable to her). The same transaction pack stored at Bob’s premises will automatically become APPROVED.

But nevertheless, Alice keeps all the transaction packs; so until “the channel is closed”, Bob should keep all the transaction packs too. If Bob keeps only the latest transaction pack (where he owns 9.97), then, if Alice (for some reason, malicious or unintended) registers another transaction pack of these series, the Bob’s transaction pack won’t become APPROVED, and Bob will lose access to his remaining tokens.

So, Bob should keep all the transaction packs sent to Alice; and, after the channel is closed by her, he should test all of them (from the most profitable to her, to the least profitable), to find which one has she registered.

Storing the signatures

Most untrivial question is how the signatures should be managed. Why is it? Because, if managed improperly, Bob may sign the transaction pack (e.g. donating 0.03, leaving 9.97 to himself); but, after passing the transaction pack to Alice, Alice signs it (and doesn’t return the new transaction pack to Bob) in such a way that the ID of the Bob’s owned contract (9.97) is irreversibly changed, and Bob doesn’t know it (and doesn’t have the valid/APPROVED contract with 9.97) anymore; so Alice get’s her money but Bob loses his stake.

The primary fact to remember is: in each transaction pack, there is a “primary” contract (or just “the contract”) which is being signed; and also several other contracts stored as “sub-items”. You normally don’t need to sign each and every contract from the transaction pack; signing just the primary contract is sufficient for the signatures to be applicable to the “sub-items” as well.

Note that during the signing, the HashID of the contract changes (it is calculated upon on the contents of the contract, including its signatures)! So if Bob had a contract where he was owning 10.00 tokens (let’s call this contract Bob_had_10_00), and creates two new contracts, “0.01 for Alice” and (the remaining) 9.99 for himself (let’s call these contracts Alice_has_0_01 and Bob_has_9_99), revoking the previous Bob_had_10_00 contract – the structure may (but should not) look like this:

{
    "contract": Bob_has_9_99,
    "subItems": [Bob_had_10_00, Alice_has_0_01],
    "references": []
}

Why “should not”? Because Bob then will sign the Bob_has_9_99 contract; so its hash is calculated like HashId( Bob_has_9_99.signed_by(Bob) ); and he now has the binary body of the contract Bob_has_9_99.signed_by(Bob)). This signature is applied for all sub-items. So if the owner rule of Bob_had_10_00 requires the contract to be signed by both Alice and Bob – after giving out this transaction pack to Alice, she may just sign the same Bob_has_9_99 contract, with the following consequences:

  1. its hash is changed to HashId( Bob_has_9_99.signed_by(Bob, Alice) );
  2. the contracts with the hashes HashId( Bob_has_9_99.signed_by(Bob, Alice) ) and HashID( Alice_has_0_01 ) are now APPROVED; the contract with the hash Bob_had_10_00 is now revoked;
  3. Alice now has the bodies of all three contracts, including HashId( Bob_has_9_99.signed_by(Bob, Alice) ) (as she was the last to sign). She can control the tokens owned by her.
  4. but Bob doesn’t have the body of the latest contract with his possessions. He does have the body of Bob_has_9_99.signed_by(Bob); but after signing by Alice, its HashID changed, and Bob doesn’t have the matching contract anymore.

How to not fall in this trap?

1. Choose the “primary” contract well

Usually, the simplest mnemonics to remember is: for a transaction pack, when it may have multiple contracts (some of them being given out, and some of them expected to be owned by you), you should choose as a “primary” contract (and sign) the contract which you plan to give out anyway.

Imagine a different structure of the transaction pack:

{
    "contract": Alice_has_0_01,
    "subItems": [Bob_has_9_99, Bob_had_10_00],
    "references": []
}

First, Bob signs the main contract, and makes it into Alice_has_0_01.signed_by(Bob). Now its hash is HashID( Alice_has_0_01.signed_by(Bob) ). Note that IDs of the other two contracts, Bob_had_10_00 (being revoked) and Bob_has_9_99 (being added), still haven’t changed (as they were not signed directly).

Then, he gives out the whole transaction pack to Alice. She signs the primary contract, and now it becomes Alice_has_0_01.signed_by(Bob, Alice) (and HashID is calculated from it since then).

After registering such a transaction pack, Alice has everything she needs to control her tokens; and Bob still has the originally built Bob_has_9_99, not altered with any signatures directly. Both of them have all their belongings, registered in Universa blockchain and approved. That’s fine!

You may wonder though: what if Alice signs both Alice_has_0_01.signed_by(Bob), and Bob_has_9_99 – just to make life harder for Bob? Interestingly she cannot affect him: internally, the primary contract (itself, not just the transaction pack) contains the hashes of all the contracts registered with it together, in its “new[]” section. As soon as Alice adds a unnecessary signature to Bob_has_9_99 – this is no more properly referred from Alice_has_0_01.new[]; and she cannot alter the reference in Alice_has_0_01.new[], without breaking the Bob’s signature on Alice_has_0_01!

2. Signing the “compound” contract

Another useful feature of Universa API is “compound contracts”, useful for make even harder and longer sequences of registrations into a single batch, registered atomically. Signing this “compound contract”, you automatically apply the signature to all its parts (this is used for such contract workflows, as, e.g., escrow). If convenient, the compound contracts can be utilized as well for easier dealing with the signatures.

Potential expansions and extra usage in Universa

The ideas above show how easy is to fit all the Lightning network concepts to the regular smart contract syntax. But actually, the syntax allows a lot of expansions to this protocol.

Closing conditions

The current protocol assumes that Bob always creates the channel, and Alice always (besides the “uncooperative Alice” case) closes it. But is that necessary? What is Bob doesn’t want Alice to be able to single-handedly cut the channel, but would prefer to keep it open as long as he is in the “donating mood”?

For such case, various conditions may be easily implemented to choose, when – or how – can Alice close the channel.

Closing flag

For example, Bob can use some extra field in transactional.data to signal Alice that he is done with the donation and expects her to close the channel. Just set some field like

transactional:
  data:
    lightning_channel_ready_to_close: true

make an according reference,

transactional:
  references:
    - reference:
        name: lightning_channel_closable
        where:
          all_of:
            - lightning_channel_ready_to_close::Boolean

and, in the appropriate part of the owner definition, add a reference, like this:

state:
  owner:
    ...
        roles:
          - roleAlice:
              type: simple  # or __type: SimpleRole
              name: 'Alice'
              addresses:
                - __type: KeyAddress
                  uaddress:
                    __type: binary
                    base58: <base58 of Alice’s key address>
              required:  # here goes the channel-closing reference
                ALL_OF:
                  - lightning_channel_closable

Closing period

Alternatively, the parties may negotiate that the channel lasts 3 hours instead of 4; after 4 hours have elapsed, the “uncooperative Alice” scenario fires; but between 3 and 4 hours marks is the only period when Alice may sign the contract. So the time line will be like this:

  • 12.03.2020, 12:34 UTC – the channel starts; Bob can donate, Alice cannot close the channel;
  • 12.03.2020, 15:34 UTC – the period of channel closing starts; Bob still can donate, but Alice can now close the channel;
  • 12.03.2020, 16:34 UTC – the channel is assumed closed; if Alice still hasn’t closed the channel, Bob can now close it single-handedly (and ever revert the payments).

The smart contract definition is also simple. Enhance the transactional.data like this:

transactional:
  data:
    closableSince: "2020-03-12 15:34:00" # 12:34 is start time; 15:34 is start time + 3 hours
    # cancelable period is configured as we’ve mentioned above already;
    # putting this data here only for clarity
    cancelableSince: "2020-03-12 16:34:00" # 12:34 is start time; 16:34 is start time + 4 hours
...    
  references:
    - reference:
        name: lightning_channel_closable
        where:
          all_of:
            - now > this.transactional.data.closableSince::ZonedDateTime
    - reference:
        name: lightning_channel_cancelable
        where:
          all_of:
            - now > this.transactional.data.cancelableSince::ZonedDateTime

And, in the Alice’s section of the owner definition, write the reference like this:

state:
  owner:
    ...
        roles:
          - roleAlice:
              type: simple  # or __type: SimpleRole
              name: 'Alice'
              addresses:
                - __type: KeyAddress
                  uaddress:
                    base58: <base58 of Alice’s key address>
                    __type: binary
              required:  # here goes the channel-closing reference
                ALL_OF:
                  - lightning_channel_closable

So, since 15:34 the “channel” is closable by Alice, and since 16:34, the “channel” is cancelable by Bob (as per the role definition already described before).

Multi-party

But is it necessary for this protocol contain one-on-one payments only? Actually no. It is rather easy to enhance this system for Bob to donate to multiple parties: like Alice, Ann, Amanda, Alex, Andrew and Anthony.

One thing to worry though is that all the receivers should be sure to receive the very same transaction pack – so some multi-party channel is required to ensure that.

Another obvious question is who, and how, is allowed to close the channel. Some closing condition (as described above) should be imposed, to prevent receivers to interfere with each other.

And final, most interesting topic, is: how to prevent any of them to “spoil the party”, registering the transaction pack that deprives other parties of the donation? For this case, actually, all the receiver parties can be joined into a single ListRole with mode=Quorum, so it will be not a single receiving party who closes the channel, but a consensus of them!

Let’s see an example of this, with:

  • 6 receiver parties (Alice, Ann, Amanda, Alex, Andrew and Anthony);
  • quorum of (at least) 4 of them required to choose how to close the channel;
  • “closable” period starting at 15:34 and
  • “cancelable” period starting at 16:34.
state:
  owner:
    type: list
    name: owner
    mode: ANY
    roles:
      - lightning_channel:
          type: list
          name: 'Lightning channel'
          mode: ALL
          required:
            ALL_OF:
              - lightning_channel_closable
          roles:
            - quorum_of_receivers:
                type: list
                name: 'quorum of receivers'
                mode: QUORUM
                quorumSize: 4
                roles:
                  - alice:
                      type: simple
                      name: 'Alice'
                      addresses:
                        - __type: KeyAddress
                          uaddress:
                            __type: binary
                            base58: <base58 of Alice’s key address>
                  - ann:
                      type: simple
                      name: 'Ann'
                      addresses:
                        - __type: KeyAddress
                          uaddress:
                            __type: binary
                            base58: <base58 of Ann’s key address>
                  - amanda:
                      type: simple
                      name: 'Amanda'
                      addresses:
                        - __type: KeyAddress
                          uaddress:
                            __type: binary
                            base58: <base58 of Amanda’s key address>
                  - alex:
                      type: simple
                      name: 'Alex'
                      addresses:
                        - __type: KeyAddress
                          uaddress:
                            __type: binary
                            base58: <base58 of Alex’s key address>
                  - andrew:
                      type: simple
                      name: 'Andrew'
                      addresses:
                        - __type: KeyAddress
                          uaddress:
                            __type: binary
                            base58: <base58 of Andrew’s key address>
                  - anthony:
                      type: simple
                      name: 'Anthony'
                      addresses:
                        - __type: KeyAddress
                          uaddress:
                            __type: binary
                            base58: <base58 of Anthony’s key address>
            - bob:
                type: simple
                name: 'Bob'
                addresses:
                  - __type: KeyAddress
                    uaddress:
                      __type: binary
                      base58: <base58 of Bob’s key address>
      - uncooperative_alice:
          type: simple
          name: 'Uncooperative Alice'
          addresses:
            - __type: KeyAddress
              uaddress:
                __type: binary
                base58: <base58 of Bob’s key address>
          required:
            ALL_OF:
              - lightning_channel_cancelable
...
transactional:
  data:
    closableSince: "2020-03-12 15:34:00" # 12:34 is start time; 15:34 is start time + 3 hours
    cancelableSince: "2020-03-12 16:34:00" # 12:34 is start time; 16:34 is start time + 4 hours
  references:
    - reference:
        name: lightning_channel_closable
        where:
          all_of:
            - now > this.transactional.data.closableSince::ZonedDateTime
    - reference:
        name: lightning_channel_cancelable
        where:
          all_of:
            - now > this.transactional.data.cancelableSince::ZonedDateTime

Any contracts

The original Lightning network was assumed to be used for value transfers, i.e. payments. Universa network though is definitely more than that, it can support many kinds of “Ricardian-style” smart contracts, rather than just the tokens. Can this “Lightning-over-Universa network” be used for non-token purposes?

It definitely can. Basically, the process above is just the regular “drafting of the smart contract”, which assumes that it happens off-chain. All the parties put their edits and sign them, until everybody is satisfied by the current edits and signed the current state; then the contract is registerable. Some ideas above, though (like, the conditional ownership, or the ownership restricted by time/flags) may be useful for your scenarios if you want some complex logic for any, token or non-token, contracts.

Wildcard receiver channel

The procedure above assumes that when Bob establishes the channel, he already knows the other party of the expected payment, as he writes the key address of Alice to the “channel revision” of the token contracts. But may he create a “wildcard channel”, where the other side is unknown at the moment of channel creation?

The purpose of this may seem unclear from the first sight; but imagine the following scenario: there is a hardware mobile wallet (with secure key storage) which, when connected to the secure PC/mobile phone, can access the Universa network and perform such network operations as registering the contracts (and thus “opening the channel”). And when it is working as a standalone device, it can use NFC to negotiate payments with other POS-terminals, sign the transaction packs (“micropayments over the Lightning channel”) and pass them over NFC to the POS terminal (which, in its turn, has the Internet access and can both read the Universa blockchain states and close the channel).

With such a scenario, the user, being at the security of his home, could pre-open some channels using the Internet-connected hardware wallet. For example, 20 (or so) of channels, each limited with €10 amount, total €200 worth. Then he leaves his computer/Internet-connected mobile phone at home, and chooses to have a walk in the city center. With such a wallet, he will be able to make up to 20 purchases, up to €10 each; or, if the payments are aggregated, there may be less number of purchases with more amount paid.

The only risk factor to consider, with such a “wildcard receiver channels”, is that each payment receiver should immediately check in the Mainnet that the channel (which user tries to open) is valid: the tokens in the channel are not bound to the receiver and can be double-spent for any other receiver at any moment, until the channel is closed. But still, with these limitations in mind, it is also a possible scenario. The Smart contracts – best practices article in Universa KB has an explanation how to make a SimpleRole which is never satisfied, or (more important for our scenario) which is always satisfied (TLDR: make a SimpleRole with no keys but a bound always-satisfied reference, like ref.origin defined).

–––

Thanks for the ideas and inspiration to Artem Krotov, CEO of the only non-geeky hazzle-free mobile wireless hardware wallet U∙HODL; and, for the double-checking the technical details, to Roman Uskov, core Universa network lead developer.