Struct DelayedPublicMutable
pub struct DelayedPublicMutable<T, let InitialDelay: u64, Context>
{ /* private fields */ }
Implementations
impl<let InitialDelay: u64, T> DelayedPublicMutable<T, InitialDelay, PublicContext>
pub fn schedule_value_change(self, new_value: T)
Schedules a write to the current value.
The current value does not immediately change. Once the current delay passes,
get_current_value automatically begins to return new_value.
Multiple Scheduled Changes
Only a single value can be scheduled to become the new value at a given point in time. Any prior scheduled changes which have not yet become current are replaced with the new one and discarded.
To illustrate this, consider a scenario at t0 with a current value A. A value change to B is scheduled to
occur at t1. At some point before t1, a second value change to C is scheduled to occur at t2 (t2 > t1). The result is that the current value continues to be A all the way until t2, at which point it
changes to C.
This also means that it is possible to cancel a scheduled change by calling schedule_value_change with
the current value.
Examples
A public setter that authorizes a user:
#[external("public")]
fn authorize_user(user: AztecAddress) {
assert_eq(self.storage.admin.read(), self.msg_sender(), "caller is not admin");
self.storage.user_authorization.at(user).schedule_value_change(true);
}
Cost
The SSTORE AVM opcode is invoked 2 * N + 2 times, where N is T's packed length.
pub fn schedule_and_get_value_change(self, new_value: T) -> ScheduledValueChange<T>
Schedules a write to the current value, returning the scheduled entry.
pub fn schedule_delay_change(self, new_delay: u64)
Schedules a write to the current delay.
This works just like schedule_value_change, except instead of
changing the value in the state variable, it changes the delay that will govern future invocations of that
function.
The current delay does not immediately change. Once the current delay passes,
get_current_delay automatically begins to return new_delay, and
schedule_value_change begins using it.
Multiple Scheduled Changes
Only a single delay can be scheduled to become the new delay at a given point in time. Any prior scheduled changes which have not yet become current are replaced with the new one and discarded.
To illustrate this, consider a scenario at t0 with a current delay A. A delay change to B is scheduled to
occur at t1. At some point before t1, a second delay change to C is scheduled to occur at t2 (t2 > t1). The result is that the current delay continues to be A all the way until t2, at which point it
changes to C.
Delays When Changing Delays
A delay change cannot always be immediate: if it were, then it'd be possible to break DelayedPublicMutable's
invariants by setting the delay to a very low or zero value and then scheduling a value change, resulting in a
new value becoming the current one earlier than was predictable based on the prior delay. This would prohibit
private reads, which is the reason for existence of this state variable.
Instead, delay changes are themselves scheduled and delay so that the property mentioned above is preserved. This results in delay increases and decreases being asymmetrical.
If the delay is being decreased, then this requires a delay equal to the difference between the current and new delay, so that a scheduled value change that occurred as the new delay came into effect would be scheduled for the same timestamp as if no delay change had occurred.
If the delay is being increased, then the new delay becomes effective immediately, as new value changes would be scheduled for a timestamp that is further than the current delay.
Examples
A public setter that sets the pause delay:
#[public]
fn set_pause_delay(delay: u64) {
assert_eq(self.storage.admin.read(), self.msg_sender(), "caller is not admin");
self.storage.paused.schedule_delay_change(delay);
}
Cost
The SSTORE AVM opcode is invoked 2 * N + 2 times, where N is T's packed length.
pub fn get_current_value(self) -> T
Returns the current value.
If schedule_value_change has never been called, then this
returns the default empty public storage value, which is all zeroes - equivalent to let t = T::unpack(std::mem::zeroed());.
It is not possible to detect if a DelayedPublicMutable has ever been initialized or not other than by testing
for the zero sentinel value. For a more robust solution, store an Option<T> in the DelayedPublicMutable.
Use get_scheduled_value to instead get the last value that was
scheduled to become the current one (which will equal the current value if the delay has already passed).
Examples
A public getter that returns a user's authorization status:
#[external("public")]
fn is_authorized(user: AztecAddress) -> bool {
self.storage.user_authorization.at(user).get_current_value()
}
Cost
The SLOAD AVM opcode is invoked 2 * N + 1 times, where N is T's packed length.
pub fn get_current_delay(self) -> u64
Returns the current delay.
This is the delay that would be used by schedule_value_change
if it were called in the current transaction.
If schedule_delay_change has never been called, then this
returns the InitialDelay used in the storage struct.
Use get_scheduled_delay to instead get the last delay that was
scheduled to become the current one (which will equal the current delay if the delay has already passed).
Examples
A public getter that returns the pause delay:
#[external("public")]
fn get_pause_delay() -> u64 {
self.storage.paused.get_current_delay()
}
Cost
The SLOAD AVM opcode is invoked a single time, regardless of T.
pub fn get_scheduled_value(self) -> (T, u64)
Returns the last scheduled value and timestamp of change.
pub fn get_scheduled_delay(self) -> (u64, u64)
Returns the last scheduled delay and timestamp of change.
impl<let InitialDelay: u64, T> DelayedPublicMutable<T, InitialDelay, UtilityContext>
pub unconstrained fn get_current_value(self) -> T
impl<let InitialDelay: u64, T> DelayedPublicMutable<T, InitialDelay, &mut PrivateContext>
pub fn get_current_value(self) -> T
Returns the current value.
If schedule_value_change has never been called, then this
returns the default empty public storage value, which is all zeroes - equivalent to let t = T::unpack(std::mem::zeroed());.
It is not possible to detect if a DelayedPublicMutable has ever been initialized or not other than by testing
for the zero sentinel value. For a more robust solution, store an Option<T> in the DelayedPublicMutable.
Privacy
This function does leak some privacy, though in a subtle way. Understanding this is key to understanding how to
use DelayedPublicMutable in a privacy-preserving way.
Private reads are based on a historical public storage read at the anchor block (i.e.
crate::history::storage::public_storage_historical_read). DelayedPublicMutable is able to provide
guarantees about values read in the past remaining the state variable's current value into the future due to
the existence of delays when scheduling writes. It then sets the expiration_timestamp property of the current
transaction (see
PrivateContext::set_expiration_timestamp) to
ensure that the transaction can only be included in a block prior to the state variable's value changing.
In other words, it knows some facts about the near future up until some time horizon, and then makes sure that
it doesn't act on this knowledge past said moment.
Because the expiration_timestamp property is part of the transaction's public information, any mutation to
this value could result in transaction fingerprinting. Note that multiple contracts may set this value during a
transaction: it is the smallest (most restrictive) timestamp that will be used.
If the state variable does not have any value changes scheduled, then the timestamp will be set to that of
the anchor block plus the current delay. If multiple contracts use the same delay for their
DelayedPublicMutable state variables, then these will all be in the same privacy set.
If the state variable does have a value change scheduled, then the timestamp will be set to equal the time
at which the current value will change, i.e. the one
get_scheduled_value returns - which is public information. This
results in an unavoidable privacy leak of any transactions in which a contract privately reads a
DelayedPublicMutable that will change soon.
Transactions that do not read from a DelayedPublicMutable are part of a privacy set in which the
expiration_timestamp is set to their anchor block plus crate::protocol::constants::MAX_TX_LIFETIME,
making this the most privacy-preserving delay. The less frequent said value changes are, the more private the
contract is. Wallets can also then choose to further lower this timestamp to make it less obvious that their
transactions are interacting with this soon-to-change variable.
Examples
A private action that requires authorization:
#[external("private")]
fn do_action() {
assert(
self.storage.user_authorization.at(self.msg_sender()).get_current_value(),
"caller is not authorized"
);
// do the action
}
A private action that can be paused:
#[external("private")]
fn do_action() {
assert(!self.storage.paused.get_current_value(), "contract is paused");
// do the action
}
Cost
This function performs a historical public storage read (which is in the order of 4k gates), regardless of
T's packed length. This is because DelayedPublicMutable::schedule_value_change stores not just the
value but also its hash: this function obtains the preimage from an oracle and proves that it matches the hash
from public storage.
Because of this reason, if two mutable values are often privately read together it can be convenient to group
them in a single type T. Note however that this will result in them sharing the mutation delay:
// Bad: reading both `paused` and `fee` will require two historical public storage reads
#[storage]
struct Storage<C> {
paused: DelayedPublicMutable<bool, INITIAL_DELAY_S, C>,
fee: DelayedPublicMutable<Field, INITIAL_DELAY_S, C>,
}
// Good: both `paused` and `fee` are retrieved in a single historical public storage read
#[derive(Packable)]
struct Config {
paused: bool,
fee: Field,
}
#[storage]
struct Storage<Context> {
config: DelayedPublicMutable<Config, INITIAL_DELAY_S, Context>,
}
Mutable public values with private read access.
This is an advanced public state variable, with no native Solidity equivalent.
Like
PublicMutableit represents a public value of typeTthat can be written to repeatedly, but with a key improvement: the current value can also be read from a private contract function.This comes at the cost of extra restrictions on the state variable: writes do not come into effect immediately, they must be scheduled to take place after some minimum delay. Reading from the state variable will therefore return the previous value until some time passes, which is why this is a delayed mutable variable.
It is these delays that enable the capacity for reads from private contract functions, as they provide guarantees regarding how long can some historical state observed at the anchor block be known to not change.
Delays can be modified during the lifetime of the contract.
Access Patterns
The current value stored in a
DelayedPublicMutablecan be read from public contract functions, and writes can be scheduled to happen in the future.Public contract functions can also schedule changes to the write delay, as well as inspect any already scheduled value or delay changes.
Private contract functions can read the current value of the state variable, but not past or scheduled values. They cannot read the current delay, and they cannot schedule any kind of value change.
Privacy
The value stored in
DelayedPublicMutableis fully public, as are all scheduled value and delay changes.Reads from a private contract function are almost fully private: the only observable effect is that they set the transaction's
expiration_timestampproperty, possibly reducing the privacy set. SeePrivateContext::set_expiration_timestamp.Use Cases
These are mostly an extension of
PublicMutable's, given that what this state variable essentially achieves is to provide private reads to it. For example, it can be used for global contract configuration (such as fees, access control, etc.) that users will need to access during private interactions.The key consideration is whether the enforced minimum delay on writes prevents using this state variable. In some scenarios this restriction is incompatible with requirements (such as a token's total supply, which must always be up to date), while in others the enhanced privacy might make the tradeoff acceptable (such as when dealing with contract pauses or access control revocation, where a delay of some hours could be acceptable).
Note that, just like in
PublicMutable, the fact that the values are public does not necessarily mean the actions that update these values must themselves be wholly public. To learn more, see the notes there regarding usage ofonly_self.Choosing Delays
A short delay reduces the most obvious downside of
DelayedPublicMutable, and so it is natural to wish to make it be as low as possible. It is therefore important to understand the tradeoffs involved in delay selection.A shorter delay will result in a lower
expiration_timestampproperty of transactions that privately read the state variable, reducing its privacy set. If the delay is smaller than that of any other contract, then this privacy leak might be large enough to uniquely identify those transactions that interact with the contract - fully defeating the purpose ofDelayedPublicMutable.Additionally, a lower
expiration_timestampobviously causes transactions to expire earlier, resulting in multiple issues. Among others, this can make large transactions that take long to prove be unfeasible, restrict users with slow proving devices, and force large transaction fees to guarantee fast inclusion.In practice, a delay of at least a couple hours is recommended. From a privacy point of view the optimal delay is
crate::protocol::constants::MAX_TX_LIFETIME, which puts contracts in the same privacy set as those that do not useDelayedPublicMutableat all.Examples
Declaring a
DelayedPublicMutablein the contract'sstoragestruct requires specifying the typeTthat is stored in the variable, along with the initial delay used when scheduling value changes:Note that this initial delay can be altered during the contract's lifetime via
DelayedPublicMutable::schedule_delay_change.Requirements
The type
Tstored in theDelayedPublicMutablemust implement theEqandPackabletraits.Implementation Details
This state variable stores more information in public storage than
PublicMutable, as it needs to keep track of the current and scheduled change information for both the value and the delay - seecrate::protocol::delayed_public_mutable::DelayedPublicMutableValues.It also stores a hash of this entire configuration so that private reads can be performed in a single historical public storage read - see
crate::utils::WithHash.This results in a total of
N * 2 + 2storage slots used, whereNis the packing length of the stored typeT. This makes it quite important to ensureT's implementation ofPackableis space-efficient.