Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 10 additions & 53 deletions noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ unconstrained fn aes128_decrypt_oracle<let N: u32>(
/// Attempts to decrypt a ciphertext using AES128.
///
/// Returns `Option::some(plaintext)` on success, or `Option::none()` if decryption fails (e.g. due to malformed
/// ciphertext). Note that decryption with the wrong key will still return `Some` with garbage data, it's up to
/// the calling function to verify correctness (e.g. via a MAC check).
/// ciphertext or invalid PKCS#7 padding). Note that decryption with the wrong key will almost always return `None`
/// because the decrypted garbage data will have invalid PKCS#7 padding.
///
/// Note that we accept ciphertext as a BoundedVec, not as an array. This is because this function is typically used
/// when processing logs and at that point we don't have comptime information about the length of the ciphertext as
Expand All @@ -32,7 +32,6 @@ mod test {
use crate::protocol::address::AztecAddress;
use crate::test::helpers::test_environment::TestEnvironment;
use super::try_aes128_decrypt;
use poseidon::poseidon2::Poseidon2;
use std::aes128::aes128_encrypt;

global CONTRACT_ADDRESS: AztecAddress = AztecAddress { inner: 42 };
Expand Down Expand Up @@ -67,70 +66,28 @@ mod test {
})
}

global TEST_MAC_LENGTH: u32 = 32;

#[test(should_fail_with = "mac does not match")]
#[test]
unconstrained fn aes_encrypt_then_decrypt_with_bad_sym_key_is_caught() {
let env = TestEnvironment::new();

env.utility_context(|_| {
// The AES decryption oracle will not fail for any valid ciphertext; it will always return
// `Some` with some data. Whether the decryption was successful is up to the app to check in a
// custom way.
//
// E.g. if it's a note that's been encrypted, upon decryption the app can check whether the
// note hash exists onchain. If it doesn't, that's a strong indicator that decryption failed.
//
// E.g. for non-note messages, the plaintext could include a MAC
// (https://en.wikipedia.org/wiki/Message_authentication_code). We demonstrate this approach in
// this test: we compute a MAC, include it in the plaintext, encrypt, and then verify that
// decryption with a bad key produces a MAC mismatch.
// Decrypting with the wrong key results in garbage data with invalid PKCS#7 padding,
// so the oracle returns None.
let shared_secret_point = point_from_x_coord(1).unwrap();
let s_app = compute_app_siloed_shared_secret(shared_secret_point, CONTRACT_ADDRESS);

let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_shared_secret::<1>(s_app)[0];

let mac_preimage = 0x42;
let mac = Poseidon2::hash([mac_preimage], 1);
let mac_as_bytes = mac.to_be_bytes::<TEST_MAC_LENGTH>();

let plaintext: [u8; TEST_PLAINTEXT_LENGTH] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let ciphertext: [u8; TEST_CIPHERTEXT_LENGTH] = aes128_encrypt(plaintext, iv, sym_key);

// We append the mac to the plaintext. It doesn't necessarily have to be 32 bytes; that's quite an extreme
// length. 16 bytes or 8 bytes might be sufficient, and would save on data broadcasting costs. The shorter
// the mac, the more possibility of false positive decryptions (decryption seemingly succeeding, but the
// decrypted plaintext being garbage). Some projects use the `iv` (all 16 bytes or the first 8 bytes) as a
// mac.
let mut plaintext_with_mac = [0 as u8; TEST_PLAINTEXT_LENGTH + TEST_MAC_LENGTH];
for i in 0..TEST_PLAINTEXT_LENGTH {
plaintext_with_mac[i] = plaintext[i];
}
for i in 0..TEST_MAC_LENGTH {
plaintext_with_mac[TEST_PLAINTEXT_LENGTH + i] = mac_as_bytes[i];
}

let ciphertext = aes128_encrypt(plaintext_with_mac, iv, sym_key);

// We now would broadcast the tuple [ciphertext, mac] to the network. The recipient will then decrypt the
// ciphertext, and if the mac inside the received plaintext matches the mac that was broadcast, then the
// recipient knows that decryption was successful.

// For this test, we intentionally mutate the sym_key to a bad one, so that decryption fails. This allows
// us to explore how the recipient can detect failed decryption by checking the decrypted mac against the
// broadcasted mac.
let mut bad_sym_key = sym_key;
bad_sym_key[0] = 0;

// We need to convert the array to a BoundedVec because the oracle expects a BoundedVec as it's designed to
// work with logs of unknown length.
let ciphertext_bvec = BoundedVec::<u8, 48>::from_array(ciphertext);
// Decryption with wrong key still returns Some (with garbage).
let received_plaintext = try_aes128_decrypt(ciphertext_bvec, iv, bad_sym_key).unwrap();

let extracted_mac_as_bytes: [u8; TEST_MAC_LENGTH] =
subarray(received_plaintext.storage(), TEST_PLAINTEXT_LENGTH);

assert_eq(mac_as_bytes, extracted_mac_as_bytes, "mac does not match");
let ciphertext_bvec = BoundedVec::<u8, TEST_CIPHERTEXT_LENGTH>::from_array(ciphertext);
// Decryption with wrong key returns None because the garbage output has invalid PKCS#7 padding.
let result = try_aes128_decrypt(ciphertext_bvec, iv, bad_sym_key);
assert(result.is_none(), "decryption with bad key should return None");
});
}
}
18 changes: 12 additions & 6 deletions yarn-project/archiver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,18 @@ Two independent syncpoints track progress on L1:

### L1-to-L2 Messages

Messages are synced from the Inbox contract via `handleL1ToL2Messages()`:
Messages are synced from the Inbox contract. The sync compares local state (message count and rolling hash) against the Inbox contract state on L1, downloads any missing messages, and verifies consistency afterwards. On success, the syncpoint advances to the current L1 block. On failure (L1 reorg or inconsistency), the syncpoint rolls back to the last known-good message and the operation retries (up to 3 times within the same sync iteration).

1. Query Inbox state at the current L1 block (message count + rolling hash)
2. Compare local vs remote state
3. If they match, nothing to do
4. If mismatch, validate the local last message still exists on L1 with the same rolling hash
- If not found or hash differs, an L1 reorg occurred: find the last common message, delete everything after, and rollback the syncpoint
5. Fetch `MessageSent` events in batches and store
2. Compare local state against remote
3. If they match, advance syncpoint and return
4. If mismatch, fetch `MessageSent` events in batches and store them
- If storing fails due to a rolling hash mismatch (indicating an L1 reorg changed or removed messages), find the last common message with L1, delete everything after, reset the syncpoint, and retry
5. After storing, verify local state matches the remote state queried in step 1
- If still mismatched (e.g., messages missed due to a concurrent L1 reorg), rollback and retry
6. On success, advance the syncpoint

The syncpoint and the `inboxTreeInProgress` marker (which tracks which checkpoint's messages are currently being filled on L1) are updated atomically. The marker is only advanced after messages are stored, so concurrent reads don't see an unsealed checkpoint as readable before its messages are available.

### Checkpoints

Expand Down Expand Up @@ -81,6 +85,8 @@ The `blocksSynchedTo` syncpoint is updated:

Note that the `blocksSynchedTo` pointer is NOT updated during normal sync when there are no new checkpoints. This protects against small L1 reorgs that could add a checkpoint on an L1 block we have flagged as already synced.

The `messagesSynchedTo` pointer is always advanced to the current L1 block on success. If a rolling hash mismatch or post-download inconsistency is detected, the pointer rolls back to the last common message and the operation retries. The rolling hash chain and pre/post-sync consistency checks provide the primary reorg protection.

### Block Queue

The archiver implements `L2BlockSink`, allowing other subsystems to push blocks before they appear on L1:
Expand Down
5 changes: 4 additions & 1 deletion yarn-project/archiver/src/archiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,10 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra
await this.store.rollbackL1ToL2MessagesToCheckpoint(targetCheckpointNumber);
this.log.info(`Setting L1 syncpoints to ${targetL1BlockNumber}`);
await this.store.setCheckpointSynchedL1BlockNumber(targetL1BlockNumber);
await this.store.setMessageSynchedL1Block({ l1BlockNumber: targetL1BlockNumber, l1BlockHash: targetL1BlockHash });
await this.store.setMessageSyncState(
{ l1BlockNumber: targetL1BlockNumber, l1BlockHash: targetL1BlockHash },
undefined,
);
if (targetL2BlockNumber < currentProvenBlock) {
this.log.info(`Rolling back proven L2 checkpoint to ${targetCheckpointNumber}`);
await this.updater.setProvenCheckpointNumber(targetCheckpointNumber);
Expand Down
18 changes: 7 additions & 11 deletions yarn-project/archiver/src/l1/data_retrieval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,14 +344,10 @@ export async function getCheckpointBlobDataFromBlobs(
/** Given an L1 to L2 message, retrieves its corresponding event from the Inbox within a specific block range. */
export async function retrieveL1ToL2Message(
inbox: InboxContract,
leaf: Fr,
fromBlock: bigint,
toBlock: bigint,
message: InboxMessage,
): Promise<InboxMessage | undefined> {
const logs = await inbox.getMessageSentEventByHash(leaf.toString(), fromBlock, toBlock);

const messages = mapLogsInboxMessage(logs);
return messages.length > 0 ? messages[0] : undefined;
const log = await inbox.getMessageSentEventByHash(message.leaf.toString(), message.l1BlockHash.toString());
return log && mapLogInboxMessage(log);
}

/**
Expand All @@ -374,22 +370,22 @@ export async function retrieveL1ToL2Messages(
break;
}

retrievedL1ToL2Messages.push(...mapLogsInboxMessage(messageSentLogs));
retrievedL1ToL2Messages.push(...messageSentLogs.map(mapLogInboxMessage));
searchStartBlock = messageSentLogs.at(-1)!.l1BlockNumber + 1n;
}

return retrievedL1ToL2Messages;
}

function mapLogsInboxMessage(logs: MessageSentLog[]): InboxMessage[] {
return logs.map(log => ({
function mapLogInboxMessage(log: MessageSentLog): InboxMessage {
return {
index: log.args.index,
leaf: log.args.leaf,
l1BlockNumber: log.l1BlockNumber,
l1BlockHash: log.l1BlockHash,
checkpointNumber: log.args.checkpointNumber,
rollingHash: log.args.rollingHash,
}));
};
}

/** Retrieves L2ProofVerified events from the rollup contract. */
Expand Down
Loading
Loading