Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SIMD-0082: Relax Transaction Constraints #82

Closed
312 changes: 312 additions & 0 deletions proposals/0082-relax-transaction-constraints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
---
simd: '0082'
title: Relax Transaction Constraints
authors:
- Andrew Fitzgerald (Solana Labs)
category: Standard
type: Core
status: Draft
created: 2023-10-30
feature:
---

## Summary

Transaction errors fall into two categories: protocol violating errors and
runtime errors.
Protocol violation errors are those that break some constraint of the protocol,
and any blocks containing such transactions must be rejected by the network.
Runtime errors are those that occur during the processing of a transaction, and
generally these errors mean that state changes are limited to a few accounts,
but the transaction itself may be included in a block.
Any block containing a transaction that results in a runtime error is still
valid.
This proposal aims to change several protocol violation errors into runtime
errors, in order to simplify the protocol, and give more flexibility to
block-producer and block-validator implementations.

## Motivation

The current protocol places many constraints on the structure and contents
of blocks; if any constraints are broken, block-validators will mark the
entire block as invalid.
Many of these constraints are necessary, but some of them are not, and lead to
additional complexity in the protocol.
This proposal aims to relax some of the constraints at the individual
transaction level, in order to simplify the protocol, and give more flexibility
to block-producer and block-validator implementations.

More specifically, this proposal changes several protocol violation errors into
runtime errors.
This means that transactions that would previously be dropped with an error,
can now be included in a block, but will either not be executed at all, or
will have limited state changes.
The goal is to remove much of the reliance on account-state in order to
validate a block.
This proposal on its' own, will not enable asynchronous execution, but it will
remove one of the barriers to asynchronous execution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit confused by "aim" in Summary section and Motivations here. As "aim" seems to yield benefit to block producer ("...transaction can be included..."), which is not part of protocol; while Motivation talks about modifying protocol.

Is it correct yo say this proposal has two parts:

  1. remove listed constraints/requirement of transaction for block producer to include them into block; this part solely in Producer implementation, does not modify protocol;
  2. instead of invalidate entire block at earlist failure of transaction due to named constraints, validator only not execute bad transactions but will continue validate the block. (bad transactions included in ledger). This part of protocol altering.

or maybe I am thinking on wrong track?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think only your 2nd point is included in the proposal. Block-production changse are enabled by the changes in the proposal, but not required beyond potential changes to the fee-colection.

instead of invalidate entire block at earlist failure of transaction due to named constraints, validator only not execute bad transactions but will continue validate the block. (bad transactions included in ledger).

yes. Those bad transactions may end up having limited effect if we are still able to collect fees, or if we decide to make the block-producer cover those fees. They will not be executed though.


Proposal does not suggest or require the block-producer to make any changes, but allows for changes. For several of the constraints, the producer is still incentivized to perform the checks (fee-payer, nonce) because otherwise they will not receive rewards for those transactions. There's freedom in how that is done, and the block-producer could potentially do this in a non-strict fashion.

I'll cover this in the updated prop, but essentially block-validation and consensus really consists of 2 primary questions:

  1. is this block valid?
  2. what is the result of the block?

The changes here are intending to make answering this first question, "is this block valid?", a simpler process that does not require executing the block. This proposal separates these questions logically, but w/ current voting we still require the 2nd question, "what is the result of the block?", be answered synchronously.

## Alternatives Considered

1. Do nothing
- This is the simplest option, as we could leave the protocol as is.
However, this leaves the protocol more complex than it needs to be.
2. Also relax fee-paying constraint
- This was considered, and included in the intially reviewed proposal.
However, this was decided to be moved to a separate follow-up proposal,
should this one be accepted.
There was significant disagreements on how exactly transactions that cannot
pay fees should be handled.
By keeping fee-paying as a constraint for the current proposal, it will
allow this proposal to be accepted more quickly, which will give
significant benefits to the network.
3. Additionally, relax the address lookup table resolution constraint
- This was considered, since it is a transaction-level constraint that is
dependent on account-state. However, due to entry-level and block-level
constraints that rely on the address lookup table resolution, this
constraint cannot easily be relaxed without also relaxing those
constraints.

