Skip to content

Commit

Permalink
Support for read-only accounts in transactions.
Browse files Browse the repository at this point in the history
  • Loading branch information
aeyakovenko committed Feb 21, 2019
1 parent 5492aad commit 1572819
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 0 deletions.
1 change: 1 addition & 0 deletions book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
- [Staking Rewards](staking-rewards.md)
- [Fork Selection](fork-selection.md)
- [Data Plane Fanout](data-plane-fanout.md)
- [Read-Only Read-Write Accounts](read-only-read-write-accounts.md)
- [Reliable Vote Transmission](reliable-vote-transmission.md)
- [Bank Forks](bank-forks.md)
- [Blocktree Validation](fullnode-with-ledger-notifications.md)
Expand Down
135 changes: 135 additions & 0 deletions book/src/read-only-read-write-accounts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Read-Only/Read-Write Transaction Accounts

This design covers handling of read-only and read-write accounts in the
[runtime](runtime.md). Accounts already distinguish themselves as read-only or
read-write based on the program id specified by the transaction's instruction.
Programs must treat accounts that are not owned by them as read-only.

To identify read-only accounts by program id would require the Account to be
fetched and loaded from disk. This operation is expensive and while it is
occurring the runtime would have to reject any transactions referencing the same
account.

The proposal introduces a `read_only_accounts` field to the Transaction
structure and removes the `program_ids` dedicated vector for program accounts.

This design doesn't change the runtime transaction processing rules.
Programs still can't write or spend accounts that they do not own. But it
allows the runtime to optimistically take the right lock for each account
specified in the transaction before loading the accounts from storage.

Accounts selected as read-write by the transaction can still be treated as
read-only by the instructions.

## Runtime handling

Read-only accounts have the following properties:

* Can be deposited into. Deposits can be implemented as a simple `atomic_add`.

* Read-only access to userdata.

Instructions that debit or modify the read-only account userdata will fail.

## Account Lock Optimizations

The Accounts module that keeps track of current locked accounts in the runtime
separates read-only accounts from the read-write accounts. The read-only
accounts can be cached in memory and shared between all the threads executing
transactions.

The current runtime can't predict if an account is read-only or read-write when
the transaction account keys are locked at the start of the transaction
processing pipeline. Accounts referenced by the transaction have not been
loaded from disk yet.

An ideal design would cache the read-only accounts while they are referenced by
any transaction moving through the runtime, and release the cache when the last
transaction exits the runtime.

## Transaction changes

To enable the possibility of caching accounts only while they are in the
runtime, the Transaction structure should be changed in the following way:

* `program_ids: Vec<Pubkey>` - This vector is removed. Program keys can be
placed at the end of the `account_keys` vector with the `read_only_accounts`
number set to the right number of programs.

* `read_only_accounts: u8` - The number of keys from the **end** of the
Transaction's `account_keys` array that are read-only.

The following possible accounts present in an transaction:

* paying account
* RW accounts
* RO accounts
* Program ids

The paying account must be read-write, and program ids must be read-only. The
first account in the `account_keys` array is always the account that pays for
the transaction fee, therefore it cannot be read-only. For these reasons the
read-only accounts are all grouped together at the end of the `account_keys`
vector. Counting read-only accounts from the end allows for the default `0`
value to still be functionally correct, since a transaction will succeed with
all read-write accounts.

Since accounts can only appear once in the transaction's `account_keys` array,
an account can only be read-only or read-write in a single transaction, not
both. The runtime treats a transaction as one atomic unit of execution. If any
instruction needs read-write access to an Account a copy needs to be made the
write lock needs to be held for the entire time the transaction is being
processed by the runtime the runtime.

## Starvation

Read locks can keep the runtime from executing transactions requesting a write.

When a request for a write lock is made while a read lock is open, the
transaction requesting the write lock should be cached. Upon closing the read
lock the pending transactions can be pushed through the runtime.

While a pending write transaction exists, any additional read lock requests for
that account should fail. It follows that any other write lock requests will
fail as well. Currently clients must retransmit when the transaction fails due
to a pending transaction. This approach would mimic that behavior as close as
possible while preventing write starvation.

## Program execution with read-only accounts

Before handing of the accounts to program execution, the runtime can mark each
account in each instruction as read-only. The read-only accounts can be passed
as references without an extra copy. The transaction will abort on a write to
read-only.

An alternative is to detect writes to read-only accounts and fail the
transaction before commit.

## Alternative design

This design attempts to cache a read-only account after loading without the use
of a transaction specified read-only accounts list. Instead the read-only
accounts are held in a reference counted table inside the runtime as the
transactions are processed.

1. Transaction accounts are locked.
a. If the account is present in the `read-only` table the TX does not fail.
The pending state for this TX is marked NeedReadLock.
2. Transaction accounts are loaded.
a. Transaction accounts that are read-only increase their reference
count in the `read-only` table.
b. Transaction accounts that need a write lock and are present in the
`read-only` table fail.
3. Transaction accounts are unlocked.
a. Decrement the `read-only` lock table reference count, remove if its 0
b. Remove from the `lock` set if the account is not in the `read-only`
table.

The downside with this approach is that if the `lock` set mutex is released
between lock and load to allow better pipelining of transactions, a request for
a read-only account may fail, so this design is not suitable for treating
programs as read-only accounts.

Holding the accounts lock mutex would potentially have a significant performance
hit on the runtime. Fetching from disk is expected to be slow, but can be
paralyzed between multiple disks.

0 comments on commit 1572819

Please sign in to comment.