Skip to content
Permalink
master
Switch branches/tags
Go to file
7 contributors

Users who have contributed to this file

Preamble

CAP: 0021
Title: Preconditions: Generalized transaction preconditions
Working Group:
    Owner: David Mazières <@stanford-scs>
    Authors: David Mazières <@stanford-scs>, Leigh McCulloch <@leighmcculloch>
Status: Final
Created: 2019-05-24
Updated: 2021-11-03
Discussion: https://groups.google.com/g/stellar-dev/c/N8vzP2Mi89U
Protocol version: 19

Simple Summary

This proposal generalizes the timeBounds field in Transaction to support other conditions, including conditions that relax sequence number checking and provide relative timelocks.

Motivation

Sequence numbers are tricky for anything other than simple payments. For instance, pre-authorized transactions can only execute when the source account has a specific sequence number. Worse yet, sequence numbers make it difficult for protocols such as payment channels to guarantee that one participant can execute a transaction signed by all participants. In general, an N-party protocol requires N auxiliary accounts, one for each participant; each logical transaction pre-signed by all N participants must actually be implemented as N pre-signed transactions using each auxiliary account at a source, so that one participant can still submit a pre-signed transaction even if another participant has changed the sequence number on a different auxiliary account. This is further complicated by the need to maintain a reserve balance on each auxiliary account.

Goals Alignment

This proposal advances network scalability by facilitating off-chain payment channels. It advances security and simplicity and interoperability with other networks by enabling relative timelocks. Finally, the proposal makes it easier for developers to create highly usable products by enabling time-delayed key recovery.

Abstract

This proposal extends AccountEntry to keep track of the time and ledger number at which the account's sequence number was last changed. It also replaces the timeBounds field of Transaction with a union that allows more general transaction preconditions. One of these preconditions requires that the sequence number of sourceAccount have been modified at least some period of time in the past, effectively providing a relative timelock. Another precondition optionally weakens sequence number checking so as to allow a transaction to execute when the sourceAccount is within some range.

Specification

AccountEntryExtensionV2's ext field is extended to keep track of seqLedger and seqTime--the ledger number and time at which the sequence number was set to its present value. These values are updated in two situations:

  1. Transaction: For the sourceAccount of every executed transaction.

  2. BumpSequenceOp operation: For the sourceAccount of every successfully executed BumpSequenceOp operation, regardless of whether the BumpSequenceOp actually increased or modified the sequence number of the account. This allows an account to update it's seqLedger or seqTime without using an additional sequence number.

If an account does not have an AccountEntryExtensionV3 because it hasn't been upgraded yet, then it behaves as if seqLedger and seqTime are both 0.

// An ExtensionPoint is always marshaled as a 32-bit 0 value.  At a
// later point, it can be replaced by a different union so as to
// extend a structure.
union ExtensionPoint switch (int v) {
case 0:
     void;
};

struct AccountEntryExtensionV3
{
    // We can use this to add more fields, or because it is first, to
    // change AccountEntryExtensionV3 into a union.
    ExtensionPoint ext;

    // Ledger number at which `seqNum` took on its present value.
    uint32 seqLedger;

    // Time at which `seqNum` took on its present value.
    TimePoint seqTime;
};

struct AccountEntryExtensionV2
{
    uint32 numSponsored;
    uint32 numSponsoring;
    SponsorshipDescriptor signerSponsoringIDs<MAX_SIGNERS>;

    union switch (int v)
    {
    case 0:
        void;
    case 3:
        AccountEntryExtensionV3 v3;
    }
    ext;
};

Preconditions are represented by a new Preconditions union with discriminant type. Values PRECOND_NONE and PRECOND_TIME are binary compatible with the current timeBounds field (which is of type TimeBounds*). Value PRECOND_V2 is the new type of precondition. Note that minSeqNum, if non-NULL, relaxes the range of sequence numbers at which a transaction can be executed. However, after executing a transaction, sourceAccount's sequence number is always set to the transaction's seqNum--like an implicit BUMP_SEQUENCE operation. This guarantees transactions cannot be replayed, even when the previous account seqNum is well below the transaction's seqNum. The final element of Preconditions is an array of extra signers required for the transaction. This can be used with SIGNER_KEY_TYPE_HASH_X to sign a transaction that can only be executed in exchange for disclosing a hash preimage.

