Skip to content

Commit 4230e93

Browse files
authored
Merge pull request #14 from AztecProtocol/ek/feat/add-account-contract-and-custom-note
feat: add password account contract and custom note contract
2 parents 7debb4c + 2948731 commit 4230e93

File tree

15 files changed

+7118
-0
lines changed

15 files changed

+7118
-0
lines changed

account-contract/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

account-contract/.yarnrc.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nodeLinker: node-modules

account-contract/Nargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "custom_account"
3+
authors = [""]
4+
compiler_version = ">=1.0.0"
5+
type = "contract"
6+
7+
[dependencies]
8+
aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v3.0.0-devnet.4", directory = "noir-projects/aztec-nr/aztec" }
9+
poseidon = { tag = "v0.1.1", git = "https://github.com/noir-lang/poseidon" }

account-contract/README.md

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
# Password Account Contract
2+
3+
A custom Aztec account contract that uses password-based authentication instead of traditional signature-based authentication. This example demonstrates how to implement a custom account contract with the Aztec protocol.
4+
5+
## Overview
6+
7+
This project implements a password-protected account contract for Aztec, showcasing how to create custom authentication logic for account contracts. Instead of using cryptographic signatures, transactions are authorized using a password hashed with Poseidon2.
8+
9+
## Features
10+
11+
- **Password-based Authentication**: Uses Poseidon2 hash for secure password verification
12+
- **Custom Account Entrypoint**: Implements a custom entrypoint interface for transaction execution
13+
- **Fee Payment Support**: Supports multiple fee payment methods (external, pre-existing FeeJuice, FeeJuice with claim)
14+
- **Authorization Witnesses**: Implements authwit verification for cross-contract calls
15+
- **Cancellable Transactions**: Optional transaction cancellation through nullifiers
16+
- **TypeScript Integration**: Complete TypeScript SDK for deployment and interaction
17+
18+
## Contract Architecture
19+
20+
### Core Contract (`PasswordAccount`)
21+
22+
The main contract implements:
23+
24+
- **constructor(password: Field)**: Initializes the account with a hashed password
25+
- **entrypoint(...)**: Main entrypoint for executing transactions with password authentication
26+
- **verify_private_authwit(...)**: Verifies authorization witnesses for cross-contract calls
27+
- **lookup_validity(...)**: Unconstrained function to check authwit validity
28+
29+
### Storage
30+
31+
```noir
32+
struct Storage<Context> {
33+
hashed_password: PublicImmutable<Field, Context>,
34+
}
35+
```
36+
37+
The contract stores only the Poseidon2 hash of the password in public state.
38+
39+
### Account Actions
40+
41+
The `AccountActions` module provides:
42+
43+
- Transaction entrypoint logic with fee payment handling
44+
- Authorization witness verification
45+
- Support for cancellable transactions via nullifiers
46+
47+
## TypeScript Integration
48+
49+
### PasswordAccountContract
50+
51+
Implements the `AccountContract` interface for easy deployment:
52+
53+
```typescript
54+
const passwordAccountContract = new PasswordAccountContract(
55+
new Fr(your_password)
56+
);
57+
```
58+
59+
### PasswordAccountInterface
60+
61+
Provides the account interface for creating transactions:
62+
63+
```typescript
64+
const accountInterface = new PasswordAccountInterface(
65+
authWitnessProvider,
66+
address,
67+
chainInfo,
68+
password
69+
);
70+
```
71+
72+
### PasswordAccountEntrypoint
73+
74+
Handles transaction construction with custom entrypoint parameters:
75+
76+
```typescript
77+
const entrypoint = new PasswordAccountEntrypoint(
78+
address,
79+
auth,
80+
password,
81+
chainId,
82+
version
83+
);
84+
```
85+
86+
## Building
87+
88+
Compile the Noir contract:
89+
90+
```bash
91+
aztec-nargo compile
92+
```
93+
94+
Install TypeScript dependencies:
95+
96+
```bash
97+
yarn install
98+
```
99+
100+
Start the local network:
101+
102+
```bash
103+
aztec start --local-network
104+
```
105+
106+
Deploy the account contract to the local network:
107+
108+
```bash
109+
npx tsx deploy-account-contract.ts
110+
```
111+
112+
### Use the account contract as normal
113+
114+
## Security Considerations
115+
116+
- The password is hashed using Poseidon2 before storage
117+
- Password is required for every transaction (no caching)
118+
- Password is included in transaction data (encrypted in private state)
119+
- This is a demonstration contract - production use should consider additional security measures
120+
- Consider using signature-based accounts for most production use cases
121+
122+
## Important Considerations
123+
124+
When implementing custom account contracts in Aztec, be aware of these critical points:
125+
126+
### All Execution Starts in Private
127+
128+
**This is the most important gotcha**: In Aztec, all transaction execution begins in the private context, even if your contract only has public functions. The account contract's `entrypoint` function always executes in private first.
129+
130+
- Your `entrypoint` function must be a `private` or `unconstrained private` function
131+
- Even when calling public functions on other contracts, the call originates from private execution
132+
- Authentication logic in the entrypoint runs in the private context
133+
- If you need to validate anything on-chain, you must enqueue public calls and handle them accordingly
134+
135+
### Password/Secret Storage
136+
137+
- **Never store passwords in plain text**: Always hash sensitive data before storage (like we do with Poseidon2)
138+
- The `hashed_password` is stored in `PublicImmutable` storage, meaning it's visible on-chain but cannot be changed
139+
- Consider whether your authentication secret should be changeable (would require mutable storage)
140+
141+
### Entrypoint Function Signature
142+
143+
- The entrypoint must match the expected signature for account contracts
144+
- It receives the payload (functions to call) and fee payment options
145+
146+
### State Management
147+
148+
- Private state is encrypted and only visible to those with the viewing key
149+
- Public state is visible to everyone on-chain
150+
- Choose storage types carefully: `PublicImmutable`, `PublicMutable`, `PrivateImmutable`, `PrivateMutable`, `PrivateSet`, etc.
151+
- Changing storage types after deployment requires a new contract deployment
152+
153+
### Transaction Construction
154+
155+
- Account contracts need TypeScript integration for proper transaction construction
156+
- You must implement the `AccountContract`, `AccountInterface`, and custom entrypoint classes
157+
- The entrypoint class handles encoding your authentication mechanism into the transaction payload
158+
- Mismatches between Noir and TypeScript implementations will cause authentication failures
159+
160+
### Testing and Debugging
161+
162+
- Private execution errors can be harder to debug since execution details aren't always visible
163+
- Test thoroughly with different fee payment methods
164+
- Ensure your authentication mechanism works for both direct calls and authwit flows
165+
166+
### Gas and Fee Considerations
167+
168+
- Account contracts are responsible for paying transaction fees
169+
- You must handle the fee payment method selection properly
170+
- Failed fee payments will cause the entire transaction to fail
171+
- Consider how users will fund their account contracts with Fee Asset
172+
173+
## Dependencies
174+
175+
### Noir Dependencies
176+
177+
- **aztec**: v3.0.0-devnet.4
178+
- **poseidon**: v0.1.1
179+
180+
### TypeScript Dependencies
181+
182+
- **@aztec/aztec.js**: 3.0.0-devnet.4
183+
- **@aztec/accounts**: 3.0.0-devnet.4
184+
- **@aztec/stdlib**: 3.0.0-devnet.4
185+
- **@aztec/entrypoints**: Included in aztec.js
186+
187+
## Project Structure
188+
189+
```
190+
account-contract/
191+
├── Nargo.toml # Noir project configuration
192+
├── package.json # Node.js dependencies and scripts
193+
├── src/
194+
│ ├── main.nr # Main contract implementation
195+
│ └── account_actions.nr # Account action handlers
196+
└── ts/
197+
├── deploy-account-contract.ts # Deployment script
198+
├── password-account-entrypoint.ts # TypeScript entrypoint implementation
199+
└── password-account-contract-artifact.ts # Contract artifact loader
200+
```
201+
202+
## Learn More
203+
204+
- [Aztec Account Contracts](https://docs.aztec.network/developers/contracts/writing_contracts/accounts)
205+
- [Account Abstraction in Aztec](https://docs.aztec.network/concepts/accounts/main)
206+
- [Aztec Documentation](https://docs.aztec.network/)
207+
- [Noir Language Documentation](https://noir-lang.org/)
208+
209+
## Notes
210+
211+
- This is an educational example demonstrating custom account contract patterns
212+
- For production use, consider using the standard ECDSA or Schnorr signature-based accounts
213+
- The password-based approach trades cryptographic security guarantees for simplicity

account-contract/package.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "account-contract",
3+
"type": "module",
4+
"scripts": {},
5+
"devDependencies": {
6+
"@jest/globals": "^29.0.0",
7+
"@swc/core": "^1.3.0",
8+
"@swc/jest": "^0.2.0",
9+
"@types/jest": "^29.0.0",
10+
"@types/node": "^20.0.0",
11+
"jest": "^29.0.0",
12+
"typescript": "^5.0.0"
13+
},
14+
"dependencies": {
15+
"@aztec/accounts": "3.0.0-devnet.4",
16+
"@aztec/aztec.js": "3.0.0-devnet.4",
17+
"@aztec/foundation": "3.0.0-devnet.4",
18+
"@aztec/noir-contracts.js": "3.0.0-devnet.4",
19+
"@aztec/stdlib": "3.0.0-devnet.4",
20+
"@aztec/test-wallet": "3.0.0-devnet.4",
21+
"tsx": "^4.20.6"
22+
}
23+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use dep::aztec::context::PrivateContext;
2+
3+
use dep::aztec::protocol_types::{
4+
constants::GENERATOR_INDEX__TX_NULLIFIER, hash::poseidon2_hash_with_separator, traits::Hash,
5+
};
6+
7+
use dep::aztec::authwit::auth::{compute_authwit_message_hash, IS_VALID_SELECTOR};
8+
use dep::aztec::authwit::entrypoint::app::AppPayload;
9+
10+
pub struct AccountActions<Context> {
11+
context: Context,
12+
is_valid_impl: fn(&mut PrivateContext, Field, Field) -> bool,
13+
}
14+
15+
impl<Context> AccountActions<Context> {
16+
pub fn init(context: Context, is_valid_impl: fn(&mut PrivateContext, Field, Field) -> bool) -> Self {
17+
AccountActions { context, is_valid_impl }
18+
}
19+
}
20+
21+
// See AccountFeePaymentMethodOptions enum in Aztec.js for docs:
22+
// https://github.com/AztecProtocol/aztec-packages/blob/next/yarn-project/entrypoints/src/account_entrypoint.ts
23+
pub struct AccountFeePaymentMethodOptionsEnum {
24+
pub EXTERNAL: u8,
25+
pub PREEXISTING_FEE_JUICE: u8,
26+
pub FEE_JUICE_WITH_CLAIM: u8,
27+
}
28+
29+
pub global AccountFeePaymentMethodOptions: AccountFeePaymentMethodOptionsEnum = AccountFeePaymentMethodOptionsEnum {
30+
EXTERNAL: 0,
31+
PREEXISTING_FEE_JUICE: 1,
32+
FEE_JUICE_WITH_CLAIM: 2,
33+
};
34+
35+
/**
36+
* An implementation of the Account Action struct for the private context.
37+
*
38+
* Implements logic to verify authorization and execute payloads.
39+
*/
40+
impl AccountActions<&mut PrivateContext> {
41+
42+
/// Verifies that the `app_hash` is authorized and executes the `app_payload`.
43+
///
44+
/// @param app_payload The payload that contains the calls to be executed in the app phase.
45+
///
46+
/// @param fee_payment_method The mechanism via which the account contract will pay for the transaction:
47+
/// - EXTERNAL (0): Signals that some other contract is in charge of paying the fee, nothing needs to be done.
48+
/// - PREEXISTING_FEE_JUICE (1): Makes the account contract publicly pay for the transaction with its own FeeJuice
49+
/// balance, which it must already have prior to this transaction. The contract will
50+
/// set itself as the fee payer and end the setup phase.
51+
/// - FEE_JUICE_WITH_CLAIM (2): Makes the account contract publicly pay for the transaction with its own FeeJuice
52+
/// balance which is being claimed in the same transaction. The contract will set
53+
/// itself as the fee payer but not end setup phase - this is done by the FeeJuice
54+
/// contract after enqueuing a public call, which unlike most public calls is
55+
/// whitelisted to be executable during setup.
56+
///
57+
/// @param cancellable Controls whether to emit app_payload.tx_nonce as a nullifier, allowing a subsequent
58+
/// transaction to be sent with a higher priority fee. This can be used to cancel the first transaction sent,
59+
/// assuming it hasn't been mined yet.
60+
///
61+
// docs:start:entrypoint
62+
pub fn entrypoint(self, app_payload: AppPayload, fee_payment_method: u8, cancellable: bool, password: Field) {
63+
let valid_fn = self.is_valid_impl;
64+
65+
assert(valid_fn(self.context, app_payload.hash(), password));
66+
67+
if fee_payment_method == AccountFeePaymentMethodOptions.PREEXISTING_FEE_JUICE {
68+
self.context.set_as_fee_payer();
69+
self.context.end_setup();
70+
}
71+
if fee_payment_method == AccountFeePaymentMethodOptions.FEE_JUICE_WITH_CLAIM {
72+
self.context.set_as_fee_payer();
73+
}
74+
app_payload.execute_calls(self.context);
75+
76+
if cancellable {
77+
let tx_nullifier = poseidon2_hash_with_separator(
78+
[app_payload.tx_nonce],
79+
GENERATOR_INDEX__TX_NULLIFIER,
80+
);
81+
self.context.push_nullifier(tx_nullifier);
82+
}
83+
}
84+
85+
/// Verifies that the `msg_sender` is authorized to consume `inner_hash` by the account.
86+
///
87+
/// Computes the `message_hash` using the `msg_sender`, `chain_id`, `version` and `inner_hash`.
88+
/// Then executes the `is_valid_impl` function to verify that the message is authorized.
89+
///
90+
/// Will revert if the message is not authorized.
91+
///
92+
/// @param inner_hash The hash of the message that the `msg_sender` is trying to consume.
93+
pub fn verify_private_authwit(self, inner_hash: Field, password: Field) -> Field {
94+
// The `inner_hash` is "siloed" with the `msg_sender` to ensure that only it can
95+
// consume the message.
96+
// This ensures that contracts cannot consume messages that are not intended for them.
97+
let message_hash = compute_authwit_message_hash(
98+
self.context.msg_sender().unwrap(),
99+
self.context.chain_id(),
100+
self.context.version(),
101+
inner_hash,
102+
);
103+
let valid_fn = self.is_valid_impl;
104+
assert(valid_fn(self.context, message_hash, password), "Message not authorized by account");
105+
IS_VALID_SELECTOR
106+
}
107+
}

0 commit comments

Comments
 (0)