CAP: 0012
Title: Deterministic accounts and creatorTxID
Author: David Mazières
Status: Draft
Created: 2019-01-17
Discussion: https://groups.google.com/forum/#!forum/stellar-dev
Protocol version: TBD
Allow an account to be created with deterministic sequence numbers and proofs of the creating transaction.
Allow a new type of account to be created whose name is a deterministic function of the source account and sequence number of the creating transaction and whose sequence numbers are deterministic. Such deterministic accounts also contain a creation time and the transaction id of the creating account, allowing transactions to verify that an account was created at least some time in the past and that it was created by a specific account.
The goal is to enable several usage patterns:
-
Do something only if a specific transaction has executed.
-
Sign transactions on an account that doesn't exist yet.
-
Allow arbitrarily nested pre-auth transactions that create accounts that have pre-auth transactions that create accounts and so forth.
-
If two operations in the same transaction both have such nested accounts with pre-auth transactions, the most deeply nested accounts resulting from the two operations should be able to reference each other's issued assets.
-
Require that one disclose the intent to execute a transaction some minimum time before actually executing the transaction.
-
Pay the fee for another transaction if the original transaction's fee is too low.
There are now two ways to create an account: the original
CREATE_ACCOUNT
operation and a new CREATE_DET_ACCOUNT
that creates
an account whose sequence number is deterministically initialized to
0x100000000 (2^{32}). A deterministically-created account has the
current transaction automatically added as a pre-auth transaction,
allowing the current transaction to add signers and otherwise
manipulate options on the account. The public key specified in the
account creation operation also gets added to the newly created
account with signer weight 255.
enum OperationType
{
/* ... */
BUMP_SEQUENCE = 11,
CREATE_DET_ACCOUNT = 12,
CHECK_ACCOUNT = 13
};
struct CreateDetAccountOp
{
opaque salt<32>; // unique data, if needed by application
Signer signer; // first signer to add
uint32 weight; // weight of first signer
int64 startingBalance; // initial amount to deposit in account
};
struct Operation
{
AccountID* sourceAccount;
union switch (OperationType type) {
case CREATE_DET_ACCOUNT:
CreateDetAccountOp createDetAccountOp;
case CHECK_ACCOUNT:
CheckAccountOp checkAccount;
/* ... */
} body;
};
struct CreateAccountOp {
PublicKey destination; // note: used to be AccountID, same XDR binary
int64 startingBalance; // amount they end up with
};
// This is debatable, but may be useful for ingesting accounts created
// for a particular key.
union CreateDetAccountResult switch (CreateAccountResultCode code) {
case CREATE_ACCOUNT_SUCCESS:
struct {
PublicKey signer; // "destination" in CreateAccountOp
AccountID account;
} success;
default:
void;
};
There are now two account types, depending on how the account was
created. To simplify the XDR, we also propose merging the public key,
account type, and signer type constants into a single enum
, since it
will be convenient to keep the constants distinct.
enum AccountOrSigner {
// A Key can name a signer or an account (or both)
KEY_ED25519 = 0,
// These specifiers can only designate signers
SIGNER_PRE_AUTH_TX = 1,
SIGNER_HASH_X = 2,
// These specifiers can only designate accounts
ACCT_DETERMINISTIC = 3, // The other kind of primary ID
};
union AccountID switch (AccountOrSigner type) {
case KEY_ED25519:
uint256 ed25519;
case ACCT_DETERMINISTIC:
Hash det; // Hash of CreatorSeqPayload
};
struct CreatorSeqPayload {
int version; // Must be zero
opaque salt<32>; // Salt from CreateDetAccountResult
AccountID account; // Account that created an account
SequenceNumber seqNum; // Sequence number of tx creating account
unsigned opIndex; // Index of operation that created account
};
Each newly created account now contains two extra pieces of information:
-
The transaction ID of the transaction that created the account (a hash of
TransactionSignaturePayload
for that transaction), and -
The creation time of the account (
closeTime
from the ledger in which the creation transaction ran).
struct AccountEntry {
/* ... */
// reserved for future use
union switch (int v)
{
/* ... */
case 2:
struct
{
Liabilities liabilities;
Hash creationTxID;
uint64 creationTime;
union switch (int v)
{
case 0:
void;
}
ext;
} v2;
}
ext;
};
A new CHECK_ACCOUNT
operation has no side effects, but is invalid if
the source account does not exist or does not meet certain criteria.
The CHECK_ACCOUNT
operation does not require a signature from the
operation's source account.
enum AccountConditionType {
ACC_MIN_AGE = 1,
ACC_CREATOR = 2,
ACC_MIN_SEQNO = 3,
ACC_MAX_SEQNO = 4
};
struct AccountCondition switch (AccountConditionType type) {
case ACC_AGE_MIN:
uint64 ageMin;
case ACCT_CREATOR:
// Invalid unless this matches the source account creationTxID
Hash creationTxID;
case ACC_SEQ_MIN:
// Invalid if source account's sequence number is less than this
SequenceNumber seqMin;
case ACC_SEQ_MAX:
// Invalid unless source account's sequence number is less than this
SequenceNumber seqMax;
};
typedef AccountConditionType CheckAccountOp<2>;
// Returns void, since it can never fail
Note that CHECK_ACCOUNT
affects the validity of a transaction. In
particular, a transaction is always invalid if the sourceAccount
of
a CHECK_ACCOUNT
operation does not exist or does not satisfy the
specified conditions at the time of transaction validation. Note,
however, that this is different from guaranteeing that CHECK_ACCOUNT
never fails. In particular, a set of transactions could be ordered so
as to delete or modify the sourceAccount
, making a previously valid
CHECK_ACCOUNT
operation fail. In that case the enclosing
transaction will fail, consuming a fee and sequence number.
Higher-level protocols may depend on a transaction with a
CHECK_ACCOUNT
operation not failing. To ensure the operation does
not fail, such a protocol must ensure monotonicity of the
conditions--in other words, an untrustworthy party may have the power
to make the condition true (rendering the transaction valid), but must
not subsequently have the power to make the condition false.
If transaction C refers to transaction P using an ACC_SEQ_MIN
condition, and C's sequence number is one less than seqMin
, then
any extra fees in C can contribute to executing P if P does not
have a sufficient fee
. This solves the problem of insufficient fees
on a transaction that cannot be resigned.
These mechanisms solve a bunch of issues that come up in the context of payment channels. Because there are competing proposals already (CAP-0007 and CAP-0008), this document adopts the rationale of those documents by reference unless and until the protocol working group decides to move forward with account aliases.
The data structures are all backwards compatible. However, the author
suggests moving keys, account IDs and account aliases into a single
namespace, namely the AccountOrSigner
enum. There's nothing wrong
with having unions that don't allow every value in an enum. By
contrast, it will get confusing if we use multiple enums and try to
keep all of their values in sync.
Consider a payment channel funded by an initial transaction TI, and intended to be closed by the last in a series of transactions T_1, T_2, ..., T_n. The channel will also involve a special declaration transaction TD that can be executed by any participant who wants to close the channel unilaterally. The T_i transactions can only be executed some time (e.g., 24 hours) after TD has been executed. Finally, for each T_i and user u, the participants sign a revocation transaction RT_{u,i} that allows user u to invalidate all T_j with j < i.
The participants first create TI, but do not sign it. Then they create, sign, and exchange TD, T_1, and RT_{u,1} for all u. Finally, the users sign and submit TI to place funds in escrow and make TD valid to submit.
From this point forward, at each step i, the participants create and sign T_i, and after obtaining a signed T_i, sign R_{u,i} for each user u.
To close the channel unilaterally when T_i is the latest transaction, user u submits TD and R_{u,i}. Should any user u' believe T_j is valid for j>i, that user submits RT_{u',j} so as to invalidate T_i. Finally, 24 hours after TD has executed, any user can submit T_i (or T_j if j > i).
In constructing transactions for the channel, we will use the following accounts:
-
D -- a declaration account, deterministically created from F, whose existence declares that some user has intent to close the channel.
-
E -- an escrow account deterministically created by TI, with n-of-n multisig for all parties.
-
F -- a fee account, also created by TI, also with n-of-n multisig for all parties, containing only as many XLM as channel owners are willing to pay to execute a transaction.
The transactions are then constructed as follows:
-
TI deterministically creates and funds E and F.
-
TD has source account F, sequence number 2^{32}+1, and a very high fee (so that in the event of rising fees, any user can add funds to F to make TD go through). It has the following operations:
- Deterministically create account D
- Move enough XLM from E to F for another transaction
-
T_i has source account F and sequence number 2^{32}+3+i, a very high fee, and the following operations:
- CHECK_ACCOUNT: ensure D has existed at least 24 hours
- MERGE_ACCOUNT: D into E
- MERGE_ACCOUNT: F into E
- Whatever is needed to disburse funds after termination at step i
- Note that if T_i is actually a series of k transactions, then the sequence should start at 2^{32}+3+ki and only the last transaction should merge the accounts.
-
R_{u,i} has a source account that belongs to u, a high fee (so u can increase the balance of that account as necessary to pay arbitrarily high fees), and the following operations:
- CHECK_ACCOUNT: make sure D exists
- BUMP_SEQUENCE E to 2^{32}+3+i (or 2^{32}+3+ki if k > 1)
Note that this protocol satisfies the monotonicity property: Once
account D exists, it cannot be deleted except by collaboration of all
the users. Hence, the CHECK_ACCOUNT
operations will never cause T_i
or R_{u,i} to fail, only to be invalid.
None yet.