Logs
Logs on Aztec are similar to logs on Ethereum, enabling smart contracts to convey arbitrary data to external entities. Offchain applications can use logs to interpret events that have occurred on-chain. There are three types of log:
Requirements
-
Availability: The logs get published.
A rollup proof won't be accepted by the rollup contract if the log preimages are not available. Similarly, a sequencer cannot accept a transaction unless log preimages accompany the transaction data.
-
Immutability: A log cannot be modified once emitted.
The protocol ensures that once a proof is generated at any stage (for a function, transaction, or block), the emitted logs are tamper-proof. In other words, only the original log preimages can generate the committed hashes in the proof.
-
Integrity: A contract cannot impersonate another contract.
Every log is emitted by a specific contract, and users need assurances that a particular log was indeed generated by a particular contract (and not some malicious impersonator contract). The protocol ensures that the source contract's address for a log can be verified, while also preventing the forging of the address.
Log Hash
Hash Function
The protocol uses SHA256 as the hash function for logs, and then reduces the 256-bit result to 248 bits for representation as a field element.
Throughout this page, hash(value)
is an abbreviated form of: truncate_to_field(SHA256(value))
Hashing
Regardless of the log type, the log hash is derived from an array of fields, calculated as:
hash(log_preimage[0], log_preimage[1], ..., log_preimage[N - 1])
Here, log_preimage is an array of field elements of length N
, representing the data to be broadcast.
Emitting Logs from Function Circuits
A function can emit an arbitrary number of logs, provided they don't exceed the specified [limit] . The function circuits must compute a hash for each log, and push all the hashes into the public inputs for further processing by the protocol circuits.
Aggregation in Protocol Circuits
To minimize the on-chain verification data size, protocol circuits aggregate log hashes. The end result is a single hash within the base rollup proof, encompassing all logs of the same type.
Each protocol circuit outputs two values for each log type:
accumulated_logs_hash
: A hash representing all logs.accumulated_logs_length
: The total length of all log preimages.
Both the accumulated_logs_hash
and accumulated_logs_length
for each type are included in the base rollup's txs_effect_hash
. When rolling up to merge and root circuits, the two input proof's txs_effect_hash
es are hashed together to form the new value of txs_effect_hash
.
When publishing a block on L1, the raw logs of each type and their lengths are provided (Availability), hashed and accumulated into each respective accumulated_logs_hash
and accumulated_logs_length
, then included in the on-chain recalculation of txs_effect_hash
. If this value doesn't match the one from the rollup circuits, the block will not be valid (Immutability).
For private and public kernel circuits, beyond aggregating logs from a function call, they ensure that the contract's address emitting the logs is linked to the logs_hash. For more details, refer to the "Hashing" sections in Unencrypted Log, Encrypted Log, and Encrypted Note Preimage.
Unencrypted Log
Unencrypted logs are used to communicate public information out of smart contracts. They can be emitted from both public and private functions.
Emitting unencrypted logs from private functions may pose a privacy leak. However, in-protocol restrictions are intentionally omitted to allow for potentially valuable use cases, such as custom encryption schemes utilizing Fully Homomorphic Encryption (FHE), and similar scenarios.
Hashing
Following the iterations for all private or public calls, the tail kernel circuits hash each log hash with the contract contract before computing the accumulated_logs_hash.
-
Hash the contract_address to each log_hash:
log_hash_a = hash(contract_address_a, log_hash_a)
- Repeat the process for all log_hashes in the transaction.
-
Accumulate all the hashes and output the final hash to the public inputs:
accumulated_logs_hash = hash(log_hash[0], log_hash[1], ..., log_hash[N - 1])
for N logs.
Encoding
The following represents the encoded data for an unencrypted log:
log_data = [log_preimage_length, contract_address, ...log_preimage]
Verification
function hash_log_data(logs_data) {
const log_preimage_length = logs_data.read_u32();
logs_data.accumulated_logs_length += log_preimage_length;
const contract_address = logs_data.read_field();
const log_preimage = logs_data.read_fields(log_preimage_length);
const log_hash = hash(...log_preimage);
return hash(log_hash, contract_address);
}
Encrypted Log
Encrypted logs contain information encrypted using the recipient's key. They can only be emitted from private functions. This restriction is due to the necessity of obtaining a secret for log encryption, which is challenging to manage privately in a public domain.
Hashing
Private kernel circuits ensure the association of the contract address with each encrypted log_hash. However, unlike unencrypted logs, submitting encrypted log preimages with their contract address poses a significant privacy risk. Therefore, instead of using the contract_address, a masked_contract_address is generated for each encrypted log_hash.
The masked_contract_address is a hash of the contract_address and a random value randomness, computed as:
masked_contract_address = hash(contract_address, randomness)
.
Here, randomness is generated in the private function circuit and supplied to the private kernel circuit. The value must be included in the preimage for encrypted log generation. The private function circuit is responsible for ensuring that the randomness differs for every encrypted log to avoid potential information linkage based on identical masked_contract_address.
After successfully decrypting an encrypted log, one can use the randomness in the log preimage, hash it with the contract_address, and verify it against the masked_contract_address to ascertain that the log originated from the specified contract.
-
Hash the contract_address_tag to each log_hash:
masked_contract_address_a = hash(contract_address_a, randomness)
log_hash_a = hash(contract_address_tag_a, log_hash_a)
- Repeat the process for all log_hashes in the transaction.
-
Accumulate all the hashes in the tail and outputs the final hash to the public inputs:
accumulated_logs_hash = hash(log_hash[0], log_hash[1], ..., log_hash[N - 1])
for N logs, with hashes defined above.
Note that, in some cases, the user may want to reveal which contract address the encrypted log came from. Providing a randomness
value of 0 signals that we should not mask the address, so in this case the log hash is simply:
log_hash_a = hash(contract_address_a, log_hash_a)
Encoding
The following represents the encoded data for an encrypted log:
log_data = [log_preimage_length, masked_contract_address, ...log_preimage]
Verification
function hash_log_data(logs_data) {
const log_preimage_length = logs_data.read_u32();
logs_data.accumulated_logs_length += log_preimage_length;
const contract_address_tag = logs_data.read_field();
const log_preimage = logs_data.read_fields(log_preimage_length);
const log_hash = hash(...log_preimage);
return hash(log_hash, contract_address_tag);
}
Encrypted Note Preimage
Similar to encrypted logs, encrypted note preimages are data that only entities possessing the keys can decrypt to view the plaintext. Unlike encrypted logs, each encrypted note preimage can be linked to a note, whose note hash can be found in the block data.
Note that a note can be "shared" to one or more recipients by emitting one or more encrypted note preimages. However, this is not mandatory, and there may be no encrypted preimages emitted for a note if the information can be obtain through alternative means.
Hashing
As each encrypted note preimage can be associated with a note in the same transaction, enforcing a contract_address_tag is unnecessary. Instead, by calculating the note_hash using the decrypted note preimage, hashed with the contract_address, and verify it against the block data, the recipient can confirm that the note was emitted from the specified contract.
The kernel circuit simply accumulates all the hashes:
accumulated_logs_hash = hash(log_hash[0], log_hash[1], ..., log_hash[N - 1])
for N logs.
Encoding
The following represents the encoded data for an unencrypted note preimage:
log_data = [log_preimage_length, ...log_preimage]
Verification
function hash_log_data(logs_data) {
const log_preimage_length = logs_data.read_u32();
logs_data.accumulated_logs_length += log_preimage_length;
const log_preimage = logs_data.read_fields(log_preimage_length);
return hash(...log_preimage);
}
Log Encryption
Refer to Private Message Delivery for detailed information on generating encrypted data.