Summary
Multisig.syncProposals() in @openzeppelin/miden-multisig-client merges the GUARDIAN response into a persistent this.proposals Map but never prunes it. Once a proposal is executed, the GUARDIAN server drops it from getDeltaProposals (it only returns pending proposals), so a co-signer who did not execute the transaction locally keeps seeing the executed proposal as pending — indefinitely.
Reproduction (2-of-2 multisig, two browsers)
- Signer 2 creates a proposal and signs it (1/2).
- Signer 1 signs it (2/2) and executes it.
- Signer 2 syncs.
Observed: the executed transaction is still listed as a pending proposal on Signer 2, stuck at 1/2.
Expected: it disappears from Signer 2's pending list.
Root cause
syncProposals() iterates the GUARDIAN deltas doing this.proposals.set(...), then returns Array.from(this.proposals.values()). When GUARDIAN returns an empty (or reduced) list, the loop leaves previously-cached proposals in the Map. The signer who executed the tx updated its own cache locally as part of executing; every other signer's cache silently ignores the now-empty response and keeps the executed proposal pinned as pending.
Versions
@openzeppelin/miden-multisig-client, @openzeppelin/guardian-client, and guardian server all on 0.16.x.
Summary
Multisig.syncProposals()in@openzeppelin/miden-multisig-clientmerges the GUARDIAN response into a persistentthis.proposalsMap but never prunes it. Once a proposal is executed, the GUARDIAN server drops it fromgetDeltaProposals(it only returns pending proposals), so a co-signer who did not execute the transaction locally keeps seeing the executed proposal as pending — indefinitely.Reproduction (2-of-2 multisig, two browsers)
Observed: the executed transaction is still listed as a pending proposal on Signer 2, stuck at 1/2.
Expected: it disappears from Signer 2's pending list.
Root cause
syncProposals()iterates the GUARDIAN deltas doingthis.proposals.set(...), then returnsArray.from(this.proposals.values()). When GUARDIAN returns an empty (or reduced) list, the loop leaves previously-cached proposals in the Map. The signer who executed the tx updated its own cache locally as part of executing; every other signer's cache silently ignores the now-empty response and keeps the executed proposal pinned as pending.Versions
@openzeppelin/miden-multisig-client,@openzeppelin/guardian-client, and guardian server all on 0.16.x.