Note that a TransactionV1Envelope may contain at most 20 signatures. Any signatures required by the extraSigners field reside in the TransactionV1Envelope and hence must reside in the same 20 signature slots. As a consequence, a transaction that would require 20 signatures without an extraSigners field generally cannot contain extraSigners unless the extraSigners are satisfied by the same signatures as the sourceAccounts.

typedef uint64 Duration;

struct LedgerBounds {
    uint32 minLedger;
    uint32 maxLedger;
};

struct PreconditionsV2 {
    TimeBounds *timeBounds;

    // Transaction only valid for ledger numbers n such that
    // minLedger <= n < maxLedger (if maxLedger == 0, then
    // only minLedger is checked)
    LedgerBounds *ledgerBounds;

    // If NULL, only valid when sourceAccount's sequence number
    // is seqNum - 1.  Otherwise, valid when sourceAccount's
    // sequence number n satisfies minSeqNum <= n < tx.seqNum.
    // Note that after execution the account's sequence number
    // is always raised to tx.seqNum.
    SequenceNumber *minSeqNum;

    // For the transaction to be valid, the current ledger time must
    // be at least minSeqAge greater than sourceAccount's seqTime.
    Duration minSeqAge;

    // For the transaction to be valid, the current ledger number
    // must be at least minSeqLedgerGap greater than sourceAccount's
    // seqLedger.
    uint32 minSeqLedgerGap;

    // For the transaction to be valid, there must be a signature
    // corresponding to every Signer in this array, even if the
    // signature is not otherwise required by the sourceAccount or
    // operations.
    SignerKey extraSigners<2>;
};

enum PreconditionType {
    PRECOND_NONE = 0,
    PRECOND_TIME = 1,
    PRECOND_V2 = 2
};

union Preconditions switch (PreconditionType type) {
    case PRECOND_NONE:
        void;
    case PRECOND_TIME:
        TimeBounds timeBounds;
    case PRECOND_V2:
        PreconditionsV2 v2;
};

Note we add an unsigned Duration type, used by minSeqAge.

We make use of the new Preconditions type by replacing timeBounds in the Transaction structure as follows:

struct Transaction
{
    // account used to run the transaction
    MuxedAccount sourceAccount;

    // the fee the sourceAccount will pay
    uint32 fee;

    // sequence number to consume in the account
    SequenceNumber seqNum;

    // validity conditions
    Preconditions cond;

    ...
};

A transaction whose preconditions are not satisfied is non-executable. In most cases, non-executable transactions should not be included in blocks. However, it is possible that a prior transaction in the same block can turn a executable transaction into a non-executable one. If this happens, the non-executable transaction still incurs a fee and increments the sourceAccount sequence number.

To minimize the presence of non-executable transactions in blocks, a block may not contain both a transaction with a non-zero minSeqLedgerGap or minSeqAge and one with a lower seqNum on the same sourceAccount. Unfortunately, this does not entirely eliminate the possibility of non-executable transactions in blocks; for instance, a BUMP_SEQUENCE operation in a transaction from a different sourceAccount can invalidate the minSeqAge or minSeqLedgerGap on another transaction.

Transaction forwarding and ordering

A transaction submitted to the network is valid only if it is part of a valid series of pending transactions on the same sourceAccount that can all be valid in the same block. For example, if a source account has seqNum 10, then a submitted transaction with seqNum 12 and no preconditions is valid (and should be forwarded) only if there is also a pending valid transaction with sequence number 11. The minSeqNum field in this proposal relaxes validity to allow a valid series of transactions on the same sourceAccount with discontinuous seqNum fields. The gaps, however, cannot be filled in (if the queue already had seqNum 10 and 12 with a valid gap, 11 should not be accepted and forwarded). Regardless of these gaps, all transactions on the same sourceAccount in the same block must be executed in order of increasing seqNum. Hence, the presence of the minSeqNum field may make transactions valid that would not otherwise be valid, but cannot invalidate otherwise valid transactions, since lower seqNum fields always execute first before higher ones that would invalidate them.

