Skip to content

feat: store discovered handshakes as facts#24483

Merged
nchamo merged 11 commits into
merge-train/fairies-v5from
mv/f-766-handshake-facts
Jul 4, 2026
Merged

feat: store discovered handshakes as facts#24483
nchamo merged 11 commits into
merge-train/fairies-v5from
mv/f-766-handshake-facts

Conversation

@vezenovm

@vezenovm vezenovm commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Why we are doing this

Discovered handshakes were persisted in capsule storage, which is not reorg-aware: a reorg left stale handshakes behind, and the sync cursor stayed past the pruned blocks, so handshakes re-mined in replacement blocks were never rescanned. Facts are tied to an origin block and PXE retracts them when that block is pruned, so storing handshakes as facts makes discovery reorg-safe, the same way F-682 did for offchain message reception.

The change

  • Aztec.nr log retrieval switches to aztec_utl_getLogsByTagV2, resolving the TODO from feat(pxe): origin block number timestamp log oracle in log retrieval #24398. The response gains the log's origin block:
    • block_number
    • block_timestamp
    • block_hash (also added to the PXE-side response, with an oracle minor version bump)
  • Each discovered handshake is a retractable fact in a single per-recipient collection, originated at the block its announcement log landed in. A reorg retracts exactly the handshakes carried by pruned blocks. Re-recording an identical fact is a no-op, and facts keep insertion order, so get_handshakes pagination stays stable.
  • Adds facts::RetractableRegister: a reorg-aware mutable value on top of retractable facts. Writes append versions anchored at origin blocks, reads return the newest surviving version, and rollback is performed by the store, never by the writer. History behind the newest finalized version is compacted away, and a write anchored behind the current version panics.

Fixes F-766

@vezenovm
vezenovm requested a review from nventuro as a code owner July 2, 2026 21:26
@vezenovm
vezenovm requested a review from nchamo July 2, 2026 21:31
@nchamo
nchamo force-pushed the mv/f-766-handshake-facts branch from 23d58bd to 5fd8fd3 Compare July 3, 2026 01:51
/// Versions originated at or before the finalized tip can never be retracted, so anything older than the newest
/// finalized version can never become current again. [`write`](Self::write) discards that dead history. After the
/// first finalized write, a reorg can therefore roll the register back at worst to that finalized version.
pub struct RetractableRegister<T> {

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.

Our sync cursor is basically a block number that we use to determine how to sync (we fetch logs from cursor to anchor block). This happens each time we execute a sync on a contract, so it can be quite a frequent. So if we used "plain" facts, we would be creating lots of them.

But in this case, we only care about the "last version" of the sync cursor. Once we have a finalized version, we can clean all that came before. I thought this could happen in other cases, so I tried to generalize it a little bit here

I don't love the name RetractableRegister, so I'm open to suggestions

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.

Maybe VersionedRecord? I think the main salient feature of this is the idea of multiple versions of the same thing

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.

Do we need it to be pub? I would keep it pub(crate) if possible so we don't buy into a backwards compatibility commitment for something that is new (this might as a consequence call for this module to be moved to where the handshake registry contract lives, I don't know)

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.

Could also be RewindableRegister, emphasizing the fact that it might be rewound depending on chain state

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.

Went with RewindableRegister, and moved it into the contract module

// TODO: PXE also exposes `getLogsByTagV2`, whose response additionally carries the origin block number and timestamp
// of each log. Switch this oracle (and `LogRetrievalResponse`) over to it once a consumer needs that context.
#[oracle(aztec_utl_getLogsByTag)]
#[oracle(aztec_utl_getLogsByTagV2)]

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.

getLogsByTagV2 wasn't used before this PR, so we don't need to write an adapter for it

@nchamo nchamo self-assigned this Jul 3, 2026
@nchamo nchamo added the ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure label Jul 3, 2026
@nchamo
nchamo requested review from nchamo and removed request for nchamo July 3, 2026 02:05
@nchamo
nchamo requested a review from mverzilli July 3, 2026 11:09
Comment thread noir-projects/aztec-nr/aztec/src/facts/retractable_register.nr Outdated
Comment on lines +61 to +65
/// # Panics
/// If `origin_block` is strictly behind the current version's origin: the current version is the one with the
/// highest origin block, not the most recently written, so a write anchored behind it would not take effect.
/// Re-writing at the current version's origin is allowed: re-recording an identical fact is a no-op, which
/// keeps rescans idempotent.

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.

Is this necessary?

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.

Discussed below

Comment thread noir-projects/aztec-nr/aztec/src/facts/retractable_register.nr Outdated
///
/// The current version is the one with the highest surviving origin. Returns `None` if no version survives:
/// nothing was ever written, or a reorg retracted every version.
pub unconstrained fn read(self) -> Option<(T, RetractableFactOrigin)>

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.

interesting how RetractableFactOrigin feels leaky for this, maybe we need a more general block + block state description (being nitpicky here, not for this PR)

}

/// Returns every surviving version, or `None` if there is none.
unconstrained fn maybe_versions(self) -> Option<EphemeralArray<Fact>> {

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.

would rename this to versions, the compiler will take care of not letting us use it without unwrapping

Comment on lines +73 to +76
assert(
origin_block.block_number >= current_version(versions).origin_block.unwrap().block_number,
"RetractableRegister write is anchored behind the current version",
);

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.

wouldn't this assertion here mean a logic error causes a crash? maybe we could make the function's contract explicitly be "writes in the past are ignored"?

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.

That was my original implementation, writes in the past were no-op. But I believe that a scenario like that was most likely an error in the contract logic, and I thought it made sense to raise it instead of silently ignoring it. If we know the client is doing something wrong, why hide/support it?

But I'm not against changing it back, just wanted to share my opinion. Do you believe we should go back?

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.

this is probably fine if we keep the module out of reach of userland, given an error in the handshake registry contract is basically a bug in Aztec.nr.

if we make this feature available to contract devs in general, what worries me is that misusing it can cause a whole tx to fail (thinking mostly of txs where there's multiple contracts interacting). maybe I'm being too conservative?

@nchamo nchamo Jul 3, 2026

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.

Moved it into the registry's module

Comment thread noir-projects/aztec-nr/aztec/src/facts/retractable_register.nr Outdated
Comment thread noir-projects/aztec-nr/aztec/src/facts/retractable_register.nr Outdated
Comment thread noir-projects/aztec-nr/aztec/src/facts/retractable_register.nr Outdated
Comment thread noir-projects/aztec-nr/aztec/src/facts/retractable_register.nr Outdated
Comment on lines +122 to +123
/// Facts within a collection are insertion-ordered, so new handshakes only ever append, keeping
/// [`HandshakeRegistry::get_handshakes`] pagination stable across pages.

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.

false, there's purposedly no guarantees of this, let's not rely on this fact

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.

I re-phrased it. It was wrong

@mverzilli mverzilli 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.

Super nice! Leaving aside the design suggestions and nitpicks, it's important to make this resilient to store insertion ordering instability. Any order reqs need to be handled explicitly.

@nchamo
nchamo merged commit 5833a9e into merge-train/fairies-v5 Jul 4, 2026
12 checks passed
@nchamo
nchamo deleted the mv/f-766-handshake-facts branch July 4, 2026 02:54
vezenovm added a commit that referenced this pull request Jul 6, 2026
…24511)

## Summary

- Renames the HandshakeRegistry's `get_handshakes` utility to
`get_non_interactive_handshakes`: it only returns handshakes discovered
from non-interactive announcement logs, so interactive handshakes
(#24473) never appear in it, and the name should say which kind it
returns.
- Sweeps the rename through the aztec-nr selector constant, the
registry's internal reader, the PXE default-authorization allowlist, and
tests, and documents the getter's privacy properties (any contract can
read a scope's discovered handshakes; ephemeral keys don't reveal the
shared secret).
- Re-pins the standard contracts, since the registry's artifact changes.

Stacked on #24483.

Fixes F-769

---------

Co-authored-by: Maxim Vezenov <mvezenov@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

3 participants