Wallet Architecture
This page talks about the architecture of a wallet in Aztec. Wallets expose to dapps an interface that allows them to act on behalf of the user, such as querying private state or sending transactions. Bear in mind that, as in Ethereum, wallets should require user confirmation whenever carrying out a potentially sensitive action requested by a dapp.
Overview
Architecture-wise, a wallet is an instance of an Private Execution Environment (PXE) which manages user keys and private state. The PXE also communicates with an Aztec Node for retrieving public information or broadcasting transactions. Note that the PXE requires a local database for keeping private state, and is also expected to be continuously syncing new blocks for trial-decryption of user notes.
Additionally, a wallet must be able to handle one or more account contract implementation. When a user creates a new account, the account is represented on-chain by an account contract. The wallet is responsible for deploying and interacting with this contract. A wallet may support multiple flavours of accounts, such as an account that uses ECDSA signatures, or one that relies on WebAuthn, or one that requires multi-factor authentication. For a user, the choice of what account implementation to use is then determined by the wallet they interact with.
In code, this translates to a wallet implementing an AccountInterface interface that defines how to create an execution request out of an array of function calls for the specific implementation of an account contract and how to generate an auth witness for authorizing actions on behalf of the user. Think of this interface as the Javascript counterpart of an account contract, or the piece of code that knows how to format a transaction and authenticate an action based on the rules defined by the user's account contract implementation.
Account interface
The account interface is used for creating an execution request out of one or more function calls requested by a dapp, as well as creating an auth witness for a given message hash. Account contracts are expected to handle multiple function calls per transaction, since dapps may choose to batch multiple actions into a single request to the wallet.
/** Creates authorization witnesses. */
export interface AuthWitnessProvider {
/**
* Computes an authentication witness from either a message hash
* @param messageHash - The message hash to approve
* @returns The authentication witness
*/
createAuthWit(messageHash: Fr | Buffer): Promise<AuthWitness>;
}
/**
* Handler for interfacing with an account. Knows how to create transaction execution
* requests and authorize actions for its corresponding account.
*/
export interface AccountInterface extends EntrypointInterface, AuthWitnessProvider {
/** Returns the complete address for this account. */
getCompleteAddress(): CompleteAddress;
/** Returns the address for this account. */
getAddress(): AztecAddress;
/** Returns the chain id for this account */
getChainId(): Fr;
/** Returns the rollup version for this account */
getVersion(): Fr;
}
Source code: yarn-project/aztec.js/src/account/interface.ts#L7-L35
PXE interface
A wallet exposes the PXE interface to dapps by running a PXE instance. The PXE requires a keystore and a database implementation for storing keys, private state, and recipient encryption public keys.
/**
* Private eXecution Environment (PXE) runs locally for each user, providing functionality for all the operations
* needed to interact with the Aztec network, including account management, private data management,
* transaction local simulation, and access to an Aztec node. This interface, as part of a Wallet,
* is exposed to dapps for interacting with the network on behalf of the user.
*/
export interface PXE {
/**
* Insert an auth witness for a given message hash. Auth witnesses are used to authorize actions on
* behalf of a user. For instance, a token transfer initiated by a different address may request
* authorization from the user to move their tokens. This authorization is granted by the user
* account contract by verifying an auth witness requested to the execution oracle. Witnesses are
* usually a signature over a hash of the action to be authorized, but their actual contents depend
* on the account contract that consumes them.
*
* @param authWitness - The auth witness to insert. Composed of an identifier, which is the hash of
* the action to be authorized, and the actual witness as an array of fields, which are to be
* deserialized and processed by the account contract.
*/
addAuthWitness(authWitness: AuthWitness): Promise<void>;
/**
* Fetches the serialized auth witness for a given message hash or returns undefined if not found.
* @param messageHash - The hash of the message for which to get the auth witness.
* @returns The serialized auth witness for the given message hash.
*/
getAuthWitness(messageHash: Fr): Promise<Fr[] | undefined>;
/**
* Adding a capsule to the capsule dispenser.
* @param capsule - An array of field elements representing the capsule.
* @remarks A capsule is a "blob" of data that is passed to the contract through an oracle.
*/
addCapsule(capsule: Fr[]): Promise<void>;
/**
* Registers a user account in PXE given its master encryption private key.
* Once a new account is registered, the PXE Service will trial-decrypt all published notes on
* the chain and store those that correspond to the registered account. Will do nothing if the
* account is already registered.
*
* @param secretKey - Secret key of the corresponding user master public key.
* @param partialAddress - The partial address of the account contract corresponding to the account being registered.
* @returns The complete address of the account.
*/
registerAccount(secretKey: Fr, partialAddress: PartialAddress): Promise<CompleteAddress>;
/**
* Retrieves the user accounts registered on this PXE Service.
* @returns An array of the accounts registered on this PXE Service.
*/
getRegisteredAccounts(): Promise<CompleteAddress[]>;
/**
* Retrieves the complete address of the account corresponding to the provided aztec address.
* Complete addresses include the address, the partial address, and the encryption public key.
*
* @param address - The address of account.
* @returns The complete address of the requested account if found.
*/
getRegisteredAccount(address: AztecAddress): Promise<CompleteAddress | undefined>;
/**
* Registers a user contact in PXE.
*
* Once a new contact is registered, the PXE Service will be able to receive notes tagged from this contact.
* Will do nothing if the account is already registered.
*
* @param address - Address of the user to add to the address book
* @returns The address address of the account.
*/
registerContact(address: AztecAddress): Promise<AztecAddress>;
/**
* Retrieves the addresses stored as contacts on this PXE Service.
* @returns An array of the contacts on this PXE Service.
*/
getContacts(): Promise<AztecAddress[]>;
/**
* Removes a contact in the address book.
*/
removeContact(address: AztecAddress): Promise<void>;
/**
* Registers a contract class in the PXE without registering any associated contract instance with it.
*
* @param artifact - The build artifact for the contract class.
*/
registerContractClass(artifact: ContractArtifact): Promise<void>;
/**
* Adds deployed contracts to the PXE Service. Deployed contract information is used to access the
* contract code when simulating local transactions. This is automatically called by aztec.js when
* deploying a contract. Dapps that wish to interact with contracts already deployed should register
* these contracts in their users' PXE Service through this method.
*
* @param contract - A contract instance to register, with an optional artifact which can be omitted if the contract class has already been registered.
*/
registerContract(contract: { instance: ContractInstanceWithAddress; artifact?: ContractArtifact }): Promise<void>;
/**
* Retrieves the addresses of contracts added to this PXE Service.
* @returns An array of contracts addresses registered on this PXE Service.
*/
getContracts(): Promise<AztecAddress[]>;
/**
* Creates a proving result based on the provided preauthenticated execution request and the results
* of executing the private part of the transaction. This will assemble the zero-knowledge proof for the private execution.
* It returns an object that contains the proof and public inputs of the tail circuit, which can be converted into a Tx ready to be sent to the network
*
* @param txRequest - An authenticated tx request ready for proving
* @param privateExecutionResult - The result of the private execution of the transaction
* @returns A transaction ready to be sent to the network for execution.
* @throws If the code for the functions executed in this transaction has not been made available via `addContracts`.
* Also throws if simulatePublic is true and public simulation reverts.
*/
proveTx(txRequest: TxExecutionRequest, privateExecutionResult: PrivateExecutionResult): Promise<TxProvingResult>;
/**
* Simulates a transaction based on the provided preauthenticated execution request.
* This will run a local simulation of private execution (and optionally of public as well), run the
* kernel circuits to ensure adherence to protocol rules (without generating a proof), and return the
* simulation results .
*
*
* Note that this is used with `ContractFunctionInteraction::simulateTx` to bypass certain checks.
* In that case, the transaction returned is only potentially ready to be sent to the network for execution.
*
*
* @param txRequest - An authenticated tx request ready for simulation
* @param simulatePublic - Whether to simulate the public part of the transaction.
* @param msgSender - (Optional) The message sender to use for the simulation.
* @param skipTxValidation - (Optional) If false, this function throws if the transaction is unable to be included in a block at the current state.
* @param profile - (Optional) If true, will run the private kernel prover with profiling enabled and include the result (gate count) in TxSimulationResult.
* @param scopes - (Optional) The accounts whose notes we can access in this call. Currently optional and will default to all.
* @returns A simulated transaction result object that includes public and private return values.
* @throws If the code for the functions executed in this transaction has not been made available via `addContracts`.
* Also throws if simulatePublic is true and public simulation reverts.
*/
simulateTx(
txRequest: TxExecutionRequest,
simulatePublic: boolean,
msgSender?: AztecAddress,
skipTxValidation?: boolean,
profile?: boolean,
scopes?: AztecAddress[],
): Promise<TxSimulationResult>;
/**
* Sends a transaction to an Aztec node to be broadcasted to the network and mined.
* @param tx - The transaction as created via `proveTx`.
* @returns A hash of the transaction, used to identify it.
*/
sendTx(tx: Tx): Promise<TxHash>;
/**
* Fetches a transaction receipt for a given transaction hash. Returns a mined receipt if it was added
* to the chain, a pending receipt if it's still in the mempool of the connected Aztec node, or a dropped
* receipt if not found in the connected Aztec node.
*
* @param txHash - The transaction hash.
* @returns A receipt of the transaction.
*/
getTxReceipt(txHash: TxHash): Promise<TxReceipt>;
/**
* Get a tx effect.
* @param txHash - The hash of a transaction which resulted in the returned tx effect.
* @returns The requested tx effect.
*/
getTxEffect(txHash: TxHash): Promise<InBlock<TxEffect> | undefined>;
/**
* Gets the storage value at the given contract storage slot.
*
* @remarks The storage slot here refers to the slot as it is defined in Noir not the index in the merkle tree.
* Aztec's version of `eth_getStorageAt`.
*
* @param contract - Address of the contract to query.
* @param slot - Slot to query.
* @returns Storage value at the given contract slot.
* @throws If the contract is not deployed.
*/
getPublicStorageAt(contract: AztecAddress, slot: Fr): Promise<Fr>;
/**
* Gets incoming notes of accounts registered in this PXE based on the provided filter.
* @param filter - The filter to apply to the notes.
* @returns The requested notes.
*/
getIncomingNotes(filter: IncomingNotesFilter): Promise<UniqueNote[]>;
/**
* Fetches an L1 to L2 message from the node.
* @param contractAddress - Address of a contract by which the message was emitted.
* @param messageHash - Hash of the message.
* @param secret - Secret used to compute a nullifier.
* @dev Contract address and secret are only used to compute the nullifier to get non-nullified messages
* @returns The l1 to l2 membership witness (index of message in the tree and sibling path).
*/
getL1ToL2MembershipWitness(
contractAddress: AztecAddress,
messageHash: Fr,
secret: Fr,
): Promise<[bigint, SiblingPath<typeof L1_TO_L2_MSG_TREE_HEIGHT>]>;
/**
* Gets outgoing notes of accounts registered in this PXE based on the provided filter.
* @param filter - The filter to apply to the notes.
* @returns The requested notes.
*/
getOutgoingNotes(filter: OutgoingNotesFilter): Promise<UniqueNote[]>;
/**
* Adds a note to the database.
* @throws If the note hash of the note doesn't exist in the tree.
* @param note - The note to add.
* @param scope - The scope to add the note under. Currently optional.
*/
addNote(note: ExtendedNote, scope?: AztecAddress): Promise<void>;
/**
* Adds a nullified note to the database.
* @throws If the note hash of the note doesn't exist in the tree.
* @param note - The note to add.
* @dev We are not deriving a nullifier in this function since that would require having the nullifier secret key
* which is undesirable. Instead, we are just adding the note to the database as nullified and the nullifier is set
* to 0 in the db.
*/
addNullifiedNote(note: ExtendedNote): Promise<void>;
/**
* Get the given block.
* @param number - The block number being requested.
* @returns The blocks requested.
*/
getBlock(number: number): Promise<L2Block | undefined>;
/**
* Simulate the execution of an unconstrained function on a deployed contract without actually modifying state.
* This is useful to inspect contract state, for example fetching a variable value or calling a getter function.
* The function takes function name and arguments as parameters, along with the contract address
* and optionally the sender's address.
*
* @param functionName - The name of the function to be called in the contract.
* @param args - The arguments to be provided to the function.
* @param to - The address of the contract to be called.
* @param from - (Optional) The msg sender to set for the call.
* @param scopes - (Optional) The accounts whose notes we can access in this call. Currently optional and will default to all.
* @returns The result of the view function call, structured based on the function ABI.
*/
simulateUnconstrained(
functionName: string,
args: any[],
to: AztecAddress,
from?: AztecAddress,
scopes?: AztecAddress[],
): Promise<AbiDecoded>;
/**
* Gets unencrypted logs based on the provided filter.
* @param filter - The filter to apply to the logs.
* @returns The requested logs.
*/
getUnencryptedLogs(filter: LogFilter): Promise<GetUnencryptedLogsResponse>;
/**
* Gets contract class logs based on the provided filter.
* @param filter - The filter to apply to the logs.
* @returns The requested logs.
*/
getContractClassLogs(filter: LogFilter): Promise<GetUnencryptedLogsResponse>;
/**
* Fetches the current block number.
* @returns The block number.
*/
getBlockNumber(): Promise<number>;
/**
* Fetches the current proven block number.
* @returns The block number.
*/
getProvenBlockNumber(): Promise<number>;
/**
* Returns the information about the server's node. Includes current Node version, compatible Noir version,
* L1 chain identifier, protocol version, and L1 address of the rollup contract.
* @returns - The node information.
*/
getNodeInfo(): Promise<NodeInfo>;
/**
* Returns information about this PXE.
*/
getPXEInfo(): Promise<PXEInfo>;
/**
* Checks whether all the blocks were processed (tree roots updated, txs updated with block info, etc.).
* @returns True if there are no outstanding blocks to be synched.
* @remarks This indicates that blocks and transactions are synched even if notes are not. Compares local block number with the block number from aztec node.
* @deprecated Use `getSyncStatus` instead.
*/
isGlobalStateSynchronized(): Promise<boolean>;
/**
* Returns the latest block that has been synchronized globally and for each account. The global block number
* indicates whether global state has been updated up to that block, whereas each address indicates up to which
* block the private state has been synced for that account.
* @returns The latest block synchronized for blocks, and the latest block synched for notes for each public key being tracked.
*/
getSyncStatus(): Promise<SyncStatus>;
/**
* Returns a Contract Instance given its address, which includes the contract class identifier,
* initialization hash, deployment salt, and public keys hash.
* TODO(@spalladino): Should we return the public keys in plain as well here?
* @param address - Deployment address of the contract.
*/
getContractInstance(address: AztecAddress): Promise<ContractInstanceWithAddress | undefined>;
/**
* Returns a Contract Class given its identifier.
* TODO(@spalladino): The PXE actually holds artifacts and not classes, what should we return? Also,
* should the pxe query the node for contract public info, and merge it with its own definitions?
* @param id - Identifier of the class.
*/
getContractClass(id: Fr): Promise<ContractClassWithId | undefined>;
/**
* Returns the contract artifact associated to a contract class.
* @param id - Identifier of the class.
*/
getContractArtifact(id: Fr): Promise<ContractArtifact | undefined>;
/**
* Queries the node to check whether the contract class with the given id has been publicly registered.
* TODO(@spalladino): This method is strictly needed to decide whether to publicly register a class or not
* during a public deployment. We probably want a nicer and more general API for this, but it'll have to
* do for the time being.
* @param id - Identifier of the class.
*/
isContractClassPubliclyRegistered(id: Fr): Promise<boolean>;
/**
* Queries the node to check whether the contract instance with the given address has been publicly deployed,
* regardless of whether this PXE knows about the contract or not.
* TODO(@spalladino): Same notes as above.
*/
isContractPubliclyDeployed(address: AztecAddress): Promise<boolean>;
/**
* Queries the node to check whether the contract instance with the given address has been initialized,
* by checking the standard initialization nullifier.
* @param address - Address of the contract to check.
*/
isContractInitialized(address: AztecAddress): Promise<boolean>;
/**
* Returns the enctypred events given search parameters.
* @param eventMetadata - Metadata of the event. This should be the class generated from the contract. e.g. Contract.events.Event
* @param from - The block number to search from.
* @param limit - The amount of blocks to search.
* @param vpks - The viewing (incoming and outgoing) public keys that correspond to the viewing secret keys that can decrypt the log.
* @returns - The deserialized events.
*/
getEncryptedEvents<T>(
eventMetadata: EventMetadataDefinition,
from: number,
limit: number,
vpks: Point[],
): Promise<T[]>;
/**
* Returns the unenctypred events given search parameters.
* @param eventMetadata - Metadata of the event. This should be the class generated from the contract. e.g. Contract.events.Event
* @param from - The block number to search from.
* @param limit - The amount of blocks to search.
* @returns - The deserialized events.
*/
getUnencryptedEvents<T>(eventMetadata: EventMetadataDefinition, from: number, limit: number): Promise<T[]>;
}
Source code: yarn-project/circuit-types/src/interfaces/pxe.ts#L47-L432