Skip to content

Commit 7d045d3

Browse files
committed
fix: remove redundant to/memo from DecryptionData
The `to` and `memo` fields in `DecryptionData` were redundant: the on-chain `ZoneInbox` already decrypts the ciphertext via AES-256-GCM and can derive these values directly from the plaintext. Remove them from the struct and use the decrypted values on-chain instead of comparing against sequencer-supplied duplicates. Saves 52 bytes of calldata per encrypted deposit (20-byte address + 32-byte memo). Closes #357 Made-with: Cursor
1 parent 7750a4b commit 7d045d3

11 files changed

Lines changed: 411 additions & 81 deletions

File tree

crates/primitives/src/abi.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,12 +378,11 @@ macro_rules! define_abi {
378378
}
379379

380380
/// Decryption data provided by the sequencer for encrypted deposits.
381+
/// The decrypted (to, memo) are derived on-chain from the AES-GCM decryption.
381382
#[derive(Debug)]
382383
struct DecryptionData {
383384
bytes32 sharedSecret;
384385
uint8 sharedSecretYParity;
385-
address to;
386-
bytes32 memo;
387386
ChaumPedersenProof cpProof;
388387
}
389388

crates/tempo-zone/src/builder.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -690,8 +690,6 @@ mod tests {
690690
decryptions: vec![abi::DecryptionData {
691691
sharedSecret: B256::ZERO,
692692
sharedSecretYParity: 0x02,
693-
to: sender,
694-
memo: B256::ZERO,
695693
cpProof: abi::ChaumPedersenProof {
696694
s: B256::ZERO,
697695
c: B256::ZERO,

crates/tempo-zone/src/l1.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,7 +1205,7 @@ impl L1BlockDeposits {
12051205
)
12061206
.await?;
12071207

1208-
let recipient = if authorized {
1208+
let _recipient = if authorized {
12091209
debug!(
12101210
target: "zone::engine",
12111211
recipient = %dec.to,
@@ -1228,8 +1228,6 @@ impl L1BlockDeposits {
12281228
let decryption = abi::DecryptionData {
12291229
sharedSecret: dec.proof.shared_secret,
12301230
sharedSecretYParity: dec.proof.shared_secret_y_parity,
1231-
to: recipient,
1232-
memo: dec.memo,
12331231
cpProof: abi::ChaumPedersenProof {
12341232
s: dec.proof.cp_proof_s,
12351233
c: dec.proof.cp_proof_c,
@@ -1257,8 +1255,6 @@ impl L1BlockDeposits {
12571255
let decryption = abi::DecryptionData {
12581256
sharedSecret: proof.shared_secret,
12591257
sharedSecretYParity: proof.shared_secret_y_parity,
1260-
to: d.sender,
1261-
memo: B256::ZERO,
12621258
cpProof: abi::ChaumPedersenProof {
12631259
s: proof.cp_proof_s,
12641260
c: proof.cp_proof_c,
@@ -1278,8 +1274,6 @@ impl L1BlockDeposits {
12781274
let decryption = abi::DecryptionData {
12791275
sharedSecret: B256::ZERO,
12801276
sharedSecretYParity: 0x02,
1281-
to: d.sender,
1282-
memo: B256::ZERO,
12831277
cpProof: abi::ChaumPedersenProof {
12841278
s: B256::ZERO,
12851279
c: B256::ZERO,

crates/tempo-zone/tests/advance_tempo.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ sol! {
3838
struct DecryptionData {
3939
bytes32 sharedSecret;
4040
uint8 sharedSecretYParity;
41-
address to;
42-
bytes32 memo;
4341
ChaumPedersenProof cpProof;
4442
}
4543
}

docs/pages/protocol/privacy/crypto-review.md

Lines changed: 381 additions & 0 deletions
Large diffs are not rendered by default.

docs/pages/protocol/privacy/overview.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,18 +1230,18 @@ This ensures deposits are processed in the exact order they were made, regardles
12301230

12311231
**On-chain decryption verification:**
12321232

1233-
The zone can verify encrypted deposit decryption on-chain without the sequencer revealing their private key. The sequencer provides the ECDH shared secret alongside the decrypted data:
1233+
The zone can verify encrypted deposit decryption on-chain without the sequencer revealing their private key. The sequencer provides the ECDH shared secret and a proof of correct derivation:
12341234

12351235
```solidity
12361236
struct DecryptionData {
12371237
bytes32 sharedSecret; // ECDH shared secret (x-coordinate of privSeq * ephemeralPub)
12381238
uint8 sharedSecretYParity; // Y coordinate parity of the shared secret point (0x02 or 0x03)
1239-
address to; // Decrypted recipient
1240-
bytes32 memo; // Decrypted memo
12411239
ChaumPedersenProof cpProof; // Proof of correct shared secret derivation
12421240
}
12431241
```
12441242

1243+
The decrypted `(to, memo)` are derived on-chain from the AES-GCM decryption of the ciphertext and do not need to be supplied by the sequencer.
1244+
12451245
Verification works by leveraging the AES-GCM authentication tag:
12461246

12471247
1. Sequencer computes: `sharedSecret = ECDH(sequencerPriv, ephemeralPub)`
@@ -1387,11 +1387,12 @@ bytes32 aesKey = _hkdfSha256(
13871387
ed.encrypted.tag
13881388
);
13891389
1390-
// Step 5: Verify decrypted plaintext matches claimed (to, memo)
1390+
// Step 5: Decode the decrypted (to, memo) from the plaintext
13911391
// Plaintext is packed as [address(20 bytes)][memo(32 bytes)][padding(12 bytes)] = 64 bytes
1392+
address decryptedTo;
1393+
bytes32 decryptedMemo;
13921394
if (valid && decryptedPlaintext.length == ENCRYPTED_PAYLOAD_PLAINTEXT_SIZE) {
1393-
(address decryptedTo, bytes32 decryptedMemo) = EncryptedDepositLib.decodePlaintext(decryptedPlaintext);
1394-
valid = (decryptedTo == dec.to && decryptedMemo == dec.memo);
1395+
(decryptedTo, decryptedMemo) = EncryptedDepositLib.decodePlaintext(decryptedPlaintext);
13951396
} else {
13961397
valid = false;
13971398
}
@@ -1403,8 +1404,8 @@ if (!valid) {
14031404
emit EncryptedDepositFailed(currentHash, ed.sender, ed.token, ed.amount);
14041405
} else {
14051406
// Decryption succeeded - mint correct zone-side TIP-20 to decrypted recipient
1406-
IZoneToken(ed.token).mint(dec.to, ed.amount);
1407-
emit EncryptedDepositProcessed(currentHash, ed.sender, dec.to, ed.token, ed.amount, dec.memo);
1407+
IZoneToken(ed.token).mint(decryptedTo, ed.amount);
1408+
emit EncryptedDepositProcessed(currentHash, ed.sender, decryptedTo, ed.token, ed.amount, decryptedMemo);
14081409
}
14091410
```
14101411

docs/pages/protocol/privacy/prover-design.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,10 @@ pub enum DepositType {
160160
}
161161

162162
/// Mirrors the Solidity `DecryptionData` struct from IZone.sol
163-
/// Provided by the sequencer for each encrypted deposit
163+
/// Provided by the sequencer for each encrypted deposit.
164+
/// The decrypted (to, memo) are derived on-chain from AES-GCM decryption.
164165
pub struct DecryptionData {
165166
pub shared_secret: B256, // ECDH shared secret (x-coordinate)
166-
pub to: Address, // Decrypted recipient
167-
pub memo: B256, // Decrypted memo
168167
pub cp_proof: ChaumPedersenProof, // Proof of correct shared secret derivation
169168
}
170169

docs/specs/src/zone/IZone.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,11 @@ struct ChaumPedersenProof {
137137
/// without exposing the sequencer's private key.
138138
/// The sequencer's public key is looked up from the deposit's keyIndex on-chain,
139139
/// so it does not need to be included here.
140+
/// The decrypted (to, memo) are derived on-chain from the AES-GCM decryption and
141+
/// do not need to be supplied by the sequencer.
140142
struct DecryptionData {
141143
bytes32 sharedSecret; // ECDH shared secret (x-coordinate of privSeq * ephemeralPub)
142144
uint8 sharedSecretYParity; // Y coordinate parity of the shared secret point (0x02 or 0x03)
143-
address to; // Decrypted recipient
144-
bytes32 memo; // Decrypted memo
145145
ChaumPedersenProof cpProof; // Proof of correct shared secret derivation
146146
}
147147

docs/specs/src/zone/ZoneInbox.sol

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,13 +257,14 @@ contract ZoneInbox is IZoneInbox {
257257
ed.encrypted.tag
258258
);
259259

260-
// Step 4: Verify decrypted plaintext matches claimed (to, memo)
260+
// Step 4: Decode the decrypted (to, memo) from the plaintext
261261
// Plaintext is packed as [address(20 bytes)][memo(32 bytes)][padding(12 bytes)]
262262
// Must be exactly ENCRYPTED_PAYLOAD_PLAINTEXT_SIZE (64) bytes
263+
address decryptedTo;
264+
bytes32 decryptedMemo;
263265
if (valid && decryptedPlaintext.length == ENCRYPTED_PAYLOAD_PLAINTEXT_SIZE) {
264-
(address decryptedTo, bytes32 decryptedMemo) =
266+
(decryptedTo, decryptedMemo) =
265267
EncryptedDepositLib.decodePlaintext(decryptedPlaintext);
266-
valid = (decryptedTo == dec.to && decryptedMemo == dec.memo);
267268
} else {
268269
valid = false;
269270
}
@@ -278,9 +279,9 @@ contract ZoneInbox is IZoneInbox {
278279
emit EncryptedDepositFailed(currentHash, ed.sender, ed.token, ed.amount);
279280
} else {
280281
// Decryption succeeded - mint the correct zone-side TIP-20 to the decrypted recipient
281-
IZoneToken(ed.token).mint(dec.to, ed.amount);
282+
IZoneToken(ed.token).mint(decryptedTo, ed.amount);
282283
emit EncryptedDepositProcessed(
283-
currentHash, ed.sender, dec.to, ed.token, ed.amount, dec.memo
284+
currentHash, ed.sender, decryptedTo, ed.token, ed.amount, decryptedMemo
284285
);
285286
}
286287
}

docs/specs/test/zone/ZoneBridge.t.sol

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -964,8 +964,6 @@ contract ZoneBridgeTest is BaseTest {
964964
decs[i] = DecryptionData({
965965
sharedSecret: bytes32(uint256(0xDEAD)),
966966
sharedSecretYParity: 0x02,
967-
to: decryptedTo,
968-
memo: decryptedMemo,
969967
cpProof: ChaumPedersenProof({ s: bytes32(uint256(1)), c: bytes32(uint256(2)) })
970968
});
971969
}
@@ -1087,7 +1085,7 @@ contract ZoneBridgeTest is BaseTest {
10871085
_sequencerObserveEncryptedDeposit(alice, netAmount, 0, payload);
10881086
_setupEncryptionKeyMockOnZone(0, encKeyX, encKeyYParity);
10891087

1090-
// Even with shouldSucceed=false, we need a to/memo for DecryptionData (values don't matter)
1088+
// Even with shouldSucceed=false, we still call the relay helper
10911089
bytes32 newProcessedHash =
10921090
_sequencerRelayEncryptedDepositsToL2(address(0xBEEF), bytes32("wrong"), false);
10931091

@@ -1193,8 +1191,6 @@ contract ZoneBridgeTest is BaseTest {
11931191
decs[0] = DecryptionData({
11941192
sharedSecret: bytes32(uint256(0xDEAD)),
11951193
sharedSecretYParity: 0x02,
1196-
to: decryptedTo,
1197-
memo: decryptedMemo,
11981194
cpProof: ChaumPedersenProof({ s: bytes32(uint256(1)), c: bytes32(uint256(2)) })
11991195
});
12001196

@@ -1306,15 +1302,11 @@ contract ZoneBridgeTest is BaseTest {
13061302
decs[0] = DecryptionData({
13071303
sharedSecret: bytes32(uint256(0xDEAD)),
13081304
sharedSecretYParity: 0x02,
1309-
to: aliceRecipient,
1310-
memo: aliceMemo,
13111305
cpProof: ChaumPedersenProof({ s: bytes32(uint256(1)), c: bytes32(uint256(2)) })
13121306
});
13131307
decs[1] = DecryptionData({
13141308
sharedSecret: bytes32(uint256(0xBEEF)),
13151309
sharedSecretYParity: 0x02,
1316-
to: bobRecipient,
1317-
memo: bobMemo,
13181310
cpProof: ChaumPedersenProof({ s: bytes32(uint256(3)), c: bytes32(uint256(4)) })
13191311
});
13201312

@@ -1326,30 +1318,11 @@ contract ZoneBridgeTest is BaseTest {
13261318
address(l1Portal), PORTAL_CURRENT_DEPOSIT_QUEUE_HASH_SLOT, hash2
13271319
);
13281320

1329-
// Mock precompiles — we use broad mocks since vm.mockCall matches any input
1330-
// For the success path, we need AES-GCM to return the correct plaintext.
1331-
// Since vm.mockCall with just the selector matches ALL calls, we mock for the LAST
1332-
// decryption (bobRecipient). For aliceRecipient we set up mock before advanceTempo,
1333-
// but vm.mockCall replaces: we need a workaround.
1334-
//
1335-
// Since Foundry's vm.mockCall uses last-registered-wins for the same address+selector,
1336-
// and encrypted deposits are processed sequentially, we can't differentiate two calls
1337-
// to the same precompile with different expected outputs using selector-only mocking.
1338-
//
1339-
// Workaround: mock both precompiles to return bobRecipient's plaintext.
1340-
// Alice's deposit will fail the plaintext check (dec.to != decryptedTo), causing a bounce.
1341-
// We test a simpler scenario: mock for aliceRecipient so BOTH succeed with the same plaintext.
1342-
//
1343-
// Actually, the cleanest approach: make both deposits decrypt to the same recipient/memo.
1344-
// This tests key rotation without needing differentiated mocks.
1345-
1346-
// Use same recipient/memo for both decryptions
1321+
// Mock precompiles to return the same plaintext for both deposits.
1322+
// Since vm.mockCall with just the selector matches ALL calls, both encrypted
1323+
// deposits will decrypt to the same (sharedRecipient, sharedMemo).
13471324
address sharedRecipient = address(0x700);
13481325
bytes32 sharedMemo = bytes32("shared-secret");
1349-
decs[0].to = sharedRecipient;
1350-
decs[0].memo = sharedMemo;
1351-
decs[1].to = sharedRecipient;
1352-
decs[1].memo = sharedMemo;
13531326

13541327
vm.etch(CHAUM_PEDERSEN_VERIFY, hex"00");
13551328
vm.etch(AES_GCM_DECRYPT, hex"00");

0 commit comments

Comments
 (0)