A transaction with a non-zero minSeqAge or minSeqLedgerGap must be discarded and not forwarded--as if its minTime has not yet arrived--if either A) the appropriate condition (minSeqAge or minSeqLedgerGap) does not yet hold, or B) there are pending valid transactions with lower sequence numbers on the same sourceAccount. Conversely, after receiving and forwarding a valid transaction with a non-zero minSeqAge or minSeqLedgerGap, subsequently received transactions with earlier sequence numbers must be discarded. However, a nominated block is valid so long as its transactions can be executed. This means a validator can vote for a block containing transactions that the validator would have discarded. For example, consider the following two transactions on the same sourceAccount which currently has seqNum 10:

  • T1 has seqNum 11 and no preconditions.
  • T2 has seqNum 12, minSeqNum 10, and minSeqLedgerGap 1.

Any validator that receives both of these transactions will keep the first one and discard the second one that it receives. However, if a validator sees a nomination vote for a block that contains T2 but not T1, the validator will nonetheless vote for the block. The logic is identical to a situation in which T1 and T2 have the same sequence number and fee--validators seeing both will discard the second one that they receive.

Transaction validation

The Preconditions in each transaction of a block are validated twice.

The first validation occurs when checking that a block itself is valid and can be nominated by the consensus protocol. As part of the validation, for each sourceAccount there can be only one transaction with a given sequence number and the seqNum fields must either be consecutive or any gaps must be permitted by non-NULL minSeqNum fields. All but the transaction with the lowest seqNum on a given sourceAccount must have 0 for the minSeqAge and minSeqLedgerGap fields.

Once a block is externalized by the consensus algorithm, the block is applied. Before executing any operations, fees are charged to the source account for all transactions. Once fees have been deducted from all accounts, transactions are one-by-one validated a second time then executed. It is possible for a previously valid transaction to fail the second validation, for instance if a BUMP_SEQUENCE operation made the sequence number invalid. Whenever a transaction fails validation during execution, the sourceAccount loses the fee.

Because PreconditionsV2 specifies multiple pre-conditions, there may be multiple reasons why a transaction is invalid. If extraSigners contains duplicate signers, the transaction is rejected with txMALFORMED (Note that signer overlap between extraSigners and AccountEntry signers is allowed). If the maxTime (inclusive) or maxLedger (exclusive) have not been satisfied-- then the transaction is rejected with TransactionResultCode txTOO_LATE. If minTime or minLedger have not been reached yet, then the transaction is rejected with txTOO_EARLY. If minSeqNum is set, but the relaxed sequence number validation still fails, then the transaction is rejected with txBAD_SEQ. If the failure is due to minSeqAge or minSeqLedgerGap, then the transaction is rejected with the new txBAD_MIN_SEQ_AGE_OR_GAP error code. If none of the above conditions holds (txTOO_EARLY, txBAD_MIN_SEQ_AGE_OR_GAP, txMALFORMED, txBAD_SEQ, and txTOO_LATE do not apply), but one of the extraSigners is unsatisfied, then the transaction fails with txBAD_AUTH.

XDR diff

diff --git a/src/xdr/Stellar-ledger-entries.x b/src/xdr/Stellar-ledger-entries.x
index c870fe09..5c772f1c 100644
--- a/src/xdr/Stellar-ledger-entries.x
+++ b/src/xdr/Stellar-ledger-entries.x
@@ -13,6 +13,7 @@ typedef string string32<32>;
 typedef string string64<64>;
 typedef int64 SequenceNumber;
 typedef uint64 TimePoint;
+typedef uint64 Duration;
 typedef opaque DataValue<64>;
 typedef Hash PoolID; // SHA256(LiquidityPoolParameters)
 
@@ -133,6 +134,19 @@ const MAX_SIGNERS = 20;
 
 typedef AccountID* SponsorshipDescriptor;
 
