Skip to content

Commit

Permalink
docs: add validate warnings and refactor the account's functions page (
Browse files Browse the repository at this point in the history
…#1302)

* add validate warnings and refactor account functions page

* fix typo

* minor fixes
  • Loading branch information
ArielElp authored and stoobie committed Jul 8, 2024
1 parent bd64233 commit 4619a6c
Showing 1 changed file with 41 additions and 225 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,67 @@

== Overview

The functions in the table xref:#starknet_account_interface_functions[] are part of account contracts. Where required, you must include these functions within your contract. The logic of these functions can be mostly arbitrary, with a few limitations. For information on these limitations, see xref:#invalid_transactions[].
The functions in the table xref:#starknet_account_interface_functions[] are part of account contracts. Where required, you must include these functions within your account contract. The logic of these functions can be mostly arbitrary, with a few limitations. For information on these limitations, see xref:#limitations_of_validation[].

[#starknet_account_interface_functions]
.Starknet account interface functions
[cols="1,3"]
[cols="1,3a"]
|===
| Function name | When required

| `+__validate__+` | Always required
| `+__execute__+` | Always required
| `+__validate_declare__+` | Required for a contract to be able to send a `DECLARE` transaction.
| `+__validate_deploy__+` a| Required when deploying an instance of an account contract with a `DEPLOY_ACCOUNT` _transaction_.
| `+__execute__+` | Always required. The signatures of `+__validate__+` and `+__execute__+` must be identical.

[WARNING]
====
At the moment of writing (Starknet 0.13.2), two critical validations must happen in `+__execute__+`, and their absence can lead to draining of the account's funds:
(1) `assert!(get_caller_address().is_zero())`
This asserts that the account's `+__execute__+` is not called from another contract, thus skipping validations (in later versions we may disallow calling `__execute__` from another contract at the protocol level)
(2) `assert!(get_tx_info().unbox().version.into() >= 1_u32)`
This asserts that the transaction's version is at least 1, preventing the account from accepting `INVOKE` v0 transactions. It is critical to explicitly disallow the deprecated v0 transaction type, as v0 transactions assume that the signature verification happens in `+__execute__+`, and are thus skipping `+__validate__+` entirely.
====
| `+__validate_declare__+` | Required for the account to be able to send a `DECLARE` transaction. This function must receive exactly one argument, which is the class hash of the declared class.
| `+__validate_deploy__+` a| Required to allow deploying an instance of the account contract with a `DEPLOY_ACCOUNT` _transaction_. The arguments of `+__validate_deploy__+` must be the class hash of the account to be deployed, the salt used for computing the account's contract address, followed by the constructor arguments.

[NOTE]
====
You can only use the `+__validate_deploy__+` function in an account contract to validate the `DEPLOY_ACCOUNT` transaction for that same contract.
That is, this function runs at most once throughout the lifecycle of the account.
====
| `constructor` | All contracts have a `constructor` function. It can be explicitly defined in the contract, or if not explicitly defined, the sequencer uses a default `constructor` function, which is empty.
|===

When the sequencer receives a transaction request, it calls the corresponding validation function with the fields of the transaction request, as follows:
When the sequencer receives a transaction, it calls the corresponding validation function with the appropriate input from the transaction's data, as follows:

* For an `INVOKE` transaction, the sequencer calls the `+__validate__+` function. After successfully completing validation, the sequencer calls the `+__execute__+` function with the fields of the transaction request.
* For a `DEPLOY_ACCOUNT` transaction, the sequencer calls the `constructor` function with the fields of the transaction request, then validates the transaction by calling the `+__validate_deploy__+` function.
* For an `INVOKE` transaction, the sequencer calls the `+__validate__+` function with the transaction's calldata as input. The transaction's calldata will be deserialized to the arguments in the `+__validate__+` function's signature, it is up to the sender to make sure that the calldata is encoded appropriately according to validate's signature. After successfully completing validation, the sequencer calls the `+__execute__+` function with the same arguments.
* For a `DEPLOY_ACCOUNT` transaction, the sequencer calls the `constructor` function with the transaction's `constructor_calldata` as input (as above, it is expected that the constructor's calldata successfully desieralizes to the arguments in the constructor signature). After the successful execution of the constructor, the sequencer validates the transaction by calling the `+__validate_deploy__+` function.
* For a `DECLARE` transaction, the sequencer validates the transaction by calling the `+__validate_declare__+` function.

After successfully completing validation, the deployment is finalized.

* For more information on the available transaction types and their fields, see xref:architecture-and-concepts:network-architecture/transactions.adoc[Transaction types].
* For more information on the validation and execution stages, see xref:architecture-and-concepts:network-architecture/transaction-life-cycle.adoc[Transaction lifecycle].

Separating the validation and execution stages guarantees payment to sequencers for work completed and protects them from Denial of Service (DoS) attacks.

[#attacks_that_validation_limitations_prevent]
== Potential attacks
== Potential DoS

The validation functions have limitations, described below, that are designed to prevent the following attacks:
The validation functions have limitations, described below, that are designed to prevent the following DoS attacks on the sequencer:

* Denial of Service (DoS) attacks. Without these limitations, an attack could cause the sequencer to perform a large amount of work before a transaction fails validation, such as by sending multiple `DEPLOY_ACCOUNT` transactions that are invalid as a result of the constructor or `+__validate_deploy__+` failing. This work would not be eligible for fee payment.
* Even if the validation is simple, the following attack could still be possible:
* An attacker could cause the sequencer to perform a large amount of work before a transaction fails validation. Two examples of such attacks are:
** Spamming `INVOKE` transactions whose `+__validate__+` requires many steps, but eventually fails
** Spamming `DEPLOY_ACCOUNT` transactions that are invalid as a result of the constructor or `+__validate_deploy__+` failing.
* The above attacks are solved by making sure that the validation step is not resource-intensive, e.g. by keeping the maximal number of steps low. However, even if the validation is simple, the following "mempool pollution" attack could still be possible:
. An attacker fills the mempool with transactions that are valid at the time they are sent.
. A sequencer starts executing them, thinking that by the time it produces a block, they will still be valid.
. However, shortly after the transactions are sent, the attacker sends one transaction that somehow invalidates all the previous ones and makes sure it's included in the block, by offering higher fees for this one transaction, before the sequencer can publish the block.
* Consider many validation functions checking that the value of a storage slot is `1`, and the attacker's transaction later sets it to `0`. Restricting validation functions from calling external contracts prevents this attack.
. The sequencer is ready to execute them, thinking that by the time it includes them in a block, they will still be valid.
. Shortly after the transactions are sent, the attacker sends one transaction that somehow invalidates all the previous ones and makes sure it's included in a block, e.g. by offering higher fees for this one transaction.
An example of such an attack is having the implementation of `+__validate__+` checks that the value of a storage slot is `1`, and the attacker's transaction later sets it to `0`. Restricting validation functions from calling external contracts prevents this attack.

[#limitations_of_validation]
== Limitations on validation that prevent attacks
== Limitations on validation

The limitations listed here apply to the following validation functions:

Expand All @@ -62,20 +76,19 @@ The validation functions have the following limitations:
+
[NOTE]
====
This restriction enforces a single storage update being able to invalidate only transactions from a single account. However, be aware that an account can always invalidate its own past transactions by changing its keys.
This restriction enforces a single storage update being able to invalidate only transactions from a single account. However, be aware that an account can always invalidate its own past transactions by e.g. changing its public key.
So the fees you need to pay to invalidate transactions in the mempool are directly proportional to the number of unique accounts. For example, if the fee you need to pay to invalidate a transaction from one account is stem:[$$x$$], then the price of invalidating ten transactions from ten different accounts is stem:[$$10x$$].
This limitation implies that the fees you need to pay to invalidate transactions in the mempool are directly proportional to the number of unique accounts whose transactions you want to invalidate.
====

* The maximum number of computational steps, measured in Cairo steps, for a validation function is 1,000,000.
* A builtin can be applied a limited number of times. For specific limits for each builtin, see xref:tools:limits-and-triggers.adoc[].
* Access is restricted to `sequencer_address` in the `get_execution_info` syscall. The syscall returns zero values for `sequencer_address`.
* The values of `block_number` and `block_timestamp` in the `get_execution_info` syscall are modified as follows:
* The `get_execution_info` syscall behaves differently When raised from one of the `validate` functions:
** `sequencer_address` is set to zero
** `block_timestamp` returns the time (in UTC), rounded to the most recent hour.
** `block_number` returns the block number, rounded down to the nearest multiple of 100.
* The following syscalls cannot be called:
** `get_block_hash`
** `get_sequencer_address` `get_sequencer_address` is only supported for Cairo 0 contracts.
** `get_sequencer_address` (this syscall is only supported for Cairo 0 contracts).

[id="invalid_transactions"]
== Invalid transactions
Expand All @@ -87,205 +100,8 @@ When the `+__validate__+`, `+__validate_deploy__+`, or `+__validate_declare__+`,

A transaction has the status `REVERTED` when the `+__execute__+` function fails. A reverted transaction is included in a block, and the sequencer is eligible to charge a fee for the work done up to the point of failure, similar to Ethereum.

== Function reference

The functions in this section must be present in account contracts, as noted, with `+__execute__+` and `+__validate__+` required in all account contracts. However, you define the logic in the these functions as needed, unless noted otherwise, while adhering to the limitations specified in xref:#limitations_of_validation[]. As a result, the function descriptions below leave you room to define your own functionality.

For examples of account contracts that implement these functions, see link:https://book.starknet.io/ch04-01-accounts.html[Account Contracts] in the Starknet Book.


[id="__execute__"]
=== `+__execute__+`

[discrete]
==== Description

_Always required_

Initiates the execution stage in the sequencer. The sequencer calls this function upon receiving an `INVOKE` transaction, after the `+__validate__+` function successfully completes.

In most implementations, `+__execute__+` initiates a sequence of calls from the account.

The purpose of the `+__execute__+` function is to abstract away the remaining actions performed by a transaction.

In Ethereum, a transaction is necessarily a call to a specific function in a smart contract. With the `+__execute__+` abstraction, the account designer controls the flow of the transaction. For example, you can natively support multicalls in your account, saving the need to send multiple transactions. In practice, however, sending multiple transactions is even harder to manage without multicalls due to nonces.

[discrete]
==== Function signature

[source,cairo]
----
fn __execute__(
self: @ContractState,
<__arguments__>
) -> felt252
----


[discrete]
==== Parameters

[horizontal,labelwidth="35",role="stripes-odd"]
`self: @ContractState`:: The contract's state. If you reference a component in a separate file, use `@ComponentState<TContractState>`.
`<__arguments__>`:: Any arguments that you add.

[discrete]
==== Returns

The list of each call's serialized return value.


'''

[id="__validate__"]
=== `+__validate__+`

[discrete]
==== Description

_Always required_

Initiates the validation stage in the sequencer. Validates the sender's address. The sequencer calls this function upon receiving an `INVOKE` transaction.

In most implementations, `+__validate__+` ensures that only the account owner can initiate transactions.

The `+__validate__+` function typically ensures that any transaction submitted was indeed initiated by the account owner and therefore does not take up unjustified resources during the execution process.


Without this mechanism, a forged transaction could result in the sequencer stealing the user's funds. So the `+__validate__+` function ensures that the sequencer can only include transactions that were approved by the account owner.

The arbitrary logic allowed in the `+__validate__+` function gives the account's designer the ability to determine what it means for a transaction to be valid, enabling different signature schemes and other xref:architecture-and-concepts:accounts/introduction.adoc#examples[exotic accounts].

[discrete]
==== Function signature

[source,cairo]
----
fn __validate__(
self: @ContractState,
<__execute_arguments__>
) -> felt252
----

[discrete]
==== Parameters

[horizontal,labelwidth="35",role="stripes-odd"]
`self: @ContractState`:: The contract's state. If you reference a component in a separate file, use `@ComponentState<TContractState>`.
`<__execute_arguments__>`:: The same arguments used in the `+__execute__+` function.
== Implementation reference

[discrete]
==== Returns

If the signature is verified, the function should return the string `VALID` as `felt252` value. If not, it should return any other value, such as `0`.


'''

[id="__validate_declare__"]
=== `+__validate_declare__+`

[discrete]
==== Description

_Required for a contract to be able to send a_ `DECLARE` _transaction._

The sequencer calls this function upon receiving a `DECLARE` transaction.

If the contract declares other contracts and handles the corresponding gas fees, this function authenticates the contract declaration.

[discrete]
==== Function signature

[source,cairo]
----
fn __validate_declare__(
self: @ContractState,
class_hash: felt252
) -> felt252
----

[discrete]
==== Parameters

[horizontal,labelwidth="35",role="stripes-odd"]
`self: @ContractState`:: The contract's state. If you reference a component in a separate file, use `@ComponentState<TContractState>`.
`class_hash: felt252`:: The class hash.


[discrete]
==== Returns

If the signature is verified, the function should return the string `VALID` as `felt252` value. If not, it should return any other value, such as `0`.



'''

[id="__validate_deploy__"]
=== `+__validate_deploy__+`

[discrete]
==== Description

_Required when deploying an account with a_ `DEPLOY_ACCOUNT` _transaction_.

The sequencer calls this function upon receiving a `DEPLOY_ACCOUNT` transaction. Validates the deployment of the class referred to by the `class_hash` parameter in the transaction.

You can use this function to set up an account contract without linking it to the address that deploys it or depending on another account contract for gas fees. When determining the contract's address, use the deployer address `0x0`.

[discrete]
==== Function signature

[source,cairo]
----
fn __validate_deploy__(
self: @ContractState,
class_hash: felt252,
contract_address_salt: felt252,
<__constructor_arguments__>
) -> felt252
----

[discrete]
==== Parameters

[horizontal,labelwidth="35",role="stripes-odd"]
`self: @ContractState`:: The contract's state. If you reference a component in a separate file, use `@ComponentState<TContractState>`.
`class_hash: felt252`:: The class hash.
`contract_address_salt: felt252`:: The contract address salt.
`<__constructor_arguments__>`:: The arguments expected by the contract’s constructor. The inputs to the constructor must be identical to the rest of the inputs for validate_deploy. The compiler enforces this requirement.

[NOTE]
====
In determining the contract address, the deployer address `0x0` is used.
====

[discrete]
==== Returns

If the signature is verified, the function should return the string `VALID` as a `felt252` value. If not, it should return any other value, such as `0`.

[discrete]
==== Example

Notice how the signature of `+__validate_deploy__+` is structured to consider the signature of the constructor:

[#validate_deploy]
[source,cairo]
----
fn __validate_deploy__(
self: @ContractState,
class_hash: felt252,
salt: felt252,
public_key: felt252
) -> felt252
#[constructor]
fn constructor(ref self: ContractState, public_key: felt252)
----

[NOTE]
====
You can access the transaction hash and value for `max_fee` by getting transaction information with the xref:smart-contracts/system-calls-cairo1.adoc#get_execution_info[`get_execution_info`] system call.
====
Thanks to account abstraction, the logic of `+__execute__+` and the different validation functions is up to the party implementing the account.
To see a concrete implementation, see OpenZeppelin's link:https://github.com/OpenZeppelin/cairo-contracts/blob/v0.14.0/src/account/account.cairo#L72[account component].
This implementation adheres to link:https://github.com/starknet-io/SNIPs/blob/main/SNIPS/snip-6.md[SNIP6], which defines a standard for account interfaces.

0 comments on commit 4619a6c

Please sign in to comment.