Skip to content

Latest commit

 

History

History
334 lines (267 loc) · 11.1 KB

cap-0012.md

File metadata and controls

334 lines (267 loc) · 11.1 KB

Preamble

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

Simple Summary

Allow an account to be created with deterministic sequence numbers and proofs of the creating transaction.

Abstract

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.

Motivation

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.

Specification

Deterministic accounts

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;
};

Modifications to AccountID

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
};

Changes to AccountEntry

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;
};

CHECK_ACCOUNT

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.

Rationale

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.

Backwards Compatibility

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.

Example

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.

Implementation

None yet.