+struct AccountEntryExtensionV3
+{
+    // We can use this to add more fields, or because it is first, to
+    // change AccountEntryExtensionV3 into a union.
+    ExtensionPoint ext;
+
+    // Ledger number at which `seqNum` took on its present value.
+    uint32 seqLedger;
+
+    // Time at which `seqNum` took on its present value.
+    TimePoint seqTime;
+};
+
 struct AccountEntryExtensionV2
 {
     uint32 numSponsored;
@@ -143,6 +157,8 @@ struct AccountEntryExtensionV2
     {
     case 0:
         void;
+    case 3:
+        AccountEntryExtensionV3 v3;
     }
     ext;
 };
diff --git a/src/xdr/Stellar-transaction.x b/src/xdr/Stellar-transaction.x
index 1a4e491a..811e4786 100644
--- a/src/xdr/Stellar-transaction.x
+++ b/src/xdr/Stellar-transaction.x
@@ -576,6 +576,58 @@ struct TimeBounds
     TimePoint maxTime; // 0 here means no maxTime
 };
 
+struct LedgerBounds
+{
+    uint32 minLedger;
+    uint32 maxLedger; // 0 here means no maxLedger
+};
+
+struct PreconditionsV2 {
+    TimeBounds *timeBounds;
+
+    // Transaction only valid for ledger numbers n such that
+    // minLedger <= n < maxLedger (if maxLedger == 0, then
+    // only minLedger is checked)
+    LedgerBounds *ledgerBounds;
+
+    // If NULL, only valid when sourceAccount's sequence number
+    // is seqNum - 1.  Otherwise, valid when sourceAccount's
+    // sequence number n satisfies minSeqNum <= n < tx.seqNum.
+    // Note that after execution the account's sequence number
+    // is always raised to tx.seqNum.
+    SequenceNumber *minSeqNum;
+
+    // For the transaction to be valid, the current ledger time must
+    // be at least minSeqAge greater than sourceAccount's seqTime.
+    Duration minSeqAge;
+
+    // For the transaction to be valid, the current ledger number
+    // must be at least minSeqLedgerGap greater than sourceAccount's
+    // seqLedger.
+    uint32 minSeqLedgerGap;
+
+    // For the transaction to be valid, there must be a signature
+    // corresponding to every Signer in this array, even if the
+    // signature is not otherwise required by the sourceAccount or
+    // operations.
+    SignerKey extraSigners<2>;
+};
+
+enum PreconditionType {
+    PRECOND_NONE = 0,
+    PRECOND_TIME = 1,
+    PRECOND_V2 = 2
+};
+
+union Preconditions switch (PreconditionType type) {
+    case PRECOND_NONE:
+        void;
+    case PRECOND_TIME:
+        TimeBounds timeBounds;
+    case PRECOND_V2:
+        PreconditionsV2 v2;
+};
+
 // maximum number of operations per transaction
 const MAX_OPS_PER_TX = 100;
 
@@ -627,8 +679,8 @@ struct Transaction
     // sequence number to consume in the account
     SequenceNumber seqNum;
 
-    // validity range (inclusive) for the last ledger close time
-    TimeBounds* timeBounds;
+    // validity conditions
+    Preconditions cond;
 
     Memo memo;
 
@@ -1508,7 +1560,9 @@ enum TransactionResultCode
 
     txNOT_SUPPORTED = -12,         // transaction type not supported
     txFEE_BUMP_INNER_FAILED = -13, // fee bump inner transaction failed
-    txBAD_SPONSORSHIP = -14        // sponsorship not confirmed
+    txBAD_SPONSORSHIP = -14,        // sponsorship not confirmed
+    txBAD_MIN_SEQ_AGE_OR_GAP = -15, //minSeqAge or minSeqLedgerGap conditions not met
+    txMALFORMED = 16                // precondition is invalid
 };
 
 // InnerTransactionResult must be binary compatible with TransactionResult
@@ -1537,6 +1591,8 @@ struct InnerTransactionResult
     case txNOT_SUPPORTED:
     // txFEE_BUMP_INNER_FAILED is not included
     case txBAD_SPONSORSHIP:
+    case txBAD_MIN_SEQ_AGE_OR_GAP:
+    cast txMALFORMED:
         void;
     }
     result;
diff --git a/src/xdr/Stellar-types.x b/src/xdr/Stellar-types.x
index 8f7d5c20..caa41d7f 100644
--- a/src/xdr/Stellar-types.x
+++ b/src/xdr/Stellar-types.x
@@ -14,6 +14,14 @@ typedef int int32;
 typedef unsigned hyper uint64;
 typedef hyper int64;
 
+// An ExtensionPoint is always marshaled as a 32-bit 0 value.  At a
+// later point, it can be replaced by a different union so as to
+// extend a structure.
+union ExtensionPoint switch (int v) {
+case 0:
+     void;
+};
+
 enum CryptoKeyType
 {
     KEY_TYPE_ED25519 = 0,

Design Rationale

Relative timelocks are a known mechanism for simplifying payment channels, implemented by Bitcoin and used in lightning payment channels. Stellar's lack of UTXOs combined with transaction sequence numbers make payment channels harder to implement. This proposal rectifies the problem in a way that is not too hard to implement in stellar-core and provides a good degree of backwards compatibility.

Fundamentally, a payment channel requires a way to enforce a time separation between declaring that one wants to execute a pre-signed transaction T and actually executing T. Furthermore, between the declaration and execution, other parties need a chance to object and invalidate T if there is a later T' superseding T. The relative timelock provides this separation, while the relaxing of sequence numbers makes it easy to object by pre-signing a transaction invalidating T that can be submitted at a variety of sequence numbers. Without such a mechanism, multiple auxiliary accounts are required.

An earlier version of the proposal did not contain the minSeqLedgerGap field. However, members of the payment channel working group were concerned that the network could, in a worst-case scenario, experience downtime right after someone incorrectly closes a payment channel, precluding the other party from correcting the problem. minSeqLedgerGap guarantees that there will be an opportunity to correct the problem when the network comes back up, because the pre-signed transaction with a minSeqLedgerGap will still not be immediately executable.

It's worth asking whether we need minSeqAge if we have minSeqLedgerGap. One reason to keep it is that, under heavy load, the network could start processing ledgers faster than once every 5 seconds. This might happen after periods of downtime.

One possible efficiency problem is that transactions with a minSeqAge or minSeqLedgerGap cannot be pipelined behind other transactions on the same sourceAccount. Though this might seem to reduce efficiency, in fact such time-delayed transactions are intended to be delayed for some "disclosure period" during which the account remains idle. Typically such time-delayed transactions are intended to correct an abnormal situation (e.g., one end of a payment channel failing, or an account owner losing the key) and so don't actually get submitted in the common case.

Two-way payment channel

The proposed mechanism can be used to implement a payment channel between two parties, an initiator I and a responder R. The protocol assumes some synchrony period, S, such that both parties are guaranteed to be able to observe the blockchain state and submit transactions within any period of length S.

The payment channel consists of a 2-of-2 multisig escrow account E, initially created and configured by I, and a series of pairs of declaration and closing transactions on E signed by both parties. The two parties maintain the following two variables during the lifetime of the channel:

  • s - the starting sequence number, is initialized to one greater than the sequence number of the escrow account E after E has been created and configured. It is increased only when withdrawing from or topping up the escrow account E.

  • i - the iteration number of the payment channel, is initialized to (s/2)+1. It is incremented with every off-chain update of the payment channel state.

To update the payment channel state, the parties 1) increment i, 2) sign and exchange a closing transaction C_i, and finally 3) sign and exchange a declaration transaction D_i. The transactions are constructed as follows:

  • D_i, the declaration transaction, declares an intent to execute the corresponding closing transaction C_i. D_i has source account E, sequence number 2i, and minSeqNum set to s. Hence, D_i can execute at any time, so long as E's sequence number n satisfies s <= n < 2i. D_i always leaves E's sequence number at 2i after executing. Because C_i has source account E and sequence number 2i+1, D_i leaves E in a state where C_i can execute. Note that D_i does not require any operations, but since Stellar disallows empty transactions, it contains a BUMP_SEQUENCE operation as a no-op.

  • C_i, the closing transaction, disburses funds to R and changes the signing weights on E such that I unilaterally controls E. C_i has source account E, sequence number 2i+1, and a minSeqAge of S (the synchrony period). The minSeqAge prevents a misbehaving party from executing C_i when the channel state has already progressed to a later iteration number, as the other party can always invalidate C_i by submitting D_i' for some i' > i. C_i contains one or more CREATE_CLAIMABLE_BALANCE operations disbursing funds to R, plus a SET_OPTIONS operation adjusting signing weights to give I full control of E.

For R to top-up or withdraw excess funds from the escrow account E, the participants skip a generation. They set s = 2(i+1), and i = i+2. They then exchange C_i and D_i (which unlike the update case, can be exchanged in a single phase of communication because D_i is not yet executable while E's sequence number is below the new s). Finally, they create a top-up transaction (on some source account other than E, in case it fails) that atomically adjusts E's balance and uses BUMP_SEQUENCE to increase E's sequence number to s.

To close the channel cooperatively, the parties re-sign C_i with a minSeqNum of s and a minSeqAge of 0, then submit this transaction.

Two-way payment channel supporting uncoordinated deposits

The proposed mechanism can be used to implement a payment channel between two parties, an initiator I and a responder R. The protocol assumes some synchrony period, S, such that both parties are guaranteed to be able to observe the blockchain state and submit transactions within any period of length S.

The payment channel consists of two 2-of-2 multisig escrow accounts:

  • EI - created and configured by I, holding all amounts contributed by I.

  • ER - created and configured by R, holding all amounts contributed by R.

The payment channel updates state using a series of declaration and closing transactions with EI as the source account. The two parties maintain the following two variables during the lifetime of the channel:

  • s - the starting sequence number, is initialized to one greater than the sequence number of the escrow account EI after EI has been created and configured. It is increased only when withdrawing.

  • i - the iteration number of the payment channel, is initialized to (s/2)+1. It is incremented with every off-chain update of the payment channel state.

To update the payment channel state, the parties 1) increment i, 2) sign and exchange a closing transaction C_i, and finally 3) sign and exchange a declaration transaction D_i. The transactions are constructed as follows:

  • D_i, the declaration transaction, declares an intent to execute the corresponding closing transaction C_i. D_i has source account EI, sequence number 2i, and minSeqNum set to s. Hence, D_i can execute at any time, so long as EI's sequence number n satisfies s <= n < 2i. D_i always leaves EI's sequence number at 2i after executing. Because C_i has source account EI and sequence number 2i+1, D_i leaves EI in a state where C_i can execute. Note that D_i does not require any operations, but since Stellar disallows empty transactions, it contains a BUMP_SEQUENCE operation as a no-op.

  • C_i, the closing transaction, disburses funds from EI to ER, and/or from ER to EI such that the balances of the escrow accounts match the final agreed state of the channel at the time C_i is generated. C_i also changes the signing weights on EI and ER such that I unilaterally controls EI and R unilaterally controls ER. C_i has source account EI, sequence number 2i+1, and a minSeqAge of S (the synchrony period). The minSeqAge prevents a misbehaving party from executing C_i when the channel state has already progressed to a later iteration number, as the other party can always invalidate C_i by submitting D_i' for some i' > i. C_i contains one or more PAYMENT operations disbursing funds between escrow accounts, plus SET_OPTIONS operations adjusting signing weights of each escrow account.

I and R may top-up their respective escrow accounts by making a payment into them directly.