## New Terminology

None

## Detailed Design

### Current Protocol-Violation Errors

Prior to this change, the list of protocol violation errors at the individual
transaction level are:

1. Transaction must be deserializable via `bincode` (or equivalent) into a structure:

```rust
pub struct VersionedTransaction {
#[serde(with = "short_vec")]
pub signatures: Vec<Signature>,
pub message: VersionedMessage,
}
```

where `Signature` and `VersionedMessage` are defined in `solana-sdk`.
2. Transaction serialized size must be less than or equal to 1232 bytes.
3. Transaction signatures must be valid and in the same order as the static
account keys in the `VersionedMessage`.
4. Transaction must have exactly the number of required signatures from the
`VersionedMessage` header.
5. Transaction must not have more signatures than static account keys.
6. The `VersionedMessage` must pass sanitization checks:
- The sum of the number of required signatures and the number of read-only
unsigned accounts must not be greater than the number of static account
keys.
- The number of readonly signed accounts must be less than the number of
required signatures.
- Each lookup table, if present, must be used to load at least one account.
- The total number of accounts, static or dynamic, must be less than 256.
- Each instruction's `program_id_index` must be less than the number of
static account keys.
- Each instruction's `program_id_index` must not be the payer index (0).
- All account indices in instructions must be less than the number of total
accounts.
7. Transactions that use address lookup tables must be resolvable:
- The address lookup table account must exist.
- The address lookup table account must be owned by the address lookup
table program: `AddressLookupTab1e1111111111111111111111111`
- The address lookup table account data must be deserializable into
`AddressLookupTable` as defined in `solana-sdk`.
- All account table indices specified in the transaction must be less than
the number of active addresses in the address lookup table.
8. Transactions containing pre-compile instructions must pass pre-compile
verification checks.
9. The transaction must not load the same account more than once.
10. The transaction must have fewer than 64 accounts.
- The limit is subject to change to 128 with the activation of
`9LZdXeKGeBV6hRLdxS1rHbHoEUsKqesCC2ZAPTPKJAbK`.
11. The `recent_blockhash` of the transaction message must be valid:
- It must exist and not have an age greater than 150.
- OR the transaction must be a nonced transaction, and the nonce
account must exist and be valid for the given `recent_blockhash`.
12. The transaction must not have already been processed.
13. The transaction fee-payer account must:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need a section talking about durable nonce transactions. They must:

  • use a recent blockhash value different from the durable nonce for the current bank
  • have at least one transaction instruction, the first of which is designated the nonce advance instruction which must:
    • invoke the system program 11111111111111111111111111111111
    • have instruction data that deserializes to the SystemInstruction::AdvanceNonceAccount variant
    • have at least one account input, the first of which is designated the nonce address/account which must:
      • be loaded with a write-lock
      • be owned by the system program: 11111111111111111111111111111111
      • be deserializable to non-legacy initialized nonce state
      • have a durable nonce hash equal to the transaction's recent blockhash field
  • be signed by the nonce authority deserialized from the nonce account

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

- exist
- be owned by the system program: `11111111111111111111111111111111`
- have more lamports than the fee
- have more lamports than the fee plus the minimum balance
14. The total loaded data size of the transaction must not exceed
`requested_loaded_accounts_data_size_limit`, or the default limit (64MB).
15. Any account used as a program in a top-level instruction must:
- be the native loader: `NativeLoader1111111111111111111111111111111`
- OR
- exist
- be executable
- be owned by the native loader: `NativeLoader1111111111111111111111111111111`
- OR
- exist
- be executable
- the owner account be owned by the native loader: `NativeLoader1111111111111111111111111111111`
- the owner account must be executable
16. Durable nonce transactions must:
- use a recent blockhash value different from the durable nonce for the
current bank
- have at least one transaction instruction, the first of which is
designated the nonce advance instruction which must:
- invoke the system program `11111111111111111111111111111111`
- have instruction data that deserializes to the
`SystemInstruction::AdvanceNonceAccount` variant
- have at least one account input, the first of which is designated the
nonce address/account which must:
- be loaded with a write-lock
- be owned by the system program: `11111111111111111111111111111111`
- be deserializable to non-legacy initialized nonce state
- have a durable nonce hash equal to the transaction's recent
blockhash field
- be signed by the nonce authority deserialized from the nonce account

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a complete test suite that tests every single one of these constraints (via txn execution) so that we can also test the behavior when the feature flag is flipped to make sure we relax all the correct constraints

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, any client should be testing against the protocol, and those tests much change if the protocol changes.

