Skip to content

feat(pxe): default unconstrained delivery to a handshake - #24387

Merged
nchamo merged 6 commits into
merge-train/fairies-v5from
nchamo/f-770-default-unconstrained-delivery-to-a-non-interactive
Jul 1, 2026
Merged

nchamo merged 6 commits into
merge-train/fairies-v5from
nchamo/f-770-default-unconstrained-delivery-to-a-non-interactive

Conversation

@nchamo

@nchamo nchamo commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Motivation

#24312 made constrained delivery default to a non-interactive handshake when no handshake covers (sender, recipient). This does the same for unconstrained delivery, branching on the recipient: an external recipient defaults to a non-interactive handshake so it discovers the message without having registered the sender, while a self-send (the recipient is one of the wallet's own accounts) uses an address-derived secret, which needs no handshake and leaves no onchain trace.

The change

  • resolveTaggingStrategy (PXE) defaults both delivery modes to a non-interactive handshake, then always falls back to an address-derived secret for an unconstrained self-send.
  • That self-send fallback is a recursion guard: establishing a handshake self-delivers its note via unconstrained delivery, so a handshake on a self-send would recurse. It applies even when a wallet hook returns a handshake.

Fixes F-770

@nchamo nchamo added ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure ci-draft Run CI on draft PRs. labels Jun 29, 2026
@nchamo nchamo self-assigned this Jun 29, 2026
@nchamo
nchamo marked this pull request as ready for review June 30, 2026 00:10
@nchamo
nchamo requested a review from nventuro as a code owner June 30, 2026 00:10
@nchamo
nchamo requested a review from vezenovm June 30, 2026 00:10

@vezenovm vezenovm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just some nits, looks good!

Comment thread docs/docs-developers/docs/aztec-nr/framework-description/note_delivery.md Outdated
Comment thread docs/docs-developers/docs/foundational-topics/advanced/storage/note_discovery.md Outdated
Comment thread docs/docs-developers/docs/foundational-topics/advanced/storage/note_discovery.md Outdated
@nchamo
nchamo enabled auto-merge (squash) July 1, 2026 01:06
@nchamo
nchamo merged commit 0c69766 into merge-train/fairies-v5 Jul 1, 2026
12 checks passed
@nchamo
nchamo deleted the nchamo/f-770-default-unconstrained-delivery-to-a-non-interactive branch July 1, 2026 02:46
AztecBot added a commit that referenced this pull request Jul 1, 2026
The bench measures steady-state app cost. #24387 defaulted unconstrained delivery to a non-interactive handshake, which injects two extra private app executions and a nullifier per cold (sender, recipient) chain — reflected in the +APPS_PER_ONCHAIN_HANDSHAKE_DELIVERY constant this commit removes.