I and R may adjust the relative balances of EI and ER as well as withdraw excess funds from these accounts by skipping a generation. They set s = 2(i+1), and i = i+2. They then exchange C_i and D_i (which unlike the update case, can be exchanged in a single phase of communication because D_i is not yet executable while EI's sequence number is below the new s). Finally, they create a withdraw transaction that atomically shifts funds between EI and ER, withdraws any desired excess funds with CREATE_CLAIMABLE_BALANCE, and uses BUMP_SEQUENCE to increase EI's sequence number to s.

To close the channel cooperatively, the parties re-sign C_i with a minSeqNum of s and a minSeqAge of 0, then submit this transaction.

One-way payment channel

A one-way payment channel enables an initiator I to make repeated payments to a recipient R. Unlike the two-way payment channel, I can unilaterally set up the payment channel without R's cooperation. Moreover, R can unilaterally withdraw funds from the payment channel at any point with no close delay.

The channel consists of a an escrow account E, initially created by I. Let s be E's sequence number after it has been created and configured. Define the following transactions with source account E:

  • D, the disclosure transaction, has sequence number s+1 and a vacuous BUMP_SEQUENCE operation.

  • C_i, version i of the closing transaction, has sequence number s+2. It disburses funds to R through one or more CREATE_CLAIMABLE_BALANCE operations, and uses SET_OPTIONS to increase I's signing weight to 2. Each C_i disburses more funds to R than C_{i-1}. Only one C_i can execute since they all have the same sequence number.

  • F, the fault-recovery transaction, allows I to recover E in case R fails. It has sequence number s+2, a minSeqAge of S (some synchrony period), and gives I signing weight 2 on the account.

After adding appropriate trustlines and funding the escrow account E, I issues a transaction configuring E to have signing threshold 2 (for low, medium, and high) and to have the following signers all with weight 1: I, R, D, and F (the latter two as SIGNER_KEY_TYPE_PRE_AUTH_TX).

To submit series of payments, I sends R successive C_i transactions each of which reflects the cumulative sum of all previous payments. R accepts these so long as E has a sufficient balance. To close the channel, R submits D and C_i. If R fails, I can close the channel by submitting D, waiting S time, and then submitting F.

Hash Time Locked Contract (HTLC)

HTLCs are a key building block for many blockchain protocols such as cross-chain atomic swaps and payment channels. An HTLC is a transaction T characterized by two values: a hash h and an expiration time t. Before the expiration time, anyone who knows the hash preimage of h can execute T in exchange for disclosing that preimage. Typically, disclosing the preimage unlocks a different transaction on the same or a different blockchain.

To make a transaction into an HTLC, the following preconditions should be set:

  • timeBounds->maxTime should be set to the expiration time t.

  • extraSigners[0] should be set to a SIGNER_KEY_TYPE_HASH_X with the hash value h.

Note that the maximum size of a hash pre-image on Stellar is 64 bytes. On Bitcoin, a hash preimage could potentially be up to 520 bytes. Hence, when pairing Stellar HTLCs with transactions on other blockchains for cross-chain operation, care must be taken to ensure that the other blockchain does not accept preimages larger than 64 bytes. Otherwise, a larger preimage disclosed on another blockchain would fail to unlock an HTLC on Stellar.

Key recovery

The owner of account A may wish for a friend with key K to gain access to A in the event that the owner loses her keys, but not otherwise. This scenario can be accommodated with pre-authorized transactions as follows.

Let s be a sequence number much higher than any that will be used in the future on A (e.g., A's current sequence number plus 2^{32}). The owner constructs the following 2 transactions:

  • The recovery transaction T_R has source account A, sequence number s+1, and minSeqAge one week. It contains a SET_OPTIONS operation giving K signing weight on A.

  • The declaration transaction T_D has source account A, sequence number s, and minSeqNum 0. It doesn't need to contain any operations, but since Stellar requires at least one operation per transaction, it contains a BUMP_SEQUENCE as a no-op.

The owner of A signs T_R and T_D, and gives them to the friend for safe keeping. If the owner loses her keys, the friend submits T_D, then a week later submits T_R, and finally uses key K to help the user recover her funds.

If T_D and K are ever compromised and an attacker unexpectedly submits T_D, then the user simply submits any transaction on A to consume sequence number s+1 and invalidate T_R.

Parallel transaction submission

A farm of 100 servers is constantly submitting transactions on the same source account, and wishes to coordinate use of sequence numbers. This can be achieved by having server number N always submit transactions with sequence numbers congruent to N modulo 100. Sending the transaction at s with minSeqNum s-99 ensures that if any of the servers do not submit transactions, the gap will not prevent other transactions from executing.

Deterministic account sequence numbers at creation

The proposed ledgerBounds field can be used to create an account with a predictable sequence number that is guaranteed if the account creation succeeds.

Assuming the user plans to create the account between ledgers 0 and N, they can specify ledgerBounds as 0 and N + 1, and include a BUMP_SEQUENCE operation that bumps the sequence of the created account to N<<32. The transaction will be guaranteed to only succeed with the created account having a sequence number of N<<32.

The sequence number is guaranteed because the account is created with a sequence number derived from the current ledger's sequence number. The BUMP_SEQUENCE operation is a no-op if the account's sequence number is greater than the bumpTo sequence number. The ledgerBounds restricts the creation to occur only up to the bumpTo to ensure that creation results with the account having the determined sequence number.

It is also possible to eliminate the BUMP_SEQUENCE operation from the transaction is a subsequent transaction uses minSeqNum with a value matching the minLedger of ledgerBounds.

This property makes it possible to setup contracts using pre-authorized transactions where the pre-authorized transaction has the created account as its source account.

Protocol Upgrade Transition

Backwards Incompatibilities

Previously signed transactions containing time points greater than 2^{63} are no longer valid with this proposal. However, given that the number 0 already represents no time bounds, this is unlikely to cause problems in practice.

The binary XDR of any other previously valid transactions will unmarshal to a valid transaction under the current proposal. Obviously legacy software will not be able to parse transactions with the new preconditions, however.

Resource Utilization

Transaction sizes will increase nominally, but only for transactions that use the new preconditions.

All account ledger entries will increase in size nominally with the addition of the account extension.

The maximum number of signatures that must be verified for each transaction will not change.

The introduction of extraSigners makes the use of SIGNER_KEY_TYPE_HASH_X signers more efficient by making them stateless, moving them from the account signers to the transaction. For any use cases utilizing this signer this may reduce the number of ledger entries and reduce the number of transactions since there would be no transactions to setup a SIGNER_KEY_TYPE_HASH_X signer before its use, and no transactions to remove it after its use.

Security Concerns

The security concerns stem primarily from new types of transaction making use of the new features. As such, the new preconditions, particularly minSeqNum, should make pre-signed transactions less brittle and simplify protocols. Nonetheless, there is still a lot of room for error in protocols.

The fact that BUMP_SEQUENCE operations are executed after all transactions have been validated leads to a counterintuitive situation in which two operations can execute in the same block although both may not succeed. This is because the BUMP_SEQUENCE can affect is the seqNum attribute of the AccountEntry. This proposal introduces two new attributes that may be affected, seqAge and seqLedgerGap. Changes in any of these fields as an effect of a BUMP_SEQUENCE may cause other transactions that passed validation to fail during apply.

An example of this is if a source account has a valid transaction with minSeqAge or minSeqLedgerGap and a second transaction, containing a BUMP_SEQUENCE that bumps the sequence of the source account, is created that is also valid. Any protocol that does this risks both transactions being accepted as valid in the same ledger. If both transactions execute in the same ledger and the bump sequence transaction is executed first, the other transaction will fail as its minSeqAge or minSeqLedgerGap will no longer be satisfied.

Any protocol that specifies for a source account a transaction with minSeqAge or minSeqLedgerGap, should not allow another transaction to be valid at the same moment unless the intent of that other transaction is to cause the first to fail or become invalid. Any transaction that is valid in the same moment as a transaction with minSeqAge or minSeqLedgerGap can cause the transaction to fail during execution even if it passed validation. Once the transaction has failed during execution it cannot be executed again as its sequence number will have been consumed.

Fortunately, it appears that in most useful protocols time-delayed "closing" transactions use a NULL minSeqNum, while transactions with non-NULL minSeqNum are "disclosure" transactions intended to be valid at any time.

The design rationale includes several multi-party protocols that require all parties to sign a transaction for it to be valid. This section does not discuss all possible security concerns with these protocols. It is at least worth noting that like most multi-party protocols there exists a period of time where a free-option may exist, where one party has authorized a transaction and another party can wait some period of time to decide if they also will authorize it, or fallback to some previously valid transaction.

Test Cases

None yet.

Implementation

None yet.