Skip to content

Commit

Permalink
chore(docs): Cleanup voting tutorial and deployment guide (#7406)
Browse files Browse the repository at this point in the history
The voting tutorial code was recently updated, this PR updates the text.
It also updates some of the deployment docs to reference source code
instead of using hardcoded blocks.
  • Loading branch information
critesjosh authored Jul 10, 2024
1 parent ad3da14 commit 60cead2
Show file tree
Hide file tree
Showing 7 changed files with 55 additions and 85 deletions.
59 changes: 15 additions & 44 deletions docs/docs/guides/smart_contracts/how_to_deploy_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,72 +26,43 @@ aztec-nargo compile
Generate the typescript class:

```bash
aztec-builder ./aztec-nargo/output/target/path -o src/artifacts
aztec-builder codegen ./aztec-nargo/output/target/path -o src/artifacts
```

This would create a typescript file like `Example.ts` in `./src/artifacts`. Read more on the [compiling page](how_to_compile_contract.md).

Now you can import it to easily deploy and interact with the contract.
You can use the `Contract` class to deploy a contract:

```ts
import { ExampleContract } from "./target/Example.js";

const tx = ExampleContract.deploy(pxe).send();
await tx.wait({ interval: 0.5 });
const receipt = await tx.getReceipt();
const exampleContract = await ExampleContract.at(
receipt.contractAddress!,
myWallet
);
```
#include_code dapp-deploy yarn-project/end-to-end/src/sample-dapp/deploy.mjs typescript

Or you can use the generated contract class. See [below](#deploying-token-contract) for more details.

### Deploy Arguments

There are several optional arguments that can be passed:

The `deploy(...)` method is generated automatically with the typescript class representing your contract.
Its arguments are `PXE` client and contract constructor arguments.

Additionally the `.send()` method can have a few optional arguments too, which are specified in an optional object:

- `contractAddressSalt?: Fr`: A salt which is one of the inputs when computing a contract address of the contract to be deployed.
By default is set to a random value.
Set it, if you need a deterministic contract address (same functionality as Ethereum's `CREATE2` opcode).

```ts
const tx = ExampleContract.deploy(pxe).send({
contractAddressSalt: new Fr(3n),
});
```
#include_code deploy_options yarn-project/aztec.js/src/contract/deploy_method.ts typescript

### Deploying token contract

To give you a more complete example we will deploy a `Token` contract whose artifacts are included in the `@aztec/noir-contracts.js` package.

The contract has `admin` as a constructor argument.
We will deploy the contract and pass the `admin` address as an argument.

```ts
const admin = AztecAddress.from(
"0x147392a39e593189902458f4303bc6e0a39128c5a1c1612f76527a162d36d529"
);
// TokenContract is the TS interface that is automatically generated when compiling the contract with the `-ts` flag.
const contract = await TokenContract.deploy(wallet, admin).send().deployed();
logger(`Contract deployed at ${contract.address}`);
```

If everything went as expected you should see the following output (with a different address):
#include_code create_account_imports yarn-project/end-to-end/src/composed/docs_examples.test.ts raw
#include_code import_contract yarn-project/end-to-end/src/composed/docs_examples.test.ts raw
#include_code import_token_contract yarn-project/end-to-end/src/composed/docs_examples.test.ts raw

> Contract deployed at `0x151de6120ae6628129ee852c5fc7bcbc8531055f76d4347cdc86003bbea96906`
async function main(){

If we pass the salt as an argument:
#include_code full_deploy yarn-project/end-to-end/src/composed/docs_examples.test.ts raw

```ts
const contract = await TokenContract.deploy(wallet, admin)
.send({ contractAddressSalt: Fr.fromString("0x123") })
.deployed();
}
```

the resulting address will be deterministic.

> **NOTE**: You can try running the deployment with the same salt the second time in which case the transaction will fail because the address has been already deployed to.
:::note
You can try running the deployment with the same salt the second time in which case the transaction will fail because the address has been already deployed to.
:::
33 changes: 17 additions & 16 deletions docs/docs/guides/smart_contracts/writing_contracts/storage/notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,42 +41,42 @@ The address of the contract is included in a Note's data to ensure that differen

### Note types

There is more than one Note type, such as the `Set` type is used for private variables. There are also `Singleton` and `ImmutableSingleton` types.
There is more than one Note type, such as the `PrivateSet` type is used for private variables. There are also `PrivateMutable` and `PrivateImmutable` types.

Furthermore, notes can be completely custom types, storing any value or set of values that are desired by an application.

### Initialization

Private state variables are stored locally when the contract is created. Depending on the application, values may be privately shared by the creator with others via encrypted logs onchain.
A hash of a note is stored in the append-only note hash tree so as to prove existence of the current state of the note in a privacy preserving way.
A hash of a note is stored in the append-only note hash tree on the network so as to prove existence of the current state of the note in a privacy preserving way.

#### Note Hash Tree

By virtue of being append only, notes are not edited. If two transactions amend a private value, multiple notes will be inserted into the tree. The header will contain the same logical storage slot.
By virtue of being append only, notes are not edited. If two transactions amend a private value, multiple notes will be inserted into the tree (to the note hash tree and the [nullifier tree](../../../../protocol-specs/state/nullifier-tree.md)). The header will contain the same logical storage slot.

### Reading Notes

:::info

- Only those with appropriate keys/information will be able to successfully read private values that they have permission to
- Notes can be read outside of a transaction or "off-chain" with no changes to data structures on-chain
:::
Only those with appropriate keys/information will be able to successfully read private values that they have permission to. Notes can be read outside of a transaction or "off-chain" with no changes to data structures on-chain.

When a note is read in a transaction, a subsequent read from another transaction of the same note would reveal a link between the two. So to preserve privacy, notes that are read in a transaction are said to be "consumed" (defined below), and new note(s) are then created with a unique hash (includes transaction identifier).
:::

When a note is read in a transaction, a subsequent read from another transaction of the same note would reveal a link between the two. So to preserve privacy, notes that are read in a transaction are said to be "consumed" (defined below), and new note(s) are then created with a unique hash.

With type `Set`, a private variable's value is interpreted as the sum of values of notes with the same logical storage slot.
With type `PrviateSet`, a private variable's value is interpreted as the sum of values of notes with the same logical storage slot.

Consuming, deleting, or otherwise "nullifying" a note is NOT done by deleting the Note hash, this would leak information, but rather by creating a nullifier deterministically linked to the value. This nullifier is inserted into another storage tree, aptly named the nullifier tree.
Consuming, deleting, or otherwise "nullifying" a note is NOT done by deleting the Note hash; this would leak information. Rather a nullifier is created deterministically linked to the value. This nullifier is inserted into another the nullifier storage tree.

When interpreting a value, the local private execution checks that its notes (of the corresponding storage slot/ID) have not been nullified.
When reading a value, the local private execution checks that its notes (of the corresponding storage slot/ID) have not been nullified.

### Updating

:::note
Only those with appropriate keys/information will be able to successfully nullify a value that they have permission to.
:::

To update a value, its previous note hash(es) are nullified. The new note value is updated in the PXE, and the updated note hash inserted into the note hash tree.
To update a value, its previous note hash(es) are nullified. The new note value is updated in the user's private execution environment (PXE), and the updated note hash inserted into the note hash tree.

## Supplementary components

Expand All @@ -88,15 +88,16 @@ Some optional background resources on notes can be found here:

Notes touch several core components of the protocol, but we will focus on a the essentials first.

#### Some code context
### Some code context

The way Aztec benefits from the Noir language is via three important components:

- `Aztec-nr` - a Noir framework enabling contracts on Aztec, written in Noir. Includes useful Note implementations
- `Aztec.nr` - a Noir framework enabling contracts on Aztec, written in Noir. Includes useful Note implementations
- `noir contracts` - example Aztec contracts
- `noir-protocol-circuits` - a crate containing essential circuits for the protocol (public circuits and private wrappers)

A lot of what we will look at will be in [aztec-nr/aztec/src/note](https://github.com/AztecProtocol/aztec-packages/tree/#include_aztec_version/noir-projects/aztec-nr/aztec/src/note), specifically the lifecycle and note interface.

Looking at the noir circuits in these components, you will see references to the distinction between public/private execution and state.

### Lifecycle functions
Expand Down Expand Up @@ -129,12 +130,12 @@ For example in ValueNote, the `serialize_content` function simply returns: the v
### Value as a sum of Notes

We recall that multiple notes are associated with a "slot" (or ID), and so the value of a numerical note (like ValueNote) is the sum of each note's value.
The helper function in [balance_utils](https://github.com/AztecProtocol/aztec-packages/blob/#include_/noir-projects/aztec-nr/value-note/src/balance_utils.nr) implements this logic taking a `Set` of `ValueNotes`.
The helper function in [balance_utils](https://github.com/AztecProtocol/aztec-packages/blob/#include_/noir-projects/aztec-nr/value-note/src/balance_utils.nr) implements this logic taking a `PrivateSet` of `ValueNotes`.

A couple of things worth clarifying:

- A `Set` takes a Generic type, specified here as `ValueNote`, but can be any `Note` type (for all notes in the set)
- A `Set` of notes also specifies _the_ slot of all Notes that it holds
- A `PrivateSet` takes a Generic type, specified here as `ValueNote`, but can be any `Note` type (for all notes in the set)
- A `PrivateSet` of notes also specifies _the_ slot of all Notes that it holds

### Example - Notes in action

Expand Down
30 changes: 16 additions & 14 deletions docs/docs/tutorials/contract_tutorials/private_voting_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ aztec = { git="https://github.com/AztecProtocol/aztec-packages", tag="#include_a
Go to `main.nr` and delete the sample code. Replace it with this contract initialization:

```rust
contract Voting {
contract EasyPrivateVoting {

}
```
Expand All @@ -69,11 +69,12 @@ Inside this, paste these imports:

We are using various utils within the Aztec library:

- `Context` and `PrivateContext` - exposes things such as the contract address, msg_sender, etc
- `PrivateContext` - exposes things such as the contract address, msg_sender, etc
- `AztecAddress` - A type for storing an address on Aztec
- `FunctionSelector` - Used for computing a selector to call a function
- `Map` - A data storage type for storing candidates with the number of votes they have
- `PublicMutable` - A type of storage, which holds a mutable public value. We'll store votes as PublicMutables
- `SharedImmutable` - an immutable storage value that is accessible in private and public execution.

## Set up storage

Expand All @@ -84,9 +85,10 @@ Define the storage struct like so:

In this contract, we will store three vars:

1. admin, as an Aztec address held in public state
2. tally, as a map with key as the persona and value as the number (in Field) held in public state
3. vote_ended, as a boolean held in public state
1. `admin`, as an Aztec address held in public state
2. `tally`, as a map with key as the persona and value as the number (in Field) held in public state
3. `vote_ended`, as a boolean held in public state
4. `active_at_block` specifies which block people can start voting. This variable specifies the block at which people must use their nullifier secret key to vote. Because nullifier keys are rotatable, if this is not included the same account would be able to vote more than once.

## Constructor

Expand All @@ -112,7 +114,7 @@ Create a private function called `cast_vote`:

In this function, we do not create a nullifier with the address directly. This would leak privacy as it would be easy to reverse-engineer. We must add some randomness or some form of secret, like [nullifier secrets](../../aztec/concepts/accounts/keys.md#nullifier-secrets).

To do this, we make an [oracle call](../../aztec/concepts/smart_contracts/oracles/index.md) to fetch the caller's secret key, hash it to create a nullifier, and push the nullifier to Aztec. The `secret.high` and `secret.low` values here refer to how we divide a large [Grumpkin scalar](https://github.com/AztecProtocol/aztec-packages/blob/7fb35874eae3f2cad5cb922282a619206573592c/noir/noir_stdlib/src/grumpkin_scalar.nr) value into its higher and lower parts. This allows for faster cryptographic computations so our hash can still be secure but is calculated faster.
To do this, we make an [oracle call](../../aztec/concepts/smart_contracts/oracles/index.md) to fetch the caller's secret key, hash it to create a nullifier, and push the nullifier to Aztec.

After pushing the nullifier, we update the `tally` to reflect this vote. As we know from before, a private function cannot update public state directly, so we are calling a public function.

Expand All @@ -124,10 +126,10 @@ The first thing we do here is assert that the vote has not ended.

`assert()` takes two arguments: the assertion, in this case that `storage.vote_ended` is not false, and the error thrown if the assertion fails.

The code after the assertion will only run if the assertion is true. In this snippet, we read the current vote tally at the voteId, add 1 to it, and write this new number to the voteId. The `Field` element allows us to use `+` to add to an integer.
The code after the assertion will only run if the assertion is true. In this snippet, we read the current vote tally at the `candidate`, add 1 to it, and write this new number to the `candidate`. The `Field` element allows us to use `+` to add to an integer.

:::danger
Note that due to [key rotation](../../aztec/concepts/accounts/keys.md#key-rotation), it would be possible for a user to rotate their nullifier secret key and be able to vote again. Refer to [common patterns](../../guides/smart_contracts/writing_contracts/common_patterns/key_rotation.md) for more information
:::warning
Refer to [common patterns](../../guides/smart_contracts/writing_contracts/common_patterns/key_rotation.md) for more information about key rotation and considerations.
:::

## Getting the number of votes
Expand All @@ -140,13 +142,13 @@ We set it as `unconstrained` and do not annotate it because it is only reading f

## Allowing an admin to end a voting period

To ensure that only an admin can end a voting period, we can use another `assert()` statement.
To ensure that only an `admin` can end a voting period, we can use another `assert()` statement.

Paste this function in your contract:

#include_code end_vote noir-projects/noir-contracts/contracts/easy_private_voting_contract/src/main.nr rust

Here, we are asserting that the `msg_sender()` is equal to the admin stored in public state. We have to create an `AztecAddress` type from the `msg_sender()` in order to do a direct comparison.
Here, we are asserting that the `msg_sender()` is equal to the `admin` stored in public state.

## Compiling and deploying

Expand All @@ -158,13 +160,13 @@ aztec-nargo compile

This will create a new directory called `target` and a JSON artifact inside it.

Once it is compiled you can [deploy](../../reference/sandbox_reference/index.md).
Use the `aztec-builder` to generate the Typescript artifact for the contract:

```bash
aztec-builder target -o src/artifacts
aztec-builder codegen target --outdir src/artifacts
```

Once it is compiled you can [deploy](../../guides/smart_contracts/how_to_deploy_contract.md) it to the sandbox just like you did in the [counter contract tutorial](./counter_contract.md).
Once it is compiled you can [deploy](../../guides/smart_contracts/how_to_deploy_contract.md) it to the sandbox.

## Next steps

Expand Down
4 changes: 1 addition & 3 deletions docs/docs/tutorials/contract_tutorials/token_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,9 +487,7 @@ aztec-builder target -o src/artifacts

### Testing

Review the end to end tests for reference:

https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/yarn-project/end-to-end/src/e2e_token_contract/*.test.ts
Review [the end to end tests](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/yarn-project/end-to-end/src/e2e_token_contract/) for reference.

### Token Bridge Contract

Expand Down
7 changes: 1 addition & 6 deletions docs/docs/tutorials/simple_dapp/2_contract_deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,7 @@ We import the contract artifacts we have generated plus the dependencies we'll n
Note that the token's `_initialize()` method expects an `owner` address to mint an initial set of tokens to. We are using the first account from the Sandbox for this.

:::info
If you are using the generated typescript classes, you can drop the generic `ContractDeployer` in favor of using the `deploy` method of the generated class, which will automatically load the artifact for you and type-check the constructor arguments:

```typescript
await Token.deploy(client).send().wait();
```

If you are using the generated typescript classes, you can drop the generic `ContractDeployer` in favor of using the `deploy` method of the generated class, which will automatically load the artifact for you and type-check the constructor arguments. SEe the [How to deploy a contract](../../guides/smart_contracts/how_to_deploy_contract.md) page for more info.
:::

Run the snippet above as `node src/deploy.mjs`, and you should see the following output, along with a new `addresses.json` file in your project root:
Expand Down
3 changes: 2 additions & 1 deletion yarn-project/aztec.js/src/contract/deploy_method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { DeploySentTx } from './deploy_sent_tx.js';
* Options for deploying a contract on the Aztec network.
* Allows specifying a contract address salt, and additional send method options.
*/
// docs:start:deploy_options
export type DeployOptions = {
/** An optional salt value used to deterministically calculate the contract address. */
contractAddressSalt?: Fr;
Expand All @@ -35,7 +36,7 @@ export type DeployOptions = {
/** Skip contract initialization. */
skipInitialization?: boolean;
} & SendMethodOptions;

// docs:end:deploy_options
// TODO(@spalladino): Add unit tests for this class!

/**
Expand Down
4 changes: 3 additions & 1 deletion yarn-project/end-to-end/src/composed/docs_examples.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { TokenContract, TokenContractArtifact } from '@aztec/noir-contracts.js/T

describe('docs_examples', () => {
it('deploys and interacts with a token contract', async () => {
// docs:start:full_deploy
// docs:start:define_account_vars
const PXE_URL = process.env.PXE_URL || 'http://localhost:8080';
const secretKey = Fr.random();
Expand All @@ -30,14 +31,15 @@ describe('docs_examples', () => {
'TokenName', // constructor arg1
'TokenSymbol', // constructor arg2
18,
) // constructor arg3
)
.send()
.deployed();
// docs:end:deploy_contract

// docs:start:get_contract
const contract = await Contract.at(deployedContract.address, TokenContractArtifact, wallet);
// docs:end:get_contract
// docs:end:full_deploy

// docs:start:send_transaction
const _tx = await contract.methods.mint_public(wallet.getAddress(), 1).send().wait();
Expand Down

0 comments on commit 60cead2

Please sign in to comment.