Wire a resolveTaggingSecretStrategy hook on the bench's user wallet that returns address-derived for unconstrained delivery (falling back to a non-interactive handshake for constrained, since Noir rejects address-derived there). Matches the pre-#24387 behavior for unconstrained sends without adding a pre-bootstrap tx or its associated nullifier.
vezenovm pushed a commit that referenced this pull request Jul 1, 2026
…bench; widen the PXE unfinalized tagging window (#24429)

## Summary

- widen the PXE `UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN` to
`MAX_PRIVATE_LOGS_PER_TX` (aligning the window with the per-tx
private-log cap, which is the real bound the store is gated on)
- wire a `resolveTaggingSecretStrategy` hook on the client-flows bench
user wallet that returns `address-derived` for unconstrained delivery,
restoring the pre-#24387 tagging secret path for these benches

## Investigation

The merge-train PR #24388 was removed from the merge queue on 2026-07-01
at 13:05 UTC after CI3 failed in `bench all` on merge-group SHA
`84262a7ee42e1c391c00b55b377910ddb4308230`, in run `28515530242`. The
concrete failures:

- `client_flows/transfers`: expected 7 execution steps but got 10 for
the sponsored-FPC transfer case, matching 2 extra app executions plus
one extra kernel batch after #24387 (`feat(pxe): default unconstrained
delivery to a handshake`).
- `bench_build_block`: timed out after repeated `Highest used index 21
is further than window length from the highest finalized index 0.
Tagging window length 20 is configured too low` errors.

Both failures share a root cause: #24387 defaulted unconstrained
delivery to a non-interactive handshake for external recipients, and the
benches don't pre-establish a chain, so every timed transfer inflates a
cold-chain handshake bootstrap into the measured numbers.

## Why the hook, not just wider fudges

The bench's subject is steady-state app cost. Baking `+2 app executions
per cold handshake` into `expectedExecutionSteps` measures a
first-time-only cost every run, and re-tunes with every future
handshake-bootstrap change. Instead, the user wallet now wires a
`resolveTaggingSecretStrategy` hook that returns `{ type:
'address-derived' }` for unconstrained delivery — the same tagging
secret path this bench got before #24387. The hook only affects the
client-flows bench's user wallet, not production defaults.

Constrained delivery is unaffected: the Noir circuit rejects
`address-derived` there, so the hook falls back to
`non-interactive-handshake` for the constrained branch.

## Why the window bump stands independently

The previous doc-comment on `UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN` was
self-contradictory: the header text said the value had to be larger than
`MAX_PRIVATE_LOGS_PER_TX` "because there could be more than
MAX_PRIVATE_LOGS_PER_TX indexes consumed in case the logs are squashed,"
but the constant was set to `20` (well below `MAX_PRIVATE_LOGS_PER_TX =
64`). The real limit is one full private-tx's worth of unfinalized tag
indexes per chain; aligning the constant with that bound makes the
semantic clearer.

Refs #24388, #24387.

## Testing

- Diff verified: `client_flows/transfers.test.ts` now runs with the
pre-#24387 `expectedExecutionSteps(1 + apps + 1 + recursions)` counts.
- Local unit tests / `bench all` will re-run under CI3 on this branch's
push.
vezenovm added a commit that referenced this pull request Jul 6, 2026
…E_LOGS_PER_TX + headroom, not exactly MAX_PRIVATE_LOGS_PER_TX (#24437)

## Summary

Follow-up to #24429. That PR widened
`UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN` from `20` to exactly
`MAX_PRIVATE_LOGS_PER_TX` (64). This PR argues that's still not the
right value for the scenario #24429 was trying to fix, and proposes
`MAX_PRIVATE_LOGS_PER_TX + 20`.

**Scope note (see "Known limitation" below):** this fixes a real,
CI-confirmed problem; multiple ordinary pending txs to the same
counterparty stacking up before the first is mined. There is a separate,
harder problem captured in
[F-783](https://linear.app/aztec-labs/issue/F-783/pxe-unfinalized-tagging-indexes-window-len-cannot-bound-a-squash-heavy).
Additional separate breakage follow-up on the sender-side when pending
logs exceed the window
[F-784](https://linear.app/aztec-labs/issue/F-784/pxe-typed-error-and-resync-recheck-when-pending-tagging-indexes-exceed).

## Investigation

`MAX_PRIVATE_LOGS_PER_TX` is a genuine, protocol-enforced bound for
ordinary transaction patterns: `noir-protocol-circuits` `constants.nr`
defines `MAX_PRIVATE_LOGS_PER_TX` directly as `MAX_NOTE_HASHES_PER_TX`.
A fresh secret's very first ordinary tx can legitimately consume that
many indexes before anything is finalized.

But exactly `MAX_PRIVATE_LOGS_PER_TX` leaves zero headroom for a second
unconfirmed tx to the same counterparty once the first one is already
near the max. `finalized` only advances once the sender's own node
observes a tx as mined (see `syncSenderTaggingIndexes`), so any burst of
sends to one counterparty before the first is mined can exhaust the
window. This is exactly what happened in #24429's `bench_build_block`
failure: `#24387`'s handshake default added extra per-cold-chain
overhead, and the bench builds up a backlog of unmined txs by design, so
`used` ran past `finalized` (which stayed at `0`) before window `20`
could absorb it.

## Confirmed live: reproduced on this branch at bare `20`

To settle whether #24429's bump was actually necessary, we reset this
branch's constant back to bare `20` and let `bench_build_block` run to
completion in CI rather than relying on the reasoning above alone. It
failed the same way (see http://ci.aztec-labs.com/4ed074af4b8ab633 from
[a4feea1](a4feea1),
on the current `merge-train/fairies-v5` tip:

```
ERROR: kv-store:lmdb-v2 pxe-0 Failed to commit transaction: Error: Highest used index 21 is further than window length from the highest finalized index 0.
            Tagging window length 20 is configured too low. Contact the Aztec team
            to increase it!
```

So this isn't a synthetic worst case: it's a real failure on real CI
infrastructure, at index `21`, i.e. just one past the window. Not even a
large burst of txs.

## Red/green evidence

Added `sender_tagging_store.test.ts`: "allows an ordinary pending tx to
stack on a fresh secret already at the `MAX_PRIVATE_LOGS_PER_TX` floor"
— one tx uses the full worst-case 64-log burst from a fresh secret, then
a second, ordinary-sized pending tx to the same secret must still be
storable before either is mined.

- **Red at bare `20`:** the *first* call in that test already throws,
with zero other txs involved:
  ```
Highest used index 63 is further than window length from the highest
finalized index 0.
  Tagging window length 20 is configured too low.
  ```
- **Green at `MAX_PRIVATE_LOGS_PER_TX + 20`:** same scenario passes.

This is the key point for review: the `bench_build_block` failure isn't
just "risky under bursty load".`20` is mathematically incapable of
tolerating even one legitimate worst-case single ordinary tx,
independent of what CI on this branch shows either way. CI passing at
`20` only means our current test/bench suite doesn't happen to construct
a single tx tagging more than 20 logs to one recipient; it was never
testing the general safety of the constant, just this specific
workload's exposure to it. The live CI failure above is the same
underlying gap, just surfaced through a different, real workload instead
of a targeted unit test.

We also added tests to pin the window's stacking semantics: a run of
window+1 one-log pending txs to one secret fits, while the next tx
throws.

## Why `+ 20`, not `* 2` or another additive constant

- The floor (`MAX_PRIVATE_LOGS_PER_TX`) covers one worst-case *ordinary*
tx. The failure mode above is about *additional, ordinary-sized* pending
txs to the same counterparty stacking up, not another maxed-out 64-log
tx.
- It's the value the team had already judged sufficient for realistic
pending-tx headroom before #24429 (it was just misapplied as the
*entire* budget instead of a margin on top of the floor).

---------

Co-authored-by: Martin Verzilli <martin@aztec-labs.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-draft Run CI on draft PRs. ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants