Skip to content

Commit

Permalink
feat(docs): applied structure feedback (#9288)
Browse files Browse the repository at this point in the history
Closes AztecProtocol/dev-rel#346
Closes AztecProtocol/dev-rel#415
Closes AztecProtocol/dev-rel#353
Closes AztecProtocol/dev-rel#429

---------

Co-authored-by: josh crites <jc@joshcrites.com>
Co-authored-by: josh crites <critesjosh@gmail.com>
3 people authored Dec 5, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent d3b8838 commit 5b0b721
Showing 69 changed files with 456 additions and 646 deletions.
83 changes: 7 additions & 76 deletions docs/docs/aztec/concepts/accounts/authwit.md
Original file line number Diff line number Diff line change
@@ -5,6 +5,8 @@ importance: 1
keywords: [authwit, authentication witness, accounts]
---

import Image from "@theme/IdealImage";

Authentication Witness is a scheme for authenticating actions on Aztec, so users can allow third-parties (eg protocols or other users) to execute an action on their behalf.

## Background
@@ -13,15 +15,7 @@ When building DeFi or other smart contracts, it is often desired to interact wit

In the EVM world, this is often accomplished by having the user `approve` the protocol to transfer funds from their account, and then calling a `deposit` function on it afterwards.

```mermaid
sequenceDiagram
actor Alice
Alice->>Token: approve(Defi, 1000);
Alice->>Defi: deposit(Token, 1000);
activate Defi
Defi->>Token: transferFrom(Alice, Defi, 1000);
deactivate Defi
```
<Image img={require("/img/authwit.png")} />

This flow makes it rather simple for the application developer to implement the deposit function, but does not come without its downsides.

@@ -36,16 +30,7 @@ This can lead to a series of issues though, eg:

To avoid this, many protocols implement the `permit` flow, which uses a meta-transaction to let the user sign the approval off-chain, and pass it as an input to the `deposit` function, that way the user only has to send one transaction to make the deposit.

```mermaid
sequenceDiagram
actor Alice
Alice->>Alice: sign permit(Defi, 1000);
Alice->>Defi: deposit(Token, 1000, signature);
activate Defi
Defi->>Token: permit(Alice, Defi, 1000, signature);
Defi->>Token: transferFrom(Alice, Defi, 1000);
deactivate Defi
```
<Image img={require("/img/authwit2.png")} />

This is a great improvement to infinite approvals, but still has its own sets of issues. For example, if the user is using a smart-contract wallet (such as Argent or Gnosis Safe), they will not be able to sign the permit message since the usual signature validation does not work well with contracts. [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) was proposed to give contracts a way to emulate this, but it is not widely adopted.

@@ -57,7 +42,7 @@ All of these issues have been discussed in the community for a while, and there

Adopting ERC20 for Aztec is not as simple as it might seem because of private state.

If you recall from the [Hybrid State model](../state_model/index.md), private state is generally only known by its owner and those they have shared it with. Because it relies on secrets, private state might be "owned" by a contract, but it needs someone with knowledge of these secrets to actually spend it. You might see where this is going.
If you recall from the [Hybrid State model](../storage/state_model/index.md), private state is generally only known by its owner and those they have shared it with. Because it relies on secrets, private state might be "owned" by a contract, but it needs someone with knowledge of these secrets to actually spend it. You might see where this is going.

If we were to implement the `approve` with an allowance in private, you might know the allowance, but unless you also know about the individual notes that make up the user's balances, it would be of no use to you! It is private after all. To spend the user's funds you would need to know the decryption key, see [keys for more](./keys.md).

@@ -105,32 +90,7 @@ This can be read as "defi is allowed to call token transfer function with the ar

With this out of the way, let's look at how this would work in the graph below. The exact contents of the witness will differ between implementations as mentioned before, but for the sake of simplicity you can think of it as a signature, which the account contract can then use to validate if it really should allow the action.

```mermaid
sequenceDiagram
actor Alice
participant AC as Alice Account
participant Token
Alice->>AC: Defi.deposit(Token, 1000);
activate AC
AC->>Defi: deposit(Token, 1000);
activate Defi
Defi->>Token: transfer(Alice, Defi, 1000);
activate Token
Token->>AC: Check if Defi may call transfer(Alice, Defi, 1000);
AC-->>Alice: Please give me AuthWit for DeFi<br/> calling transfer(Alice, Defi, 1000);
activate Alice
Alice-->>Alice: Produces Authentication witness
Alice-->>AC: AuthWit for transfer(Alice, Defi, 1000);
AC->>Token: AuthWit validity
deactivate Alice
Token->>Token: throw if invalid AuthWit
Token->>Token: transfer(Alice, Defi, 1000);
Token->>Defi: success
deactivate Token
Defi->>Defi: deposit(Token, 1000);
deactivate Defi
deactivate AC
```
<Image img={require("/img/authwit3.png")} />

:::info Static call for AuthWit checks
The call to the account contract for checking authentication should be a static call, meaning that it cannot change state or make calls that change state. If this call is not static, it could be used to re-enter the flow and change the state of the contract.
@@ -144,36 +104,7 @@ The above flow could be re-entered at token transfer. It is mainly for show to i

As noted earlier, we could use the ERC20 standard for public. But this seems like a waste when we have the ability to try righting some wrongs. Instead, we can expand our AuthWit scheme to also work in public. This is actually quite simple, instead of asking an oracle (which we can't do as easily because not private execution) we can just store the AuthWit in a shared registry, and look it up when we need it. While this needs the storage to be updated ahead of time (can be same tx), we can quite easily do so by batching the AuthWit updates with the interaction - a benefit of Account Contracts. A shared registry is used such that execution from the sequencers point of view will be more straight forward and predictable. Furthermore, since we have the authorization data directly in public state, if they are both set and unset (authorized and then used) in the same transaction, there will be no state effect after the transaction for the authorization which saves gas ⛽.

```mermaid
sequenceDiagram
actor Alice
participant AC as Alice Account
participant AR as Auth Registry
participant Token
participant Defi
rect rgb(191, 223, 255)
note right of Alice: Alice sends a batch
Alice->>AC: Authorize Defi to call transfer(Alice, Defi, 1000);
activate AC
Alice->>AC: Defi.deposit(Token, 1000);
end
AC->>AR: Authorize Defi to call transfer(Alice, Defi, 1000);
AR->>AR: add authorize to true
AC->>Defi: deposit(Token, 1000);
activate Defi
Defi->>Token: transfer(Alice, Defi, 1000);
activate Token
Token->>AR: Check if Defi may call transfer(Alice, Defi, 1000);
AR->>AR: set authorize to false
AR->>Token: AuthWit validity
Token->>Token: throw if invalid AuthWit
Token->>Token: transfer(Alice, Defi, 1000);
Token->>Defi: success
deactivate Token
Defi->>Defi: deposit(Token, 1000);
deactivate Defi
deactivate AC
```
<Image img={require("/img/authwit4.png")} />

### Replays

25 changes: 3 additions & 22 deletions docs/docs/aztec/concepts/pxe/index.md
Original file line number Diff line number Diff line change
@@ -6,32 +6,13 @@ keywords: [pxe, private execution environment]
importance: 1
---

import Image from "@theme/IdealImage";

The Private Execution Environment (or PXE, pronounced 'pixie') is a client-side library for the execution of private operations. It is a TypeScript library and can be run within Node, such as when you run the sandbox. In the future it could be run inside wallet software or a browser.

The PXE generates proofs of private function execution, and sends these proofs along with public function requests to the sequencer. Private inputs never leave the client-side PXE.

```mermaid
graph TD;
subgraph client[Client]
subgraph pxe [PXE]
acirSim[ACIR Simulator]
db[Database]
keyStore[KeyStore]
end
end
subgraph server[Application Server]
subgraph pxeService [PXE Service]
acctMgmt[Account Management]
contractTxInteract[Contract & Transaction Interactions]
noteMgmt[Note Management]
end
end
pxe -->|interfaces| server
```
<Image img={require("/img/pxe.png")} />

## PXE Service

5 changes: 0 additions & 5 deletions docs/docs/aztec/concepts/state_model/public_vm.md

This file was deleted.

53 changes: 0 additions & 53 deletions docs/docs/aztec/concepts/storage/index.md
Original file line number Diff line number Diff line change
@@ -10,56 +10,3 @@ In Aztec, private data and public data are stored in two trees; a public data tr
These trees have in common that they store state for _all_ accounts on the Aztec network directly as leaves. This is different from Ethereum, where a state trie contains smaller tries that hold the individual accounts' storage.

It also means that we need to be careful about how we allocate storage to ensure that they don't collide! We say that storage should be _siloed_ to its contract. The exact way of siloing differs a little for public and private storage. Which we will see in the following sections.

## Public State Slots

As mentioned in [State Model](../state_model/index.md), Aztec public state behaves similarly to public state on Ethereum from the point of view of the developer. Behind the scenes however, the storage is managed differently. As mentioned, public state has just one large sparse tree in Aztec - so we silo slots of public data by hashing it together with its contract address.

The mental model is that we have a key-value store, where the siloed slot is the key, and the value is the data stored in that slot. You can think of the `real_storage_slot` identifying its position in the tree, and the `logical_storage_slot` identifying the position in the contract storage.

```rust
real_storage_slot = H(contract_address, logical_storage_slot)
```

The siloing is performed by the [Kernel circuits](../circuits/index.md).

For structs and arrays, we are logically using a similar storage slot computation to ethereum, e.g., as a struct with 3 fields would be stored in 3 consecutive slots. However, because the "actual" storage slot is computed as a hash of the contract address and the logical storage slot, the actual storage slot is not consecutive.

## Private State Slots - Slots aren't real

Private storage is a different beast. As you might remember from [Hybrid State Model](../state_model/index.md), private state is stored in encrypted logs and the corresponding private state commitments in append-only tree where each leaf is a commitment. Being append-only, means that leaves are never updated or deleted; instead a nullifier is emitted to signify that some note is no longer valid. A major reason we used this tree, is that lookups at a specific storage slot would leak information in the context of private state. If you could look up a specific address balance just by looking at the storage slot, even if encrypted you would be able to see it changing! That is not good privacy.

Following this, the storage slot as we know it doesn't really exist. The leaves of the note hashes tree are just commitments to content (think of it as a hash of its content).

Nevertheless, the concept of a storage slot is very useful when writing applications, since it allows us to reason about distinct and disjoint pieces of data. For example we can say that the balance of an account is stored in a specific slot and that the balance of another account is stored in another slot with the total supply stored in some third slot. By making sure that these slots are disjoint, we can be sure that the balances are not mixed up and that someone cannot use the total supply as their balance.

### But how?

If we include the storage slot, as part of the note whose commitment is stored in the note hashes tree, we can _logically link_ all the notes that make up the storage slot. For the case of a balance, we can say that the balance is the sum of all the notes that have the same storage slot - in the same way that your physical \$ balance might be the sum of all the notes in your wallet.

Similarly to how we siloed the public storage slots, we can silo our private storage by hashing the logical storage slot together with the note content.

```rust
note_hash = H(logical_storage_slot, note_content_hash);
```

This siloing (there will be more) is done in the application circuit, since it is not necessary for security of the network (but only the application).
:::info
The private variable wrappers `PrivateSet` and `PrivateMutable` in Aztec.nr include the `logical_storage_slot` in the commitments they compute, to make it easier for developers to write contracts without having to think about how to correctly handle storage slots.
:::

When reading the values for these notes, the application circuit can then constrain the values to only read notes with a specific logical storage slot.

To ensure that one contract cannot insert storage that other contracts would believe is theirs, we do a second siloing by hashing the `commitment` with the contract address.

```rust
siloed_note_hash = H(contract_address, note_hash);
```

By doing this address-siloing at the kernel circuit we _force_ the inserted commitments to include and not lie about the `contract_address`.

:::info
To ensure that nullifiers don't collide across contracts we also force this contract siloing at the kernel level.
:::

For an example of this see [developer documentation on storage](../../../reference/developer_references/smart_contract_reference/storage/index.md).
Original file line number Diff line number Diff line change
@@ -35,4 +35,4 @@ This is achieved with two main features:

## Further reading

Read more about how to leverage the Aztec state model in Aztec contracts [here](../storage/index.md).
Read more about how to leverage the Aztec state model in Aztec contracts [here](../../storage/index.md).
5 changes: 5 additions & 0 deletions docs/docs/aztec/concepts/storage/state_model/public_vm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: Public VM
---

Refer to the [protocol specs section](../../../../protocol-specs/public-vm/index.md) for the latest information about the Aztec Public VM.
55 changes: 55 additions & 0 deletions docs/docs/aztec/concepts/storage/storage_slots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@

# Storage Slots

## Public State Slots

As mentioned in [State Model](../storage/state_model/index.md), Aztec public state behaves similarly to public state on Ethereum from the point of view of the developer. Behind the scenes however, the storage is managed differently. As mentioned, public state has just one large sparse tree in Aztec - so we silo slots of public data by hashing it together with its contract address.

The mental model is that we have a key-value store, where the siloed slot is the key, and the value is the data stored in that slot. You can think of the `real_storage_slot` identifying its position in the tree, and the `logical_storage_slot` identifying the position in the contract storage.

```rust
real_storage_slot = H(contract_address, logical_storage_slot)
```

The siloing is performed by the [Kernel circuits](../circuits/index.md).

For structs and arrays, we are logically using a similar storage slot computation to ethereum, e.g., as a struct with 3 fields would be stored in 3 consecutive slots. However, because the "actual" storage slot is computed as a hash of the contract address and the logical storage slot, the actual storage slot is not consecutive.

## Private State Slots

Private storage is a different beast. As you might remember from [Hybrid State Model](../storage/state_model/index.md), private state is stored in encrypted logs and the corresponding private state commitments in append-only tree, called the note hash tree where each leaf is a commitment. Append-only means that leaves are never updated or deleted; instead a nullifier is emitted to signify that some note is no longer valid. A major reason we used this tree, is that updates at a specific storage slot would leak information in the context of private state, even if the value is encrypted. That is not good privacy.

Following this, the storage slot as we know it doesn't really exist. The leaves of the note hashes tree are just commitments to content (think of it as a hash of its content).

Nevertheless, the concept of a storage slot is very useful when writing applications, since it allows us to reason about distinct and disjoint pieces of data. For example we can say that the balance of an account is stored in a specific slot and that the balance of another account is stored in another slot with the total supply stored in some third slot. By making sure that these slots are disjoint, we can be sure that the balances are not mixed up and that someone cannot use the total supply as their balance.

### Implementation

If we include the storage slot, as part of the note whose commitment is stored in the note hashes tree, we can _logically link_ all the notes that make up the storage slot. For the case of a balance, we can say that the balance is the sum of all the notes that have the same storage slot - in the same way that your physical wallet balance is the sum of all the physical notes in your wallet.

Similarly to how we siloed the public storage slots, we can silo our private storage by hashing the logical storage slot together with the note content.

```rust
note_hash = H(logical_storage_slot, note_content_hash);
```

Note hash siloing is done in the application circuit, since it is not necessary for security of the network (but only the application).
:::info
The private variable wrappers `PrivateSet` and `PrivateMutable` in Aztec.nr include the `logical_storage_slot` in the commitments they compute, to make it easier for developers to write contracts without having to think about how to correctly handle storage slots.
:::

When reading the values for these notes, the application circuit can then constrain the values to only read notes with a specific logical storage slot.

To ensure that contracts can only modify their own logical storage, we do a second siloing by hashing the `commitment` with the contract address.

```rust
siloed_note_hash = H(contract_address, note_hash);
```

By doing this address-siloing at the kernel circuit we _force_ the inserted commitments to include and not lie about the `contract_address`.

:::info
To ensure that nullifiers don't collide across contracts we also force this contract siloing at the kernel level.
:::

For an example of this see [developer documentation on storage](../../../reference/developer_references/smart_contract_reference/storage/index.md).
Loading

0 comments on commit 5b0b721

Please sign in to comment.