Emitting Events
Events in Aztec work similarly to Ethereum events in the sense that they are a way for contracts to communicate with the outside world. They are emitted by contracts and stored inside each instance of an AztecNode.
Aztec events are currently represented as raw data and are not ABI encoded. ABI encoded events are a feature that will be added in the future.
Unlike on Ethereum, there are 2 types of events supported by Aztec: encrypted and unencrypted.
Encrypted Events
Register a recipient
Encrypted events can only be emitted by private functions and are encrypted using a public key of a recipient. For this reason it is necessary to register a recipient in the Private Execution Environment (PXE) before encrypting the events for them.
First we need to get a hold of recipient's complete address. Below are some ways how we could instantiate it after getting the information in a string form from a recipient:
// Typically a recipient would share their complete address with the sender
const completeAddressFromString = CompleteAddress.fromString(
'0x1de12596818ab6bc3584b943f791b206ff588d3c307358ab6918f59ed7d381bc02a9372135ce5b49b46102732fabd742c31642543396013dde5b460075864607264c605bc115c6cb92a4db0a6b893fd3777341078693d0af22e3ff53f4c2ee2a2fae73914fc50d325e2707a8e996f1ad498429f715f998225dc6bd2ede05aaee055ee137d28b634322e0ea98afc42dfc48833e8d2879c34d23d6d1d337069cca212af0f28b7865b339e202a0077fd3bd8dddc472d055945ad99c02dcccd28bb22bb3585fca3e5751c9913521a390458d63e4d9b292e4872582f3b13da214470c14083a4567cf4f1e92696e6c01923bc6a8b414159446268b12fe8669ce44f1f5196561aca6c654d2405a5653002cba5552b50b6ce1afc9515ed6682507abcb3010040d791aeb30138efc9c7d36b47684af2f26f686672448349f05934ae7bbbf',
);
// Alternatively, a recipient could share the individual components with the sender
const address = Fr.fromString('0x1de12596818ab6bc3584b943f791b206ff588d3c307358ab6918f59ed7d381bc');
const npkM = Point.fromString(
'0x02a9372135ce5b49b46102732fabd742c31642543396013dde5b460075864607264c605bc115c6cb92a4db0a6b893fd3777341078693d0af22e3ff53f4c2ee2a',
);
const ivpkM = Point.fromString(
'0x2fae73914fc50d325e2707a8e996f1ad498429f715f998225dc6bd2ede05aaee055ee137d28b634322e0ea98afc42dfc48833e8d2879c34d23d6d1d337069cca',
);
const ovpkM = Point.fromString(
'0x212af0f28b7865b339e202a0077fd3bd8dddc472d055945ad99c02dcccd28bb22bb3585fca3e5751c9913521a390458d63e4d9b292e4872582f3b13da214470c',
);
const tpkM = Point.fromString(
'0x14083a4567cf4f1e92696e6c01923bc6a8b414159446268b12fe8669ce44f1f5196561aca6c654d2405a5653002cba5552b50b6ce1afc9515ed6682507abcb30',
);
const partialAddress = Fr.fromString('0x10040d791aeb30138efc9c7d36b47684af2f26f686672448349f05934ae7bbbf');
const completeAddressFromComponents = new CompleteAddress(
address,
new PublicKeys(npkM, ivpkM, ovpkM, tpkM),
partialAddress,
);
Source code: yarn-project/circuits.js/src/structs/complete_address.test.ts#L38-L66
Then to register the recipient's complete address in PXE we would call registerRecipient
PXE endpoint using Aztec.js:
await pxe.registerRecipient(completeAddress);
Source code: yarn-project/aztec.js/src/wallet/create_recipient.ts#L11-L13
If a note recipient is one of the accounts inside the PXE, we don't need to register it as a recipient because we already have the public key available. You can register a recipient as shown here
Call emit
To emit encrypted logs you can import the encode_and_encrypt
or encode_and_encrypt_with_keys
functions and pass them into the emit
function after inserting a note. An example can be seen in the reference token contract's transfer function:
storage.balances.sub(from, amount).emit(encode_and_encrypt_note_with_keys(&mut context, from_ovpk, from_ivpk));
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L363-L365
Furthermore, if not emitting the note, one should explicitly discard
the value returned from the note creation.
Successfully process the encrypted event
One of the functions of the PXE is constantly loading encrypted logs from the AztecNode
and decrypting them.
When new encrypted logs are obtained, the PXE will try to decrypt them using the private encryption key of all the accounts registered inside PXE.
If the decryption is successful, the PXE will store the decrypted note inside a database.
If the decryption fails, the specific log will be discarded.
For the PXE to successfully process the decrypted note we need to compute the note's 'note hash' and 'nullifier'.
Aztec.nr enables smart contract developers to design custom notes, meaning developers can also customize how a note's note hash and nullifier should be computed. Because of this customizability, and because there will be a potentially-unlimited number of smart contracts deployed to Aztec, an PXE needs to be 'taught' how to compute the custom note hashes and nullifiers for a particular contract. This is done by a function called compute_note_hash_and_optionally_a_nullifier
, which is automatically injected into every contract when compiled.
Encrypted Events
To emit generic event information as an encrypted log, call the context method encrypt_and_emit_note
. Currently, only arrays of
fields are supported, and the PXE will fail to decrypt, process and store this data, so it will not be queryable automatically.
Unencrypted Events
Unencrypted events are events which can be read by anyone. They can be emitted by both public and private functions.
- Emitting unencrypted events from private function is a significant privacy leak and it should be considered by the developer whether it is acceptable.
Call emit_unencrypted_log
To emit unencrypted logs you don't need to import any library. You call the context method emit_unencrypted_log
:
context.emit_unencrypted_log(/*message=*/ value);
context.emit_unencrypted_log(/*message=*/ [10, 20, 30]);
context.emit_unencrypted_log(/*message=*/ "Hello, world!");
Source code: noir-projects/noir-contracts/contracts/test_contract/src/main.nr#L346-L350
Querying the unencrypted event
Once emitted, unencrypted events are stored in AztecNode and can be queried by anyone:
// Get the unencrypted logs from the last block
const fromBlock = await pxe.getBlockNumber();
const logFilter = {
fromBlock,
toBlock: fromBlock + 1,
};
const unencryptedLogs = (await pxe.getUnencryptedLogs(logFilter)).logs;
Source code: yarn-project/end-to-end/src/fixtures/utils.ts#L556-L564
Costs
All event data is pushed to Ethereum as calldata by the sequencer and for this reason the cost of emitting an event is non-trivial.
In the Sandbox, an encrypted note has a fixed overhead of 4 field elements (to broadcast an ephemeral public key, a contract address, and a storage slot); plus a variable number of field elements depending on the type of note being emitted.
A ValueNote
, for example, currently uses 3 fields elements (plus the fixed overhead of 4). That's roughly 7 * 32 = 224
bytes of information.
#[aztec(note)]
struct ValueNote {
value: Field,
// The nullifying public key hash is used with the nsk_app to ensure that the note can be privately spent.
npk_m_hash: Field,
randomness: Field,
}
Source code: noir-projects/aztec-nr/value-note/src/value_note.nr#L14-L22
- There are plans to compress encrypted note data further.
- There are plans to adopt EIP-4844 blobs to reduce the cost of data submission further.