OpenZeppelin / OpenZeppelin/guardian

Per-account proposal handshake cost grows with interleaved proposal traffic (send + consume) — +128–153% over 300 ops

Open
#320 4 comments 1 reaction 1 assignee View on GitHub

@zeljkoX is already working on this.

Since Jul 17, 2026.

cla: allowlist performance
Dominant language
Rust
Stars
10
Forks
20
Avg merge
2d 11h
Merged PRs (30d)
43

Description

Environment
  • guardian.openzeppelin.com v0.15.1 (git 82225104c8a2), Miden testnet
  • Paired A/B stress runs: identical PRNG seed, 300 guardian-cosigned sends, per-transaction stage timing. Single variable: the receiving wallet either also issued consume (claim) proposals for ~50% of received notes, or none at all.
Observation

With interleaved consumes, the guardian handshake stages grow monotonically over the run; removing consume traffic flattens exactly those stages:

stage growth Q1→Q5, claims on claims off
proposal-create → signing +153% +77% (non-monotonic)
signing → sending +128% +41%
sending → submitting +43% 0%

Everything else was measured flat in both runs (client sync, proving, block wait, local execution, UI). Net effect: median send latency grows from ~11 s to ~19 s across the run, driven by these stages. Since the flattening tracks the traffic-mix knob and not client state, session age (survives page reloads), or network (per-call durations flat), the growth follows per-account proposal co-traffic.

This looks like the non-catastrophic sibling of #303: #312 fixed the multi-minute stall/409-churn form; the steady per-account cost growth under normal interleaved traffic remains.

Where the time goes (narrowing measurements)
  • Server per-call response times are flat. Guardian delta/proposal endpoints answered in ~250–280 ms at the start AND end of the runs; sync RPCs pinned at 201–215 ms. The growth is not server response latency per request.
  • The number of round-trips per proposal grows. In a heavier run on the same account shape (2026-07-13, 442 ops), client-issued guardian proposal-path calls grew from 7.5 to 12.2 per operation while per-call durations stayed flat — the handshake takes more exchanges, not slower ones.
  • Account payload grows. GetAccount was the only RPC whose duration grew (305 → 483 ms) over that run, tracking a growing account state payload.
  • It is not client-session state. Full page reloads of both wallets at ops 100 and 200 produced no reset (+0.8% / +5.9% across the boundaries) — whatever accumulates is durable/server-side, not in-memory client state.
  • It is not queue depth at send time. The sender's local tx queue was empty at the start of nearly every op; latency ramps even for ops issued with zero in-flight work.
Suspected mechanism (falsifiable)

Per-proposal work appears to scale with the account's accumulated delta/proposal history: each handshake re-fetches or re-derives per-account state whose size grows with every canonicalized delta, and interleaved consume proposals both add history faster and add a canonicalization cycle (~10–12 s, #316) that overlaps the next send's handshake. Two checks would confirm or kill this: (1) do get_deltas/state-fetch response sizes grow linearly with canonicalized-delta count? (2) does per-proposal server-side work (DB reads, verify passes) scale with history length? If both are flat, the growth must be in the client-side multisig handshake logic instead — we can instrument that side next.

Reproduction

Two guardian-backed accounts on testnet, v0.15.1. Alternate ~300 sends between them; receiver claims ~50% of notes (each claim = a consume proposal on the receiving account). Measure proposal-create → submit per transaction. Expected: those stages roughly double by op ~300. Control: same seed with claims disabled — the submit stage stays flat and the others grow at less than half the rate. Full per-stage dataset (both runs, per-transaction timestamps) available — happy to attach.

References

#303 / #312 · #316 (post-send canonicalization wait) · #317 (scalability umbrella)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.