Edit: Back up now ~6:15pm est. My test suite passes now and the new handles properly decrypt, no code changes on my end. The old failing ones are working properly now.
Old post:
Since some point after 2026-08-30 I cannot decrypt any newly created handle on Sepolia, while older handles on the same contract, same account and same code decrypt normally. I have a clean control on both sides and I have not been able to make it my own bug, so I think it may be infrastructure, but I would rather be told I am wrong.
Symptom
User decryption: Relayer API error [internal_server_error]:
Transaction simulation failed: Execution reverted: execution reverted
status: 500
url: https://relayer.testnet.zama.org/v2/user-decrypt
operation: USER_DECRYPT
Reproduction, with a control
One account, one contract (an OpenZeppelin ERC7984ERC20Wrapper), two balance handles. The
only difference between them is when they were minted.
handle A 0xc2f25cb3d62b950dc2c58e0bba0aec056b0ed5e5c2ff0000000000aa36a70500
minted 2026-08-30T02:01:36Z (block 11595767)
-> decrypts fine, repeatedly, today
handle B 0x6bcd1048d904ae7a1991aecdc7d1592bdcaff43ecfff0000000000aa36a70500
minted 2026-09-01T19:18:12Z (block 11614598)
-> has never decrypted; still failing 45+ minutes after minting
Handle A was decrypted successfully on its first ever attempt, seconds after it was minted, on 2026-08-30. So the same code path did work on a brand-new handle two days earlier, and handle A’s success today cannot be a cached result from some earlier decryption.
I also polled handle B for 25 minutes. It never succeeded.
It is not only us — other people’s contracts are affected right now
This is the part I would most like someone to check, because it means the problem is wider
than one project.
I took recent AllowedForDecryption events straight from the ACL registry at
0xf0Ffdc93b7E186bC2f8CB3dAA75D86d1930A433D, filtered to a contract that has nothing to do
with us — 0x290df9be25eafcc26d6c5e6c2ea8034113ccfee0 — and tried to publicly decrypt the
handles it had just published. No permissions needed; the contract had already marked them
public itself.
handle 0x62f3c08ef2ee0206ff774f667e12b0cc729ea65918ff0000000000aa36a70500 1 min old FAILS
handle 0x15b74a6cfbb914e96beaf75f0331dcce6efc279a12ff0000000000aa36a70500 1 min old FAILS
handle 0x07ab5550118d3506d56e55b037a1e39c9164e8c646ff0000000000aa36a70500 2 min old FAILS
handle 0x58aefab90dc7ebba789bd23d45de45422630e83cadff0000000000aa36a70500 3 min old FAILS
All four report isAllowedForDecryption == true on-chain. All four fail with the same
Transaction simulation failed: Execution reverted.
That contract is still writing new handles as I post this, so whoever runs it is affected too
and may not have noticed yet.
It is not SDK-specific
Both SDKs, both flows, same account, same two handles:
@zama-fhe/relayer-sdk 0.4.4 handle A OK handle B FAIL
@zama-fhe/sdk 3.4.0 (v2 flow) handle A OK (3.5s) handle B FAIL
@zama-fhe/sdk classifies the failure as retryable: false.
It is not one contract or one decryption path
A second person reproduced the same split independently, using publicDecrypt on one contract and userDecrypt on another - three unrelated contracts in total, both mdecryption paths, all showing the same “old works, new does not” boundary.
Reproduction: one account, one session, three minutes apart
This is the tightest version, and it needs no comparison across days or accounts.
Account 0x9B25F1f746147796DeE1A11637CcA8770e0A73E0, one pool contract, one EIP-712 permit,
nothing else touched:
1. decrypt my confidential share -> OK
handle 0x92dcd3ab20b47e0c40cb00ffc15462aa8aa541d7dbff0000000000aa36a70500
2. deposit into the pool -> OK, mined 2026-09-01T20:36:36Z, block 11614978
the deposit updates my share, which necessarily produces a NEW handle
handle 0xbde263d033db61abacf8eff9df5a3a066ece79206fff0000000000aa36a70500
3. decrypt my confidential share again -> FAILS, and has not recovered
The account did not change, the contract did not change, the permit did not change, and the
old handle had been decrypting happily moments earlier. The only thing that changed is that
step 2 created a new handle. Both handles report persistAllowed == true for this account.
So an account is not “good” or “bad” — it becomes affected the instant it writes a new value.
What is correct on-chain at the moment of failure
ACL.persistAllowed(handleB, owner) true
ACL.persistAllowed(handleB, token) true
confidentialBalanceOf(owner) == handle B (it is the current handle, not a stale one)
An established public handle on an unrelated contract also decrypts throughout, in about
2.4 s, so the relayer and the KMS are clearly up and answering:
publicDecrypt(0xbff29d0ca71b95104aa88c7c6237e6e1976ed5efb7ff0000000000aa36a70500) -> 650
What we checked on our own side first
Before concluding anything, in case something of ours changed in the same window:
- Our contracts cannot have changed. All three are plain and immutable — no EIP-1967
implementation, admin or beacon slots are set, so there is no upgrade path. The pool’s
deployed bytecode is byte-for-byte the length we recorded on 2026-08-23. On 2026-08-30 its
handles decrypted immediately; today they do not. Identical bytecode cannot change its own
behaviour. - Nothing was redeployed. No
.soland no deployment manifest has changed in our
repository since 2026-08-29; the only edits are to off-chain scripts. - It is not our client. Two SDKs sharing none of the same client code behave identically.
- It is not this machine. Its clock is within 10 s of chain time, and older handles decrypt
from the same machine, the same network and the same TLS session, minutes apart from the
failures. - It is not our own load. Our only automation funds once per draw and otherwise exits in
about two seconds. - KMS key material initializes.
/v2/keyurlanswers 200, and the KMS contract returns a
signer set and a threshold of 7.
Ruled out
- Permissions -
persistAllowedis true for both the owner and the token. - A stale handle - the failing handle is the one
confidentialBalanceOfcurrently returns. - Address checksumming - all addresses are EIP-55 checksummed. (Passing a lowercase
address gives a different, client-sideBad address checksumfrom the KMS WASM; this is not that.) - Permit validity window -
startTimestampwas 150 s behind chain time; our clock is
within 10 s of the chain. - Signer/user mismatch - that produces “User address … is not authorized”, not this.
- A general outage - established handles decrypt throughout, on this contract and others.
- SDK version or the legacy/v2 split - both fail identically.
Environment
chain Sepolia (11155111)
tokens OpenZeppelin ERC7984ERC20Wrapper over a 6-decimal ERC-20
SDKs @zama-fhe/relayer-sdk 0.4.4 and @zama-fhe/sdk 3.4.0
The question
Is this expected behaviour that I should be designing around - a coprocessor or Gateway replication window that can extend to tens of minutes - or is something stuck on the Sepolia testnet since roughly 2026-08-31?
I ask because the two have opposite consequences for my application. If it is a window, an app has to show “not readable yet” and retry, and I would love to know a realistic upper bound so the copy can be honest. If something is stuck, no amount of client-side patience helps.
For what it is worth, the practical impact on our app is not just balances: freezing a value for a scheduled task creates a new handle too, so anything that snapshots-then-decrypts stalls in the same way.
Happy to supply transaction hashes, a minimal repro script, or to re-run anything against a specific handle.