There should already (big should in our codebase...) be tests for all these cases, as they are currently protocol violations, that we must check for.
Tests can be modified to use feature-flag which would change the result of processing pipeline from rejected -> not executed.

### Proposed Protocol-Violation Errors

The proposal is to move some items of the above list from protocol violations
to runtime errors.
Specifically, the following constraints will remain as protocol violations:

1. Transaction must be deserializable via `bincode` (or equivalent) into a structure:

```rust
pub struct VersionedTransaction {
#[serde(with = "short_vec")]
pub signatures: Vec<Signature>,
pub message: VersionedMessage,
}
```

where `Signature` and `VersionedMessage` are defined in `solana-sdk`.
2. Transaction serialized size must be less than or equal to 1232 bytes.
3. Transaction signatures must be valid and in the same order as the static
account keys in the `VersionedMessage`.
Comment on lines +183 to +184
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From conversation in #159 we should only be requiring the fee payer signature to be valid

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but that was a separate change not part of this SIMD.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok maybe rather than having a single all encompassing SIMD for transaction constraints, we limit this one to account loading related constraints?

4. Transaction must have exactly the number of required signatures from the
`VersionedMessage` header.
5. Transaction must not have more signatures than static account keys.
6. The `VersionedMessage` must pass sanitization checks:
- The sum of the number of required signatures and the number of read-only
unsigned accounts must not be greater than the number of static account
keys.
- The number of readonly signed accounts must be less than the number of
required signatures.
- Each lookup table, if present, must be used to load at least one account.
- The total number of accounts, static or dynamic, must be less than 256.
- Each instruction's `program_id_index` must be less than the number of
static account keys.
- Each instruction's `program_id_index` must not be the payer index (0).
- All account indices in instructions must be less than the number of total
accounts.
7. Transactions that use address lookup tables must be resolvable:
- The address lookup table account must exist.
- The address lookup table account must be owned by the address lookup
table program: `AddressLookupTab1e1111111111111111111111111`
- The address lookup table account data must be deserializable into
`AddressLookupTable` as defined in `solana-sdk`.
- All account table indices specified in the transaction must be less than
the number of active addresses in the address lookup table.
Comment on lines +201 to +208
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be moved to the runtime error section. It's pretty straightforward to handle transactions with invalid address lookup tables in replay.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plan was for this to be in a separate follow-up SIMD since the choice here is very much not obvious imo.

How do we apply CUs to account-level cost if we cannot resolve ALTs?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with it being covered in a separate SIMD

8. The `recent_blockhash` of the transaction message must be valid:
- It must exist and not have an age greater than 150.
- OR the transaction must be a nonced transaction, and the nonce
account must exist and be valid for the given `recent_blockhash`.
9. The transaction must not have already been processed.
10. The transaction fee-payer account must:
- exist
- be owned by the system program: `11111111111111111111111111111111`
- have more lamports than the fee
- have more lamports than the fee plus the minimum balance
11. Durable nonce transactions must:
- use a recent blockhash value different from the durable nonce for the
current bank
- have at least one transaction instruction, the first of which is
designated the nonce advance instruction which must:
- invoke the system program `11111111111111111111111111111111`
- have instruction data that deserializes to the
`SystemInstruction::AdvanceNonceAccount` variant
- have at least one account input, the first of which is designated the
nonce address/account which must:
- be loaded with a write-lock
- be owned by the system program: `11111111111111111111111111111111`
- be deserializable to non-legacy initialized nonce state
- have a durable nonce hash equal to the transaction's recent
blockhash field
- be signed by the nonce authority deserialized from the nonce account

### Proposed New Runtime Errors

The following constraints will be moved to become runtime errors.
Transactions that break these constraints may be included in a block.
The fee-payer account must be charged fees, nonce advanced, but the transaction
will not be executed.

1. Transactions containing pre-compile instructions must pass pre-compile
verification checks.
2. The transaction must not load the same account more than once.
3. The transaction must have fewer than 64 accounts.
- The limit is subject to change to 128 with the activation of
`9LZdXeKGeBV6hRLdxS1rHbHoEUsKqesCC2ZAPTPKJAbK`.
4. The total loaded data size of the transaction must not exceed
`requested_loaded_accounts_data_size_limit`, or the default limit (64MB).
5. Any account used as a program in a top-level instruction must:
- be the native loader: `NativeLoader1111111111111111111111111111111`
- OR
- exist
- be executable
- be owned by the native loader: `NativeLoader1111111111111111111111111111111`
- OR
- exist
- be executable
- the owner account be owned by the native loader: `NativeLoader1111111111111111111111111111111`
- the owner account must be executable

apfitzge marked this conversation as resolved.
Show resolved Hide resolved
### Block-Limits

This proposal allows for transactions which do not execute to be included in a
block.
This was not previously possible, so how transaction costs are applied to
limits must be clarified.
This proposal proposes that an included unexecuted transaction should have all
non-execution transaction costs applied towards block-limits, as well as all
the writable account limits.

### Rationale

The intent with relaxing these constraints is to reduce the amount of
account-state which is required in order to validate a block.
This gives more flexibility to when and how account-state is updated during
both block production and validation.
Particularly with the relaxation of the program account constraints,
block-packing can be done without needing to load large program accounts
for the initial decision to include or not.
This is a major step towards asynchronous block production and validation.

Any transaction included in the block, regardless of whether or not the
transaction is executed, must be charged fees, inserted into the status cache,
and the nonce account, if present, must be advanced.

## Impact

- Transactions that would previously be dropped with an error, can now be
included and will be charged fees.
- Users must be more careful when constructing transactions to ensure they
are executable if they don't want to waste fees
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are there additional tools (or simulator enhancement) needed for transaction submitters ensure their transactions against those relaxed constraints?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simulation doesn't change. It currently would tell you that your transaction had an error. That error doesn't change, it only changes what will happen to a block it may be included in.

For example, if a transaction had an invalid program account, simulation will still return ProgramAccountNotFound error.
If the user simulates, and then still submits that transaction, then they probably deserve to be charged, imo.

- Block-production is simplified as it can be done without needing to load
large program accounts for the initial decision to include or not.

## Security Considerations
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could any other relaxed constraints, in addition to relaxing "fee-payer requirement", lead to similar dos vector, if block producer build with bad transactions?

Is it necessary to charge transactions failed due to those relaxed constraints by its transaction base fee?


None

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, malicious leaders could create full of garbage blocks, which only contains bad transactions at no cost, burdening all the validations onto other validators, esp, heavy account state lookup.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed with @ryoqun, this attack vector is the main thing that needs to be solved. Maybe this specific problem can be discussed in a separate SIMD since relaxing other transaction constraints is more straightforward.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made a comment in this chain: #82 (comment)

I don't think there's a DOS that's creating more work than would be possible within current protocol.
I can currently create a block thats full of valid heavy transactions, and then tag in some invalidating transaction at the last tick. All the validators wasted their time executing all those txs, and there's no reward for them since the block is thrown out entirely.

This would change the vector, since they could have all txs that do not pay fees. But, that only makes the execution less intense, since they still have a cost that counts towards block limits.

## Drawbacks

- Users must be more careful about what they sign, as they will be charged fees
for transactions that are included in a block, even if they are not executed.
- This will likely break a lot of tooling, such as explorers, which may expect
all transactions to attempt execution.

## Backwards Compatibility

This proposal is backwards compatible with the current protocol, since it only
relaxes constraints, and does not add any new constraints. All previously valid
blocks would still be valid.
Loading