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
This proposal generalizes the timeBounds
field in Transaction
to
support other conditions, including conditions that relax sequence
number checking and provide relative timelocks.
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.
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.
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.
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:
-
Transaction: For the
sourceAccount
of every executed transaction. -
BumpSequenceOp
operation: For thesourceAccount
of every successfully executedBumpSequenceOp
operation, regardless of whether theBumpSequenceOp
actually increased or modified the sequence number of the account. This allows an account to update it'sseqLedger
orseqTime
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 sourceAccount
s.
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.
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, andminSeqLedgerGap
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.
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
.
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,
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.
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 aBUMP_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). TheminSeqAge
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 moreCREATE_CLAIMABLE_BALANCE
operations disbursing funds to R, plus aSET_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.
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 aBUMP_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). TheminSeqAge
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 morePAYMENT
operations disbursing funds between escrow accounts, plusSET_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.
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 usesSET_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.
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 aSIGNER_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.
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 aSET_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 aBUMP_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.
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.
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.
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.
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.
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.
None yet.
None yet.