Closed holic closed 1 year ago
@alvrs : a use case for afterSetRecord might be if you want to actually change the storage value of what has been set.
Ori: I agree. I don't like the situation where we can fix some changes, but only revert out of others.
Action items here:
onSetRecord
into onBeforeSetRecord
and onAfterSetRecord
onBeforeSetField
and onAfterSetField
onBeforeSpliceRecord
and onAfterSpliceRecord
onDeleteRecord
into onBeforeDeleteRecord
and onAfterDeleteRecord
Approach A: a single Hooks
table with flags for enabling/disabling hooks
Approach B: individual lists for each type of hook (i.e. the hooks table changes its schema from { hooks: address[] }
to { beforeSetRecord: address[], afterSetRecord: [], ... }
)
Note the cost estimates are disregarding the cost to read the dynamic data length because it's the same for both approaches.
SLOAD
) SLOAD
since it can be packed in two slots)SLOAD
)SLOAD
since it can be packed in two slots)Example calculations
before
events of the respective operation.
before
, once for after
, 6x150) = 5000 gasbefore
array (4100 gas), don't need to perform bitmap checks, and don't need to access storage for the after
array = 4100 gasbefore
and after
event of the respective operation
before
, once for after
, 6x150) = 4100 gasbefore
event and three care about the after
event.
before
event of the respective operation, but we have a transaction with two storage operations (eg one setRecord
and one setField
).
before
, once for after
, 6x150). In the second storage operation we again load three hooks but now from warm storage (200 gas) and perform the same bitmap checks as before (6x150) = 6100 gasIn general, for each hook that cares about both the before
and after
event approach A is about 1800 gas cheaper than approach B (due to the missing SLOAD
, minus the bitmap overhead), while for each hook that only cares about one of before
or after
approach B is about 300 gas cheaper than approach A (due to the missing bitmap overhead)
If there is a second storage operation on the same table in the same transaction, approach A is around 1700 gas cheaper than approach B (due to a warm SLOAD
instead of a cold SLOAD
, minus the bitmap overhead).
While I do think in most cases hooks would only care about either before
or after
and I estimate it to be rather uncommon to have multiple storage operations on the same table in a single transaction, the savings for approach A in those scenarios are much more significant (~6x), and 300 gas overhead is also not that significant for operations that likely cost 20k+ (because they're writing storage), so I would go with approach A.
frolic — Today at 1:40 PM thinking about store hooks and we should prob move to either 1) separate registration array per hook method (eg set record) or 2) the registration includes a bitmap of all the hook methods used registering a hook right now means every operation goes through that hook, even if it’s a no op?
alvarius — Today at 1:42 PM we could say we can expect a store hook to implement all functions because data can be modified via each of them, it's similar to how an off-chain indexer has to support all store events and can't just ignore one
frolic — Today at 1:43 PM I’m just thinking this is the worst approach for gas in the hot code path
alvarius — Today at 1:44 PM we could do a bitmap if it doesn't add much gas (which might be reasonable since we can store it in the same storage slot) rn i can't think of a use case of only implementing one store hook function but not another though
frolic — Today at 1:45 PM yeah, there’s a trade off of my two proposals: iterate through all hooks but skip some (per store op) vs more registration gas cost (one time)
alvarius — Today at 1:45 PM do you have something in mind?
frolic — Today at 1:45 PM maybe I’m thinking more about system hooks where you may only want before or after could see potentially doing a before and after for store events too but can’t think of a specific use case right now one thought, prob not a great example: a hook that makes an array field append only
alvarius — Today at 1:48 PM we currently do that for field but not for record, because for handling a field update you might want to just read the entire record from storage before and after the update instead of manually reconstructing the new record with the field update applied in the hook could allow this for setRecord too but the usecase is less clear
frolic — Today at 1:50 PM this feels like one of those “I can’t think of a good example but maybe someone will think of one” and we get some potential gas benefits
curious if there’s a strong case for store hooks implementing all methods
alvarius — Today at 1:51 PM curious if there’s a strong case for store hooks implementing all methods just that if you want to react to data changes you probably want to react to all possible ways the data can change i see the point for system hooks though a use case for afterSetRecord might be if you want to actually change the storage value of what has been set
frolic — Today at 2:01 PM I think my main point is: first order effect is better gas (in the hot code path), second order effect is more granularity
alvarius — Today at 2:05 PM yeah i understand the argument for one array in terms of gas would be: if we expect that every hook implements all functions, one array would be cheaper if a table is modified twice in a transaction with different methods (setRecord, setField, deleteRecord, ...) because the storage is hot for every access after the first one but i see advantages for system hooks, and the potential disadvantages for store hooks are small enough to justify making it consistent for both imo
frolic — Today at 2:17 PM this makes sense, at the very least we could couple a “which hooks” bitmap with the address into the same slot