Skip to content

fix(aztec-nr): reject zero delays in DelayedPublicMutable - #25104

Open
vezenovm wants to merge 15 commits into
merge-train/fairiesfrom
mv/f-840-nonzero-delay
Open

fix(aztec-nr): reject zero delays in DelayedPublicMutable#25104
vezenovm wants to merge 15 commits into
merge-train/fairiesfrom
mv/f-840-nonzero-delay

Conversation

@vezenovm

@vezenovm vezenovm commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Fixes F-840

A zero delay leaves no window during which a private read is known to remain valid, and get_effective_minimum_delay_at underflowed instead of reporting one: private simulation aborted with an opaque "attempt to subtract with overflow" instead of failing where the delay was configured.

I decided as this does affect the soundness of the state variable we may as well validate on construction and restrict these invalid declarations. For setting a sufficiently high enough delay I think the current docs outline that clearly. I think we could potentially decide on a good "floor" delay policy, but that is an API decision that is unclear without more user stories.

Changes

  • ScheduledDelayChange::schedule_change asserts a strictly positive delay.
  • A zero initial delay is rejected at compile time when declaring a DelayedPublicMutable, in aztec-nr and the aztec_sublib fork. That is where a contract author supplies the generic, so it is where the error is most useful.
  • unpack_delay_change carries the same compile-time guard. Every read of a stored delay funnels through it, so this also covers readers that do not go through a state variable, such as the private kernel's contract update horizon. static_assert emits no constraints, so neither guard costs anything at proving time.
  • The restriction is documented alongside delay selection in the state variable docs, and on the docs site.

assert_effective_minimum_delay_invariants deliberately scheduled a zero delay, so it now reduces to 1, the smallest permitted value.

No test contract in this repo hits this case: declared delays are 360, 86400, 2 and DEFAULT_UPDATE_DELAY, and the instance registry already rejects anything below MINIMUM_UPDATE_DELAY.

A zero delay leaves no window in which a private read is known to remain
valid, and `get_effective_minimum_delay_at` underflowed instead of
reporting one, so private simulation aborted with an opaque
"attempt to subtract with overflow".

Reject the misconfiguration where it is made: `schedule_change` asserts a
strictly positive delay, and a zero `INITIAL_DELAY` fails to compile.
@vezenovm
vezenovm marked this pull request as draft August 4, 2026 23:47
@vezenovm
vezenovm removed the request for review from LeilaWang August 4, 2026 23:47
@vezenovm
vezenovm marked this pull request as ready for review August 5, 2026 17:14
@vezenovm
vezenovm requested a review from nchamo August 5, 2026 17:15
Guard `unpack_delay_change`, the single funnel every read of a stored delay goes
through. This covers the private kernel's contract update horizon, which does not
go through a state variable and so was not covered by the wrapper guard.
@vezenovm vezenovm changed the title fix: reject zero delays in DelayedPublicMutable fix(aztec-nr): reject zero delays in DelayedPublicMutable Aug 5, 2026

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

A tx in which the value is privately read sets its experation time to the anchor block time plus the minimum delay. This is because it is possible for there to be a second block at the same time in which the value is updated, and this update would happen (at the earliest) after said delay - hence the tx is only valid before then.

If the delay is zero, then there could be an immediate next block in which the value is instantly changed, and there is no possible expiration timestamp that can be set - if set at the anchor block then the value might still change and render the read invalid, and if set before then the tx would be invalid by construction.

Notably, this only affects private reads: in public the delay is a non-issue due to execution happening at the tip of the chain with perfect knowledge of current state.

I argue that preventing a zero delay is security theatre and provides a false sense of security. Minimum delays of e.g. one, five or ten seconds are equally useless, as it is impossible to create a block, broadcast it, use it as an anchor, craft a new transaction, prove it, broadcast it to the sequencer and have it be included in less than this time. We could opt for setting a minimum value of e.g. one or two minutes, though that would convey a sense of this being a safe value - and it's not clear to me that we can have a minimum that is safe for all usecases.


The 'Choosing Delays' section of the docs already recommends a minimum delay of 'at least a couple hours'. I'd expand that section slightly explaining why low delays are highly problematic, and would add a large minimum delay - I think one hour would be ok. The better reason to prevent delays so short that they might impede action is precisely to avoid letting admins impede user action via delay selection, which is a very subtle and implicit way to escalate privileges. If we don't add this, then they will need to add these safeguards to their setDelay functions, and we don't want that.

The zero guard only rejects a configuration that can never work; values
above zero are not necessarily safe and there is no universal minimum.
Document that delay selection is itself a power: an account that can
schedule delay changes can make private reads unusable, so setters
exposed to privileged accounts should enforce a use-case minimum.
@vezenovm

vezenovm commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

zero delay is security theatre and provides a false sense of security. Minimum delays of e.g. one, five or ten seconds are equally useless, as it is impossible to create a block, broadcast it, use it as an anchor, craft a new transaction, prove it, broadcast it to the sequencer and have it be included in less than this time.

Yes, I see how there is not a clear set safe floor value for all use cases. In fact in the PR description I say "I think we could potentially decide on a good "floor" delay policy, but that is an API decision that is unclear without more user stories." but deciding an appropriate "floor" felt out of scope for now and also it isn't clear what delay could be valid.

However, I think it is a bit strong to call it "theatre". The goal isn't to make a safety claim about over which delay the variable is safe. It is simply to add a correctness guard as a zero delay variable is broken by construction. When we try to read the value in private with get_current_value, ScheduledDelayChange::get_effective_minimum_delay_at is called and will error with "attempt to subtract with overflow". Independent of use case the protocol arithmetic breaks with an unclear error.

I felt like still restricting as zero still makes things clearer as we have a deterministic invalid state. 1s/5s/10s at least produce an expiration horizon, even if it is invalidly set. 0 does not produce any expiration horizon.

I agree the documentation can be improved a bit more though here so I mention explicitly how a zero delay does not make usage of the variable safe. I also added how delay selection can allow admins to prevent user actions. If you still think we should take out the zero guard I can remove it, but it feels like little harm and prevents an unclear error later on.

@vezenovm
vezenovm requested a review from nventuro August 10, 2026 15:00
Comment on lines +88 to +89
/// privileged account call [`DelayedPublicMutable::schedule_delay_change`] should enforce a minimum delay appropriate
/// to their use case in that function.

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 see your point re. disallowing zero, but I think this is the key part. We force users to do this or have a hidden escalation of privileges: I'd rather we had an e.g. 1 hour minimum so that this risk is massively diminished.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants