Lightning on Stellar: Technical Spec and Roadmap



  • We want Stellar to become the world’s digital payment rail. We’re already the most deployment-ready
    of the major platforms (see the below chart), but given the scale of
    the future we see for Stellar, we know we need to keep pushing our
    technology forward.

    Stellar Network Performance: Transaction Speed vs. Transaction Cost

    Scalability—namely, how best to achieve it—has been at the center of some of the most bitter disagreements in blockchain. We’ve tried to approach the problem with an open mind. To the extent an idea improves what our users care about—speed, throughput, privacy—we will explore it, and since a typical Lightning payment:

    – can be confirmed instantly – has negligible fees – doesn’t have to become public

    the protocol has always interested us. As we said in our 2018 Roadmap it’s now clear that Lightning is the right way forward for Stellar.

    How Lightning Works

    Lightning is a scaling solution for distributed payment networks, originally proposed for the Bitcoin blockchain. Lightning is designed to allow users to make off-chain payments through routers and hubs. Lightning even has the potential to support cross-protocol payments, such as a payment where the sender sends Bitcoins on the Bitcoin network and the recipient receives lumens on the Stellar network, without having to trust any parties in between.

    Lightning is constructed from building blocks known as payment channels. The concept behind payment channels is simple but powerful. They allow users to open a channel off-chain and transact there instead of on the public ledger. Because they’re off-chain, transactions in the channel can be extremely fast and cheap, but similar to on-chain transactions, there’s no counterparty risk. When the channel participants are ready to go their separate ways, they close the channel and settle back to the public ledger. No matter what happened in-channel, the rest of the world only sees that final transaction. It’s like showing someone the last frame of a movie; from that one still, there’s no way to unpack the rest of the film.

    A drab family drama about the Great Depression

    Developers have begun working on payment channel designs and implementations for several chains and ledgers beyond just Bitcoin, including Ethereum and Zcash. Each platform’s channels are unique and depend on the nuances of the platform, but as a rule, any implementation will support a few basic requirements:

    – No transaction submitted to the network, except when parties disengage – No loss of funds caused by cheating parties – No vulnerability to third-party interference – No channel-side speed bottlenecks

    Stellar supports a more flexible generalization of payment channels called state channels, meaning that any operation you can execute on the Stellar network (such as not only payments, but also creating, deleting, or changing permissions on accounts), you can execute within a payment channel.

    Stellar’s state channel implementation relies on the fact that every Stellar transaction specifies a source account and a sequence number. We’ve figured out how to use those sequence numbers as a natural versioning mechanism for off-chain payments; it’s similar to how your bank gets alerted for out-of-order cheques. To do the versioning, we’re taking advantage of a new operation, BUMP_SEQUENCE, which we’ll describe in complete detail below.

    Our release timeline for Lightning on Stellar is:

    BUMP_SEQUENCE pushed to a testnet
    State channels beta implementation
    State channels on Stellar livenet + Lightning Network beta
    Lightning Network on Stellar livenet

    Apr 1
    Aug 1
    Oct 1
    Dec 1

    Stellar’s creator, Jed McCaleb, first explored Lightning back in 2015; our 2018 implementation still reflects the cleverness of his original plan, but Jeremy Rubin, with the support of Nicolas Barry and David Mazières from SDF, has added the necessary improvements to make Lightning right for us. The explanation that follows is theirs.

    State Channels on Stellar Update Rules Example Using JavaScript SDK Informal Proof Future Work





    https://www.stellar.org/wp-content/uploads/2018/03/lightning-on-stellar.png


    Example Using JavaScript SDK

    We’ll use the Stellar JavaScript SDK to show how one can create a state channel between Alice and Bob. This example is simplified for educational purposes and does not implement a fully functional payment channel, nor does it precisely reflect the specification or final implementation.

    The channel will have 1000 lumens deposited into it, with an initial balance of 250 for Alice and 750 for Bob. We will then have them sign transactions that update the balance to 500/500, without any of those transactions having to hit the chain. Finally, they will close the channel.

    Alice and Bob need to select values for TIMEOUT_CLAIM and TIMEOUT_CLAIM_DELAY based on their payment frequency and network connectivity expectations (including the synchrony assumption for the network, D" role="presentation">

    ). TIMEOUT_CLAIM_DELAY should be at least D" role="presentation">, whereas TIMEOUT_CLAIM should be at least D" role="presentation">
    plus the maximum expected time between rounds. To be able to use
    concrete time periods in the examples below, we will pick a value of one
    week for D" role="presentation">

    , set TIMEOUT_CLAIM_DELAY to 1 week, and set TIMEOUT_CLAIM to 2 weeks. (These times are unrealistically conservative, but should be easy to track in the below examples.)

    We’ll start like this:

    const moment = require('moment')
    const bigInt = require('big-integer')
    const {
      Account,
      Asset,
      Keypair,
      Network,
      Operation,
      Server,
      TransactionBuilder,
    } = require('stellar-sdk')
    const TIMEOUT_CLAIM = moment.duration(2, 'week').seconds()
    const TIMEOUT_CLAIM_DELAY = moment.duration(1, 'week').seconds()
    const server = new Server('https://horizon-testnet.stellar.org')
    Network.useTestNetwork()
    // Alice and Bob are preexisting funded accounts controlled by AliceKeypair and BobKeypair
    const AliceKeypair = Keypair.fromSecret('SCIXVMGTGHIOVMHRA7B7ICJ4XWAYSQP67VNSLNXS7OYZKXDS7I45OJUE')
    const AliceKey = AliceKeypair.publicKey()
    const Alice = await server.loadAccount(AliceKeypair.publicKey())
    // Alice generates throwaway keys for her version account and for the ratchet account
    const AliceVersionKeypair = Keypair.random()
    const AliceRatchetKeypair = Keypair.random()
    const AliceVersionKey = AliceVersionKeypair.publicKey()
    const AliceRatchetKey = AliceRatchetKeypair.publicKey()
    // Bob does the same
    const BobKeypair = Keypair.fromSecret('SAJ2ISPPRUA4MPCDFOILZ6E4H3X6I4OVTMPX4QZBLXTMWMSKO5MC4H6E')
    const BobKey = BobKeypair.publicKey()
    const Bob = await server.loadAccount(BobKey)
    const BobVersionKeypair = Keypair.random()
    const BobRatchetKeypair = Keypair.random()
    const BobVersionKey = BobVersionKeypair.publicKey()
    const BobRatchetKey = BobRatchetKeypair.publicKey()
    // the Ratchet account ID is Alice's ratchet key
    const RatchetAccountId = AliceRatchetKeypair.publicKey()
    

    We then create three accounts:

    const setupAccountsTx = new TransactionBuilder(Alice)

      .addOperation(

        Operation.createAccount({

          destination: AliceVersionKey,

          startingBalance: '1',

        })

      )

      .addOperation(

        Operation.createAccount({

          destination: BobVersionKey,

          startingBalance: '1',

        })

      )

      .addOperation(

        // set up the ratchet account

        // which initially has only Alice's ratchet key

        // the funding transaction will add Bob's key

        Operation.createAccount({

          destination: AliceRatchetKey,

          startingBalance: '2',

        })

      )

      .build()

    setupAccountsTx.sign(AliceKeypair)

    await server.submitTransaction(setupAccountsTx)

    const AliceVersion = await server.loadAccount(AliceVersionKey)

    const BobVersion = await server.loadAccount(BobVersionKey)

    const Ratchet = await server.loadAccount(RatchetAccountId)

    Alice and Bob must now prepare round 0 before funding the channel.

    First, they prepare snapshot transactions reflecting their current balances, and exchange their signatures on them.

    They then exchange their initial Ratchet transactions, which will bump the sequence number of the ratchet account to the sequence number immediately preceding the snapshot transactions. (Note that this will not work yet in the existing SDK, because the BUMP_SEQUENCE operation is not yet supported on the network.)

    const Ratchet0Alice = new TransactionBuilder(

      new Account(AliceVersion.accountId(), AliceVersion.sequenceNumber()),

      { timebounds: { minTime: Round0Time, maxTime: Round0Time + TIMEOUT_CLAIM } }

    )

      .addOperation(

        Operation.BumpSequence({

          sourceAccount: RatchetKey,

          target: Ratchet0SequenceNumber.minus(1).toString(),

        })

      )

      .build()

    const Ratchet0Bob = new TransactionBuilder(

      new Account(BobVersion.accountId(), BobVersion.sequenceNumber()),

      { timebounds: { minTime: Round0Time, maxTime: Round0Time + TIMEOUT_CLAIM } }

    )

      .addOperation(

        Operation.BumpSequence({

          sourceAccount: RatchetKey,

          target: Ratchet0SequenceNumber.minus(1).toString(),

        })

      )

      .build()

    Now that the snapshot transactions and ratchet transactions are in place, either Alice or Bob will have the ability to close the channel and receive their portion of the lumens. This means it is now safe for Alice and Bob to fund the channel.

    const fundingTx = new TransactionBuilder(Ratchet)

      .addOperation(

        Operation.payment({

          source: Alice.accountId(),

          destination: Ratchet.accountId(),

          asset: Asset.native(),

          amount: '248', // Alice has already paid in 2 lumens

        })

      )

      .addOperation(

        Operation.payment({

          source: Bob.accountId(),

          destination: Ratchet.accountId(),

          asset: Asset.native(),

          amount: '750',

        })

      )

      .addOperation(

        Operation.setOptions({

          signer: { ed25519PublicKey: BobVersionKey, weight: 1 },

          lowThreshold: 2,

          medThreshold: 2,

          highThreshold: 2,

        })

      )

      .build()

    fundingTx.sign(AliceKeypair)

    fundingTx.sign(BobKeypair)

    fundingTx.sign(AliceRatchetKeypair)

    await server.submitTransaction(fundingTx)

    Now the channel is fully set up. If, at this point, either Alice or Bob were to act dishonestly (e.g. by going offline or refusing to respond) either party can initiate their ratchet transaction, then the snapshot transactions, to get back to their initial state.

    Critically, the redeeming party must act within the specified time range. In this case, if there are no further rounds in the channel and Bob does not cooperate in creating further rounds, Alice should attempt to close the channel within one week (to give herself at least D" role="presentation">

    time to get her transaction included). She must then wait two weeks (a total of three weeks from the start time of the channel) for the snapshot transactions to become valid.

    Now, Bob wants to pay Alice 250 lumens over the channel. In other words, they want to update the channel state, so the balances change from 250/750 (with Alice owning 250) to 500/500.

    Alice and Bob create new snapshot transactions, reflecting the updated state, and exchange their signatures on them.

    const Ratchet1SequenceNumber = Ratchet0SequenceNumber.plus(3)

    const Ratchet1Account = new Account(

      Ratchet.accountId(),

      Ratchet1SequenceNumber.toString()

    )

    const Round1Time = moment().unix()

    const Snapshot1Alice = new TransactionBuilder(

      new Account(RatchetAccountId, Ratchet1SequenceNumber.toString()),

      {

        timebounds: {

          minTime: Round1Time + TIMEOUT_CLAIM + TIMEOUT_CLAIM_DELAY,

          maxTime: 0,

        },

      }

    )

      .addOperation(

        Operation.payment({

          destination: Alice.accountId(),

          asset: Asset.native(),

          amount: '500',

        })

      )

      .build()

    const Snapshot1Bob = new TransactionBuilder(

      new Account(

        RatchetAccountId,

        Ratchet1SequenceNumber.plus(1).toString()

      ),

      {

        timebounds: {

          minTime: Round1Time + TIMEOUT_CLAIM + TIMEOUT_CLAIM_DELAY,

          maxTime: 0,

        },

      }

    )

      .addOperation(

        Operation.setOptions({

          signer: { ed25519PublicKey: BobKey, weight: 2 },

        })

      )

      .build()

    // exchange signatures

    Snapshot1Alice.sign(AliceRatchetKeypair)

    Snapshot1Bob.sign(AliceRatchetKeypair)

    Snapshot1Alice.sign(BobRatchetKeypair)

    Snapshot1Bob.sign(BobRatchetKeypair)

    They now can create and exchange signatures on new ratchet transactions:

    const Ratchet1Bob = new TransactionBuilder(

      new Account(BobVersion.accountId(), BobVersion.sequenceNumber()),

      { timebounds: { minTime: Round1Time, maxTime: Round1Time + TIMEOUT_CLAIM } }

    )

      .addOperation(

        Operation.BumpSequence({

          sourceAccount: RatchetKey,

          target: Ratchet1SequenceNumber.minus(1).toString(),

        })

      )

      .build()

    const Ratchet1Alice = new TransactionBuilder(

      new Account(AliceVersion.accountId(), AliceVersion.sequenceNumber()),

      { timebounds: { minTime: Round1Time, maxTime: Round1Time + TIMEOUT_CLAIM } }

    )

      .addOperation(

        (Operation as any).BumpSequence({

          sourceAccount: RatchetKey,

          target: Ratchet1SequenceNumber.minus(1).toString(),

        })

      )

      .build()

    Ratchet1Bob.sign(AliceRatchetKeypair)

    Ratchet1Alice.sign(BobRatchetKeypair)

    This payment is now done. Note that none of these transactions are broadcast to the network.

    However, there’s now a potential problem—Alice and Bob still have valid ratchet and snapshot transactions from round 0, when their balances were different. What happens if Bob tries to submit those transactions, to close the channel at an outdated state?

    Each of Alice and Bob should therefore monitor the network to detect any transactions from the other’s version account. If they detect one, they should immediately submit the ratchet transaction from the latest round.

    const streamHandler = server

      .transactions()

      .forAccount(BobVersion.accountId())

      .cursor('now')

      .stream({

        onmessage: async function(transaction) {

          if (transaction.hash !== Ratchet1Bob.hash().toString('hex')) {

            await server.submitTransaction(Ratchet1Alice)

          }

        },

      })

    To ensure that there is enough time for Alice or Bob to challenge any invalid submission, they should make sure that rounds happen frequently enough that the remaining time that latest ratchet transaction is valid is at least as long as D, so they will have time to respond to any submissions of stale ratchet transactions.

    The parties can add as many payments as they like to the channel by creating and signing new snapshot transactions reflecting the new state of the channel, as well as ratchet transactions that set up those snapshot transactions. For each new round, the starting sequence number of the snapshot transactions is incremented by 3. None of these transactions need to be submitted to the network.

    Finally, to close the channel, Alice and Bob sign and submit closing transactions to the network, using the balances from the latest snapshot transactions. These transactions are similar to the previous rounds—involving both ratchet transactions and snapshot transactions—except that that snapshot transactions do not need time bounds, and only one shared Ratchet transaction is required.

    const CooperativeCloseSequenceNumber = Ratchet1SequenceNumber.plus(3)

    const CooperativeCloseSnapshotAlice = new TransactionBuilder(

      new Account(RatchetAccountId, CooperativeCloseSequenceNumber.toString())

    )

      .addOperation(

        Operation.payment({

          destination: Alice.accountId(),

          asset: Asset.native(),

          amount: '500',

        })

      )

      .build()

    const CooperativeCloseSnapshotBob = new TransactionBuilder(

      new Account(

        RatchetAccountId,

        CooperativeCloseSequenceNumber.plus(1).toString()

      )

    )

      .addOperation(

        Operation.setOptions({

          signer: { ed25519PublicKey: BobKey, weight: 2 },

        })

      )

      .build()

    CooperativeCloseSnapshotAlice.sign(AliceRatchetKeypair)

    CooperativeCloseSnapshotBob.sign(AliceRatchetKeypair)

    CooperativeCloseSnapshotAlice.sign(BobRatchetKeypair)

    CooperativeCloseSnapshotBob.sign(BobRatchetKeypair)

    const CooperativeCloseRatchet = new TransactionBuilder(

      new Account(Ratchet.accountId(), RatchetInitialSequenceNumber.toString()),

      { timebounds: { minTime: ClosingTime, maxTime: ClosingTime + TIMEOUT_CLAIM } }

    )

      .addOperation(

        Operation.BumpSequence({

          target: CooperativeCloseSequenceNumber.minus(1).toString(),

        })

      )

      .build()

    CooperativeCloseRatchet.sign(AliceRatchetKeypair)

    CooperativeCloseRatchet.sign(BobRatchetKeypair)

    await server.submitTransaction(CooperativeCloseRatchet)

    await server.submitTransaction(CooperativeCloseSnapshotAlice)

    await server.submitTransaction(CooperativeCloseSnapshotBob)

    This is one way to do a safe honest close; there are others that reveal even less information to the network.

    Informal Proof

    We can informally prove that at this point we have made it impossible to close at the initial channel state after the next round has completed. Our argument generalizes to any number of prior states, and also holds when Alice and Bob’s roles are reversed.

    Assume Bob is malicious and Alice is honest.

    1. Alice is monitoring the network for activity on the BobVersion account.
    2. After the second round, Bob submits Ratchet0Bob.
    3. Alice now has at least D" role="presentation">

    to submit a later ratchet transaction to counter Bob’s. (This relies on
    Alice enforcing this invariant—i.e., if her latest ratchet transaction,
    in this case Ratchet1Alice, is less than D" role="presentation">

    1. away, she must immediately close the channel or submit the ratchet transaction, to ensure that Bob isn’t able to submit a stale ratchet transaction that leaves her too little time to respond).
    2. Alice can submit Ratchet1Alice, to set up Snapshot1Alice.
    3. Because all of Bob’s ratchet transcations use the same sequence number, there are no other operations Bob can use to affect the Ratchet account
    4. Once the minTime on Snapshot1Alice is satisfied, Alice can use it to redeem her funds.

    Assume Bob disappears after Snapshot1Alice, and Alice is honest.

    1. Alice requests an honest_close, but Bob is unreachable.
    2. Alice submits Ratchet1Alice before it expires.
    3. Alice waits until the minTime of Snapshot1Alice has been satisfied.
    4. Alice submits Snapshot1Alice.

    Assume Bob disappears part-way through a payment or honest close—after signing Snapshot1Alice and Snapshot1Bob, but before signing Ratchet1Alice—and Alice is honest.

    1. Neither party has the applicable ratchet transaction for the second round, so that payment is never finalized.
    2. Alice can use Ratchet0Alice and Alice0Snapshot to close the channel.

    Assume Bob disappears part-way through a payment or honest close—after receiving Alice’s signature on BobRatchet1, but before giving Alice his signature on AliceRatchet1—and Alice is honest.

    1. Alice proceeds with AliceRatchet0.
    2. If Bob comes back online before BobRatchet1 expires, he can contest with BobRatchet1. After the delay period, either can submit Snapshot1Alice and Snapshot1Bob.
    3. Alternatively, if Bob doesn’t come back online, Alice can wait until BobRatchet1 has expired and the minTime of Alice0Snapshot has been satisfied, and submit Alice0Snapshot.

    Assume Alice disappears part-way through a payment or honest close—after creating Snapshot1Alice, but before creating Ratchet1Alice—while Bob is honest.

    1. Neither party has Ratchet1Alice or Ratchet1Bob.
    2. Bob forces a close using Ratchet0Bob, Snapshot0Alice, and Bob0Snapshot.

    Future Work

    This is a simple design for payment channels on Stellar, but there is still much work to be done.

    We’re currently working on support for multi-hop payments, increased privacy and scalability, and interoperability with Lightning Network channels on other blockchains such as Bitcoin. If you’re interested in helping us build out our protocol, join us on GitHub or StackExchange.




Looks like your connection to Cryptocentral was lost, please wait while we try to reconnect.