diff --git a/.changeset/few-papayas-leave.md b/.changeset/few-papayas-leave.md new file mode 100644 index 0000000000..27a1cfa787 --- /dev/null +++ b/.changeset/few-papayas-leave.md @@ -0,0 +1,97 @@ +--- +"@latticexyz/store": major +"@latticexyz/world": major +--- + +- The `IStoreHook` interface was changed to replace `onBeforeSetField` and `onAfterSetField` with `onBeforeSpliceStaticData`, `onAfterSpliceStaticData`, `onBeforeSpliceDynamicData` and `onAfterSpliceDynamicData`. + + This new interface matches the new `StoreSpliceStaticData` and `StoreSpliceDynamicData` events, and avoids having to read the entire field from storage when only a subset of the field was updated + (e.g. when pushing elements to a field). + + ```diff + interface IStoreHook { + - function onBeforeSetField( + - bytes32 tableId, + - bytes32[] memory keyTuple, + - uint8 fieldIndex, + - bytes memory data, + - FieldLayout fieldLayout + - ) external; + + - function onAfterSetField( + - bytes32 tableId, + - bytes32[] memory keyTuple, + - uint8 fieldIndex, + - bytes memory data, + - FieldLayout fieldLayout + - ) external; + + + function onBeforeSpliceStaticData( + + bytes32 tableId, + + bytes32[] memory keyTuple, + + uint48 start, + + uint40 deleteCount, + + bytes memory data + + ) external; + + + function onAfterSpliceStaticData( + + bytes32 tableId, + + bytes32[] memory keyTuple, + + uint48 start, + + uint40 deleteCount, + + bytes memory data + + ) external; + + + function onBeforeSpliceDynamicData( + + bytes32 tableId, + + bytes32[] memory keyTuple, + + uint8 dynamicFieldIndex, + + uint40 startWithinField, + + uint40 deleteCount, + + bytes memory data, + + PackedCounter encodedLengths + + ) external; + + + function onAfterSpliceDynamicData( + + bytes32 tableId, + + bytes32[] memory keyTuple, + + uint8 dynamicFieldIndex, + + uint40 startWithinField, + + uint40 deleteCount, + + bytes memory data, + + PackedCounter encodedLengths + + ) external; + } + ``` + +- All `calldata` parameters on the `IStoreHook` interface were changed to `memory`, since the functions are called with `memory` from the `World`. + +- `IStore` exposes two new functions: `spliceStaticData` and `spliceDynamicData`. + + These functions provide lower level access to the operations happening under the hood in `setField`, `pushToField`, `popFromField` and `updateInField` and simplify handling + the new splice hooks. + + `StoreCore`'s internal logic was simplified to use the `spliceStaticData` and `spliceDynamicData` functions instead of duplicating similar logic in different functions. + + ```solidity + interface IStore { + // Splice data in the static part of the record + function spliceStaticData( + bytes32 tableId, + bytes32[] calldata keyTuple, + uint48 start, + uint40 deleteCount, + bytes calldata data + ) external; + + // Splice data in the dynamic part of the record + function spliceDynamicData( + bytes32 tableId, + bytes32[] calldata keyTuple, + uint8 dynamicFieldIndex, + uint40 startWithinField, + uint40 deleteCount, + bytes calldata data + ) external; + } + ``` diff --git a/packages/store/gas-report.json b/packages/store/gas-report.json index d224bfcb03..765b65cbf3 100644 --- a/packages/store/gas-report.json +++ b/packages/store/gas-report.json @@ -351,7 +351,7 @@ "file": "test/KeyEncoding.t.sol", "test": "testRegisterAndGetFieldLayout", "name": "register KeyEncoding table", - "gasUsed": 687791 + "gasUsed": 687778 }, { "file": "test/Mixed.t.sol", @@ -363,19 +363,19 @@ "file": "test/Mixed.t.sol", "test": "testRegisterAndGetFieldLayout", "name": "register Mixed table", - "gasUsed": 549609 + "gasUsed": 549596 }, { "file": "test/Mixed.t.sol", "test": "testSetAndGet", "name": "set record in Mixed", - "gasUsed": 103911 + "gasUsed": 103898 }, { "file": "test/Mixed.t.sol", "test": "testSetAndGet", "name": "get record from Mixed", - "gasUsed": 7028 + "gasUsed": 7015 }, { "file": "test/PackedCounter.t.sol", @@ -585,55 +585,55 @@ "file": "test/StoreCoreDynamic.t.sol", "test": "testGetSecondFieldLength", "name": "get field length (cold, 1 slot)", - "gasUsed": 7757 + "gasUsed": 7744 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testGetSecondFieldLength", "name": "get field length (warm, 1 slot)", - "gasUsed": 1752 + "gasUsed": 1739 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testGetThirdFieldLength", "name": "get field length (warm due to , 2 slots)", - "gasUsed": 7756 + "gasUsed": 7743 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testGetThirdFieldLength", "name": "get field length (warm, 2 slots)", - "gasUsed": 1752 + "gasUsed": 1739 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testPopFromSecondField", "name": "pop from field (cold, 1 slot, 1 uint32 item)", - "gasUsed": 20239 + "gasUsed": 19408 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testPopFromSecondField", "name": "pop from field (warm, 1 slot, 1 uint32 item)", - "gasUsed": 14250 + "gasUsed": 13418 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testPopFromThirdField", "name": "pop from field (cold, 2 slots, 10 uint32 items)", - "gasUsed": 22434 + "gasUsed": 17176 }, { "file": "test/StoreCoreDynamic.t.sol", "test": "testPopFromThirdField", "name": "pop from field (warm, 2 slots, 10 uint32 items)", - "gasUsed": 14446 + "gasUsed": 13187 }, { "file": "test/StoreCoreGas.t.sol", "test": "testAccessEmptyData", "name": "access non-existing record", - "gasUsed": 7054 + "gasUsed": 7041 }, { "file": "test/StoreCoreGas.t.sol", @@ -645,13 +645,13 @@ "file": "test/StoreCoreGas.t.sol", "test": "testAccessEmptyData", "name": "access dynamic field of non-existing record", - "gasUsed": 2046 + "gasUsed": 2033 }, { "file": "test/StoreCoreGas.t.sol", "test": "testAccessEmptyData", "name": "access length of dynamic field of non-existing record", - "gasUsed": 1121 + "gasUsed": 1108 }, { "file": "test/StoreCoreGas.t.sol", @@ -663,7 +663,7 @@ "file": "test/StoreCoreGas.t.sol", "test": "testDeleteData", "name": "delete record (complex data, 3 slots)", - "gasUsed": 6691 + "gasUsed": 6678 }, { "file": "test/StoreCoreGas.t.sol", @@ -681,67 +681,67 @@ "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "register subscriber", - "gasUsed": 60150 + "gasUsed": 58654 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "set record on table with subscriber", - "gasUsed": 71004 + "gasUsed": 71024 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "set static field on table with subscriber", - "gasUsed": 20780 + "gasUsed": 20506 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooks", "name": "delete record on table with subscriber", - "gasUsed": 16398 + "gasUsed": 16373 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "register subscriber", - "gasUsed": 60150 + "gasUsed": 58654 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "set (dynamic) record on table with subscriber", - "gasUsed": 164126 + "gasUsed": 164146 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "set (dynamic) field on table with subscriber", - "gasUsed": 23980 + "gasUsed": 24273 }, { "file": "test/StoreCoreGas.t.sol", "test": "testHooksDynamicData", "name": "delete (dynamic) record on table with subscriber", - "gasUsed": 17383 + "gasUsed": 17358 }, { "file": "test/StoreCoreGas.t.sol", "test": "testPushToField", "name": "push to field (1 slot, 1 uint32 item)", - "gasUsed": 11994 + "gasUsed": 10241 }, { "file": "test/StoreCoreGas.t.sol", "test": "testPushToField", "name": "push to field (2 slots, 10 uint32 items)", - "gasUsed": 34748 + "gasUsed": 32918 }, { "file": "test/StoreCoreGas.t.sol", "test": "testRegisterAndGetFieldLayout", "name": "StoreCore: register table", - "gasUsed": 609760 + "gasUsed": 609747 }, { "file": "test/StoreCoreGas.t.sol", @@ -765,13 +765,13 @@ "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetDynamicData", "name": "set complex record with dynamic data (4 slots)", - "gasUsed": 101831 + "gasUsed": 101818 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetDynamicData", "name": "get complex record with dynamic data (4 slots)", - "gasUsed": 4216 + "gasUsed": 4203 }, { "file": "test/StoreCoreGas.t.sol", @@ -807,7 +807,7 @@ "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "set static field (1 slot)", - "gasUsed": 31550 + "gasUsed": 31579 }, { "file": "test/StoreCoreGas.t.sol", @@ -819,7 +819,7 @@ "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "set static field (overlap 2 slot)", - "gasUsed": 30190 + "gasUsed": 30219 }, { "file": "test/StoreCoreGas.t.sol", @@ -831,31 +831,31 @@ "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "set dynamic field (1 slot, first dynamic field)", - "gasUsed": 53074 + "gasUsed": 53942 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "get dynamic field (1 slot, first dynamic field)", - "gasUsed": 2214 + "gasUsed": 2201 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "set dynamic field (1 slot, second dynamic field)", - "gasUsed": 31300 + "gasUsed": 32169 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetField", "name": "get dynamic field (1 slot, second dynamic field)", - "gasUsed": 2216 + "gasUsed": 2203 }, { "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetStaticData", "name": "set static record (1 slot)", - "gasUsed": 32137 + "gasUsed": 32124 }, { "file": "test/StoreCoreGas.t.sol", @@ -867,7 +867,7 @@ "file": "test/StoreCoreGas.t.sol", "test": "testSetAndGetStaticDataSpanningWords", "name": "set static record (2 slots)", - "gasUsed": 54641 + "gasUsed": 54628 }, { "file": "test/StoreCoreGas.t.sol", @@ -879,19 +879,19 @@ "file": "test/StoreCoreGas.t.sol", "test": "testUpdateInField", "name": "update in field (1 slot, 1 uint32 item)", - "gasUsed": 13032 + "gasUsed": 9585 }, { "file": "test/StoreCoreGas.t.sol", "test": "testUpdateInField", "name": "push to field (2 slots, 6 uint64 items)", - "gasUsed": 13834 + "gasUsed": 10022 }, { "file": "test/StoreHook.t.sol", "test": "testCallHook", "name": "call an enabled hook", - "gasUsed": 14588 + "gasUsed": 15038 }, { "file": "test/StoreHook.t.sol", @@ -927,97 +927,97 @@ "file": "test/tables/Callbacks.t.sol", "test": "testSetAndGet", "name": "Callbacks: set field", - "gasUsed": 56142 + "gasUsed": 57008 }, { "file": "test/tables/Callbacks.t.sol", "test": "testSetAndGet", "name": "Callbacks: get field (warm)", - "gasUsed": 2894 + "gasUsed": 2881 }, { "file": "test/tables/Callbacks.t.sol", "test": "testSetAndGet", "name": "Callbacks: push 1 element", - "gasUsed": 35052 + "gasUsed": 33292 }, { "file": "test/tables/StoreHooks.t.sol", "test": "testOneSlot", "name": "StoreHooks: set field with one elements (cold)", - "gasUsed": 58148 + "gasUsed": 59012 }, { "file": "test/tables/StoreHooks.t.sol", "test": "testTable", "name": "StoreHooks: set field (cold)", - "gasUsed": 58147 + "gasUsed": 59012 }, { "file": "test/tables/StoreHooks.t.sol", "test": "testTable", "name": "StoreHooks: get field (warm)", - "gasUsed": 2896 + "gasUsed": 2883 }, { "file": "test/tables/StoreHooks.t.sol", "test": "testTable", "name": "StoreHooks: push 1 element (cold)", - "gasUsed": 15147 + "gasUsed": 13389 }, { "file": "test/tables/StoreHooks.t.sol", "test": "testTable", "name": "StoreHooks: pop 1 element (warm)", - "gasUsed": 11702 + "gasUsed": 10686 }, { "file": "test/tables/StoreHooks.t.sol", "test": "testTable", "name": "StoreHooks: push 1 element (warm)", - "gasUsed": 13172 + "gasUsed": 11411 }, { "file": "test/tables/StoreHooks.t.sol", "test": "testTable", "name": "StoreHooks: update 1 element (warm)", - "gasUsed": 34269 + "gasUsed": 30626 }, { "file": "test/tables/StoreHooks.t.sol", "test": "testTable", "name": "StoreHooks: delete record (warm)", - "gasUsed": 7102 + "gasUsed": 7086 }, { "file": "test/tables/StoreHooks.t.sol", "test": "testTable", "name": "StoreHooks: set field (warm)", - "gasUsed": 30308 + "gasUsed": 31173 }, { "file": "test/tables/StoreHooks.t.sol", "test": "testThreeSlots", "name": "StoreHooks: set field with three elements (cold)", - "gasUsed": 80838 + "gasUsed": 81703 }, { "file": "test/tables/StoreHooks.t.sol", "test": "testTwoSlots", "name": "StoreHooks: set field with two elements (cold)", - "gasUsed": 80750 + "gasUsed": 81615 }, { "file": "test/tables/StoreHooksColdLoad.t.sol", "test": "testDelete", "name": "StoreHooks: delete record (cold)", - "gasUsed": 15967 + "gasUsed": 15954 }, { "file": "test/tables/StoreHooksColdLoad.t.sol", "test": "testGet", "name": "StoreHooks: get field (cold)", - "gasUsed": 8893 + "gasUsed": 8880 }, { "file": "test/tables/StoreHooksColdLoad.t.sol", @@ -1029,19 +1029,19 @@ "file": "test/tables/StoreHooksColdLoad.t.sol", "test": "testLength", "name": "StoreHooks: get length (cold)", - "gasUsed": 5853 + "gasUsed": 5840 }, { "file": "test/tables/StoreHooksColdLoad.t.sol", "test": "testPop", "name": "StoreHooks: pop 1 element (cold)", - "gasUsed": 22188 + "gasUsed": 19126 }, { "file": "test/tables/StoreHooksColdLoad.t.sol", "test": "testUpdate", "name": "StoreHooks: update 1 element (cold)", - "gasUsed": 24324 + "gasUsed": 21069 }, { "file": "test/tightcoder/DecodeSlice.t.sol", @@ -1095,13 +1095,13 @@ "file": "test/Vector2.t.sol", "test": "testRegisterAndGetFieldLayout", "name": "register Vector2 field layout", - "gasUsed": 411055 + "gasUsed": 411019 }, { "file": "test/Vector2.t.sol", "test": "testSetAndGet", "name": "set Vector2 record", - "gasUsed": 33040 + "gasUsed": 33027 }, { "file": "test/Vector2.t.sol", diff --git a/packages/store/src/IStore.sol b/packages/store/src/IStore.sol index d1709594f4..0e8546acaf 100644 --- a/packages/store/src/IStore.sol +++ b/packages/store/src/IStore.sol @@ -118,7 +118,26 @@ interface IStoreWrite { FieldLayout fieldLayout ) external; - // Set partial data at schema index + // Splice data in the static part of the record + function spliceStaticData( + bytes32 tableId, + bytes32[] calldata keyTuple, + uint48 start, + uint40 deleteCount, + bytes calldata data + ) external; + + // Splice data in the dynamic part of the record + function spliceDynamicData( + bytes32 tableId, + bytes32[] calldata keyTuple, + uint8 dynamicFieldIndex, + uint40 startWithinField, + uint40 deleteCount, + bytes calldata data + ) external; + + // Set partial data at field index function setField( bytes32 tableId, bytes32[] calldata keyTuple, @@ -127,7 +146,7 @@ interface IStoreWrite { FieldLayout fieldLayout ) external; - // Push encoded items to the dynamic field at schema index + // Push encoded items to the dynamic field at field index function pushToField( bytes32 tableId, bytes32[] calldata keyTuple, @@ -136,7 +155,7 @@ interface IStoreWrite { FieldLayout fieldLayout ) external; - // Pop byte length from the dynamic field at schema index + // Pop byte length from the dynamic field at field index function popFromField( bytes32 tableId, bytes32[] calldata keyTuple, @@ -145,7 +164,7 @@ interface IStoreWrite { FieldLayout fieldLayout ) external; - // Change encoded items within the dynamic field at schema index + // Change encoded items within the dynamic field at field index function updateInField( bytes32 tableId, bytes32[] calldata keyTuple, diff --git a/packages/store/src/IStoreErrors.sol b/packages/store/src/IStoreErrors.sol index e5214d19cd..5987a1d408 100644 --- a/packages/store/src/IStoreErrors.sol +++ b/packages/store/src/IStoreErrors.sol @@ -14,4 +14,5 @@ interface IStoreErrors { error StoreCore_InvalidFieldNamesLength(uint256 expected, uint256 received); error StoreCore_InvalidValueSchemaLength(uint256 expected, uint256 received); error StoreCore_DataIndexOverflow(uint256 length, uint256 received); + error StoreCore_InvalidSplice(uint40 startWithinField, uint40 deleteCount, uint40 fieldLength); } diff --git a/packages/store/src/IStoreHook.sol b/packages/store/src/IStoreHook.sol index c3132fcea7..a2be6d40f0 100644 --- a/packages/store/src/IStoreHook.sol +++ b/packages/store/src/IStoreHook.sol @@ -8,45 +8,69 @@ import { PackedCounter } from "./PackedCounter.sol"; // ERC-165 Interface ID (see https://eips.ethereum.org/EIPS/eip-165) bytes4 constant STORE_HOOK_INTERFACE_ID = IStoreHook.onBeforeSetRecord.selector ^ IStoreHook.onAfterSetRecord.selector ^ - IStoreHook.onBeforeSetField.selector ^ - IStoreHook.onAfterSetField.selector ^ + IStoreHook.onBeforeSpliceStaticData.selector ^ + IStoreHook.onAfterSpliceStaticData.selector ^ + IStoreHook.onBeforeSpliceDynamicData.selector ^ + IStoreHook.onAfterSpliceDynamicData.selector ^ IStoreHook.onBeforeDeleteRecord.selector ^ IStoreHook.onAfterDeleteRecord.selector ^ ERC165_INTERFACE_ID; interface IStoreHook is IERC165 { + error StoreHook_NotImplemented(); + function onBeforeSetRecord( bytes32 tableId, bytes32[] memory keyTuple, - bytes calldata staticData, + bytes memory staticData, PackedCounter encodedLengths, - bytes calldata dynamicData, + bytes memory dynamicData, FieldLayout fieldLayout ) external; function onAfterSetRecord( bytes32 tableId, bytes32[] memory keyTuple, - bytes calldata staticData, + bytes memory staticData, PackedCounter encodedLengths, - bytes calldata dynamicData, + bytes memory dynamicData, FieldLayout fieldLayout ) external; - function onBeforeSetField( + function onBeforeSpliceStaticData( + bytes32 tableId, + bytes32[] memory keyTuple, + uint48 start, + uint40 deleteCount, + bytes memory data + ) external; + + function onAfterSpliceStaticData( bytes32 tableId, bytes32[] memory keyTuple, - uint8 fieldIndex, + uint48 start, + uint40 deleteCount, + bytes memory data + ) external; + + function onBeforeSpliceDynamicData( + bytes32 tableId, + bytes32[] memory keyTuple, + uint8 dynamicFieldIndex, + uint40 startWithinField, + uint40 deleteCount, bytes memory data, - FieldLayout fieldLayout + PackedCounter encodedLengths ) external; - function onAfterSetField( + function onAfterSpliceDynamicData( bytes32 tableId, bytes32[] memory keyTuple, - uint8 fieldIndex, + uint8 dynamicFieldIndex, + uint40 startWithinField, + uint40 deleteCount, bytes memory data, - FieldLayout fieldLayout + PackedCounter encodedLengths ) external; function onBeforeDeleteRecord(bytes32 tableId, bytes32[] memory keyTuple, FieldLayout fieldLayout) external; diff --git a/packages/store/src/StoreCore.sol b/packages/store/src/StoreCore.sol index 1f107f4789..7fae2a9f36 100644 --- a/packages/store/src/StoreCore.sol +++ b/packages/store/src/StoreCore.sol @@ -14,8 +14,13 @@ import { IStoreErrors } from "./IStoreErrors.sol"; import { IStoreHook } from "./IStoreHook.sol"; import { StoreSwitch } from "./StoreSwitch.sol"; import { Hook, HookLib } from "./Hook.sol"; -import { BEFORE_SET_RECORD, AFTER_SET_RECORD, BEFORE_SET_FIELD, AFTER_SET_FIELD, BEFORE_DELETE_RECORD, AFTER_DELETE_RECORD } from "./storeHookTypes.sol"; +import { BEFORE_SET_RECORD, AFTER_SET_RECORD, BEFORE_SPLICE_STATIC_DATA, AFTER_SPLICE_STATIC_DATA, BEFORE_SPLICE_DYNAMIC_DATA, AFTER_SPLICE_DYNAMIC_DATA, BEFORE_DELETE_RECORD, AFTER_DELETE_RECORD } from "./storeHookTypes.sol"; +/** + * StoreCore includes implementations for all IStore methods. + * StoreCoreInternal includes helper methods used by multiple StoreCore methods. + * It's split into a separate library to make it clear that it's not intended to be outside StoreCore. + */ library StoreCore { event HelloStore(bytes32 indexed version); event StoreSetRecord( @@ -193,7 +198,7 @@ library StoreCore { ************************************************************************/ /** - * Set full data record for the given tableId and key tuple and field layout + * Set full data record for the given table ID and key tuple and field layout */ function setRecord( bytes32 tableId, @@ -282,38 +287,128 @@ library StoreCore { } } - /** - * Set data for a field in a table with the given tableId, key tuple and value field layout - */ - function setField( + function spliceStaticData( bytes32 tableId, bytes32[] memory keyTuple, - uint8 fieldIndex, - bytes memory data, - FieldLayout fieldLayout + uint48 start, + uint40 deleteCount, + bytes memory data ) internal { - // Call onBeforeSetField hooks (before modifying the state) + uint256 location = StoreCoreInternal._getStaticDataLocation(tableId, keyTuple); + + // Call onBeforeSpliceStaticData hooks (before actually modifying the state, so observers have access to the previous state if needed) bytes21[] memory hooks = StoreHooks._get(tableId); for (uint256 i; i < hooks.length; i++) { Hook hook = Hook.wrap(hooks[i]); - if (hook.isEnabled(BEFORE_SET_FIELD)) { - IStoreHook(hook.getAddress()).onBeforeSetField(tableId, keyTuple, fieldIndex, data, fieldLayout); + if (hook.isEnabled(BEFORE_SPLICE_STATIC_DATA)) { + IStoreHook(hook.getAddress()).onBeforeSpliceStaticData({ + tableId: tableId, + keyTuple: keyTuple, + start: start, + deleteCount: deleteCount, + data: data + }); } } - if (fieldIndex < fieldLayout.numStaticFields()) { - StoreCoreInternal._setStaticField(tableId, keyTuple, fieldLayout, fieldIndex, data); - } else { - StoreCoreInternal._setDynamicField(tableId, keyTuple, fieldLayout, fieldIndex, data); - } + // Store the provided value in storage + Storage.store({ storagePointer: location, offset: start, data: data }); - // Call onAfterSetField hooks (after modifying the state) + // Call onAfterSpliceStaticData hooks for (uint256 i; i < hooks.length; i++) { Hook hook = Hook.wrap(hooks[i]); - if (hook.isEnabled(AFTER_SET_FIELD)) { - IStoreHook(hook.getAddress()).onAfterSetField(tableId, keyTuple, fieldIndex, data, fieldLayout); + if (hook.isEnabled(AFTER_SPLICE_STATIC_DATA)) { + IStoreHook(hook.getAddress()).onAfterSpliceStaticData({ + tableId: tableId, + keyTuple: keyTuple, + start: start, + deleteCount: deleteCount, + data: data + }); } } + + // Emit event to notify offchain indexers + emit StoreCore.StoreSpliceStaticData({ + tableId: tableId, + keyTuple: keyTuple, + start: start, + deleteCount: deleteCount, + data: data + }); + } + + function spliceDynamicData( + bytes32 tableId, + bytes32[] memory keyTuple, + uint8 dynamicFieldIndex, // need this to compute the dynamic data location + uint40 startWithinField, + uint40 deleteCount, + bytes memory data + ) internal { + StoreCoreInternal._spliceDynamicData({ + tableId: tableId, + keyTuple: keyTuple, + dynamicFieldIndex: dynamicFieldIndex, + startWithinField: startWithinField, + deleteCount: deleteCount, + data: data, + previousEncodedLengths: StoreCoreInternal._loadEncodedDynamicDataLength(tableId, keyTuple) + }); + } + + /** + * Set data for a field in a table with the given tableId, key tuple and value field layout + */ + function setField( + bytes32 tableId, + bytes32[] memory keyTuple, + uint8 fieldIndex, + bytes memory data, + FieldLayout fieldLayout + ) internal { + if (fieldIndex < fieldLayout.numStaticFields()) { + setStaticField(tableId, keyTuple, fieldLayout, fieldIndex, data); + } else { + setDynamicField(tableId, keyTuple, fieldIndex - uint8(fieldLayout.numStaticFields()), data); + } + } + + function setStaticField( + bytes32 tableId, + bytes32[] memory keyTuple, + FieldLayout fieldLayout, + uint8 fieldIndex, + bytes memory data + ) internal { + spliceStaticData({ + tableId: tableId, + keyTuple: keyTuple, + start: uint48(StoreCoreInternal._getStaticDataOffset(fieldLayout, fieldIndex)), + deleteCount: uint40(fieldLayout.atIndex(fieldIndex)), + data: data + }); + } + + function setDynamicField( + bytes32 tableId, + bytes32[] memory keyTuple, + uint8 dynamicFieldIndex, + bytes memory data + ) internal { + // Load the previous length of the field to set from storage to compute how much data to delete + PackedCounter previousEncodedLengths = StoreCoreInternal._loadEncodedDynamicDataLength(tableId, keyTuple); + uint40 previousFieldLength = uint40(previousEncodedLengths.atIndex(dynamicFieldIndex)); + + StoreCoreInternal._spliceDynamicData({ + tableId: tableId, + keyTuple: keyTuple, + dynamicFieldIndex: dynamicFieldIndex, + startWithinField: 0, + deleteCount: previousFieldLength, + data: data, + previousEncodedLengths: previousEncodedLengths + }); } /** @@ -365,30 +460,7 @@ library StoreCore { revert IStoreErrors.StoreCore_NotDynamicField(); } - // TODO add push-specific hook to avoid the storage read? (https://github.com/latticexyz/mud/issues/444) - bytes memory fullData = abi.encodePacked( - getDynamicField(tableId, keyTuple, fieldIndex - uint8(fieldLayout.numStaticFields())), - dataToPush - ); - - // Call onBeforeSetField hooks (before modifying the state) - bytes21[] memory hooks = StoreHooks._get(tableId); - for (uint256 i; i < hooks.length; i++) { - Hook hook = Hook.wrap(hooks[i]); - if (hook.isEnabled(BEFORE_SET_FIELD)) { - IStoreHook(hook.getAddress()).onBeforeSetField(tableId, keyTuple, fieldIndex, fullData, fieldLayout); - } - } - StoreCoreInternal._pushToDynamicField(tableId, keyTuple, fieldLayout, fieldIndex, dataToPush); - - // Call onAfterSetField hooks (after modifying the state) - for (uint256 i; i < hooks.length; i++) { - Hook hook = Hook.wrap(hooks[i]); - if (hook.isEnabled(AFTER_SET_FIELD)) { - IStoreHook(hook.getAddress()).onAfterSetField(tableId, keyTuple, fieldIndex, fullData, fieldLayout); - } - } } /** @@ -405,31 +477,7 @@ library StoreCore { revert IStoreErrors.StoreCore_NotDynamicField(); } - // TODO add pop-specific hook to avoid the storage read? (https://github.com/latticexyz/mud/issues/444) - bytes memory fullData; - { - bytes memory oldData = getDynamicField(tableId, keyTuple, fieldIndex - uint8(fieldLayout.numStaticFields())); - fullData = SliceLib.getSubslice(oldData, 0, oldData.length - byteLengthToPop).toBytes(); - } - - // Call onBeforeSetField hooks (before modifying the state) - bytes21[] memory hooks = StoreHooks._get(tableId); - for (uint256 i; i < hooks.length; i++) { - Hook hook = Hook.wrap(hooks[i]); - if (hook.isEnabled(BEFORE_SET_FIELD)) { - IStoreHook(hook.getAddress()).onBeforeSetField(tableId, keyTuple, fieldIndex, fullData, fieldLayout); - } - } - StoreCoreInternal._popFromDynamicField(tableId, keyTuple, fieldLayout, fieldIndex, byteLengthToPop); - - // Call onAfterSetField hooks (after modifying the state) - for (uint256 i; i < hooks.length; i++) { - Hook hook = Hook.wrap(hooks[i]); - if (hook.isEnabled(AFTER_SET_FIELD)) { - IStoreHook(hook.getAddress()).onAfterSetField(tableId, keyTuple, fieldIndex, fullData, fieldLayout); - } - } } /** @@ -453,35 +501,7 @@ library StoreCore { revert IStoreErrors.StoreCore_DataIndexOverflow(type(uint40).max, startByteIndex); } - // TODO add setItem-specific hook to avoid the storage read? (https://github.com/latticexyz/mud/issues/444) - bytes memory fullData; - { - bytes memory oldData = getDynamicField(tableId, keyTuple, fieldIndex - uint8(fieldLayout.numStaticFields())); - fullData = abi.encodePacked( - SliceLib.getSubslice(oldData, 0, startByteIndex).toBytes(), - dataToSet, - SliceLib.getSubslice(oldData, startByteIndex + dataToSet.length, oldData.length).toBytes() - ); - } - - // Call onBeforeSetField hooks (before modifying the state) - bytes21[] memory hooks = StoreHooks._get(tableId); - for (uint256 i; i < hooks.length; i++) { - Hook hook = Hook.wrap(hooks[i]); - if (hook.isEnabled(BEFORE_SET_FIELD)) { - IStoreHook(hook.getAddress()).onBeforeSetField(tableId, keyTuple, fieldIndex, fullData, fieldLayout); - } - } - StoreCoreInternal._setDynamicFieldItem(tableId, keyTuple, fieldLayout, fieldIndex, startByteIndex, dataToSet); - - // Call onAfterSetField hooks (after modifying the state) - for (uint256 i; i < hooks.length; i++) { - Hook hook = Hook.wrap(hooks[i]); - if (hook.isEnabled(AFTER_SET_FIELD)) { - IStoreHook(hook.getAddress()).onAfterSetField(tableId, keyTuple, fieldIndex, fullData, fieldLayout); - } - } } /************************************************************************ @@ -491,7 +511,7 @@ library StoreCore { ************************************************************************/ /** - * Emit the ephemeral event without modifying storage for the full data of the given tableId and key tuple + * Emit the ephemeral event without modifying storage for the full data of the given table ID and key tuple */ function emitEphemeralRecord( bytes32 tableId, @@ -515,7 +535,7 @@ library StoreCore { ************************************************************************/ /** - * Get full record (all fields, static and dynamic data) for the given tableId and key tuple, with the given value field layout + * Get full record (all fields, static and dynamic data) for the given table ID and key tuple, with the given value field layout */ function getRecord( bytes32 tableId, @@ -549,7 +569,7 @@ library StoreCore { } /** - * Get a single field from the given tableId and key tuple, with the given value field layout + * Get a single field from the given table ID and key tuple, with the given value field layout */ function getField( bytes32 tableId, @@ -565,7 +585,7 @@ library StoreCore { } /** - * Get a single static field from the given tableId and key tuple, with the given value field layout. + * Get a single static field from the given table ID and key tuple, with the given value field layout. * Note: the field value is left-aligned in the returned bytes32, the rest of the word is not zeroed out. * Consumers are expected to truncate the returned value as needed. */ @@ -586,7 +606,7 @@ library StoreCore { } /** - * Get a single dynamic field from the given tableId and key tuple, with the given value field layout + * Get a single dynamic field from the given table ID and key tuple, with the given value field layout */ function getDynamicField( bytes32 tableId, @@ -604,7 +624,7 @@ library StoreCore { } /** - * Get the byte length of a single field from the given tableId and key tuple, with the given value field layout + * Get the byte length of a single field from the given table ID and key tuple, with the given value field layout */ function getFieldLength( bytes32 tableId, @@ -623,7 +643,7 @@ library StoreCore { } /** - * Get a byte slice (including start, excluding end) of a single dynamic field from the given tableId and key tuple, with the given value field layout. + * Get a byte slice (including start, excluding end) of a single dynamic field from the given table ID and key tuple, with the given value field layout. * The slice is unchecked and will return invalid data if `start`:`end` overflow. */ function getFieldSlice( @@ -640,8 +660,8 @@ library StoreCore { } // Get the length and storage location of the dynamic field - uint8 dynamicSchemaIndex = fieldIndex - numStaticFields; - uint256 location = StoreCoreInternal._getDynamicDataLocation(tableId, keyTuple, dynamicSchemaIndex); + uint8 dynamicFieldIndex = fieldIndex - numStaticFields; + uint256 location = StoreCoreInternal._getDynamicDataLocation(tableId, keyTuple, dynamicFieldIndex); return Storage.load({ storagePointer: location, length: end - start, offset: start }); } @@ -658,69 +678,89 @@ library StoreCoreInternal { * ************************************************************************/ - function _setStaticField( + function _spliceDynamicData( bytes32 tableId, bytes32[] memory keyTuple, - FieldLayout fieldLayout, - uint8 fieldIndex, - bytes memory data + uint8 dynamicFieldIndex, + uint40 startWithinField, + uint40 deleteCount, + bytes memory data, + PackedCounter previousEncodedLengths ) internal { - uint256 location = _getStaticDataLocation(tableId, keyTuple); - uint256 offset = _getStaticDataOffset(fieldLayout, fieldIndex); - - Storage.store({ storagePointer: location, offset: offset, data: data }); + uint256 previousFieldLength = previousEncodedLengths.atIndex(dynamicFieldIndex); + uint256 updatedFieldLength = previousFieldLength - deleteCount + data.length; - // Emit event to notify indexers - emit StoreCore.StoreSpliceStaticData({ - tableId: tableId, - keyTuple: keyTuple, - start: uint48(offset), - deleteCount: uint40(fieldLayout.atIndex(fieldIndex)), - data: data - }); - } - - function _setDynamicField( - bytes32 tableId, - bytes32[] memory keyTuple, - FieldLayout fieldLayout, - uint8 fieldIndex, - bytes memory data - ) internal { - uint8 dynamicSchemaIndex = fieldIndex - uint8(fieldLayout.numStaticFields()); + // If the total length of the field is changed, the data has to be appended/removed at the end of the field. + // Otherwise offchain indexers would shift the data after inserted data, while onchain the data is truncated at the end. + if (previousFieldLength != updatedFieldLength && startWithinField + deleteCount != previousFieldLength) { + revert IStoreErrors.StoreCore_InvalidSplice(startWithinField, deleteCount, uint40(previousFieldLength)); + } - // Load dynamic data length from storage - uint256 dynamicSchemaLengthSlot = _getDynamicDataLengthLocation(tableId, keyTuple); - PackedCounter encodedLengths = PackedCounter.wrap(Storage.load({ storagePointer: dynamicSchemaLengthSlot })); + // Compute start index for the splice + uint256 start = startWithinField; + unchecked { + // (safe because it's a few uint40 values, which can't overflow uint48) + for (uint8 i; i < dynamicFieldIndex; i++) { + start += previousEncodedLengths.atIndex(i); + } + } // Update the encoded length - uint256 oldFieldLength = encodedLengths.atIndex(dynamicSchemaIndex); - encodedLengths = encodedLengths.setAtIndex(dynamicSchemaIndex, data.length); + PackedCounter updatedEncodedLengths = previousEncodedLengths.setAtIndex(dynamicFieldIndex, updatedFieldLength); - // Set the new lengths - Storage.store({ storagePointer: dynamicSchemaLengthSlot, data: encodedLengths.unwrap() }); + // Call onBeforeSpliceDynamicData hooks (before actually modifying the state, so observers have access to the previous state if needed) + bytes21[] memory hooks = StoreHooks._get(tableId); + for (uint256 i; i < hooks.length; i++) { + Hook hook = Hook.wrap(hooks[i]); + if (hook.isEnabled(BEFORE_SPLICE_DYNAMIC_DATA)) { + IStoreHook(hook.getAddress()).onBeforeSpliceDynamicData({ + tableId: tableId, + keyTuple: keyTuple, + dynamicFieldIndex: dynamicFieldIndex, + startWithinField: startWithinField, + deleteCount: deleteCount, + data: data, + encodedLengths: updatedEncodedLengths + }); + } + } + + // Store the updated encoded lengths in storage + if (previousFieldLength != updatedFieldLength) { + uint256 dynamicSchemaLengthSlot = _getDynamicDataLengthLocation(tableId, keyTuple); + Storage.store({ storagePointer: dynamicSchemaLengthSlot, data: updatedEncodedLengths.unwrap() }); + } // Store the provided value in storage - uint256 dynamicDataLocation = _getDynamicDataLocation(tableId, keyTuple, dynamicSchemaIndex); - Storage.store({ storagePointer: dynamicDataLocation, offset: 0, data: data }); + { + uint256 dynamicDataLocation = _getDynamicDataLocation(tableId, keyTuple, dynamicFieldIndex); + Storage.store({ storagePointer: dynamicDataLocation, offset: startWithinField, data: data }); + } - // Compute start index for the splice event - uint256 start; - unchecked { - // (safe because it's a few uint40 values, which can't overflow uint48) - for (uint8 i; i < dynamicSchemaIndex; i++) { - start += encodedLengths.atIndex(i); + // Call onAfterSpliceDynamicData hooks + for (uint256 i; i < hooks.length; i++) { + Hook hook = Hook.wrap(hooks[i]); + if (hook.isEnabled(AFTER_SPLICE_DYNAMIC_DATA)) { + IStoreHook(hook.getAddress()).onAfterSpliceDynamicData({ + tableId: tableId, + keyTuple: keyTuple, + dynamicFieldIndex: dynamicFieldIndex, + startWithinField: startWithinField, + deleteCount: deleteCount, + data: data, + encodedLengths: updatedEncodedLengths + }); } } - // Emit event to notify indexers + // Emit event to notify offchain indexers emit StoreCore.StoreSpliceDynamicData({ tableId: tableId, keyTuple: keyTuple, start: uint48(start), - deleteCount: uint40(oldFieldLength), + deleteCount: deleteCount, data: data, - encodedLengths: encodedLengths.unwrap() + encodedLengths: updatedEncodedLengths.unwrap() }); } @@ -731,39 +771,21 @@ library StoreCoreInternal { uint8 fieldIndex, bytes memory dataToPush ) internal { - uint8 dynamicSchemaIndex = fieldIndex - uint8(fieldLayout.numStaticFields()); - - // Load dynamic data length from storage - uint256 dynamicDataLengthSlot = _getDynamicDataLengthLocation(tableId, keyTuple); - PackedCounter encodedLengths = PackedCounter.wrap(Storage.load({ storagePointer: dynamicDataLengthSlot })); - - // Update the encoded length - uint256 oldFieldLength = encodedLengths.atIndex(dynamicSchemaIndex); - encodedLengths = encodedLengths.setAtIndex(dynamicSchemaIndex, oldFieldLength + dataToPush.length); + uint8 dynamicFieldIndex = fieldIndex - uint8(fieldLayout.numStaticFields()); - // Set the new length - Storage.store({ storagePointer: dynamicDataLengthSlot, data: encodedLengths.unwrap() }); + // Load the previous length of the field to set from storage to compute where to start to push + PackedCounter previousEncodedLengths = _loadEncodedDynamicDataLength(tableId, keyTuple); + uint40 previousFieldLength = uint40(previousEncodedLengths.atIndex(dynamicFieldIndex)); - // Append `dataToPush` to the end of the data in storage - _setPartialDynamicData(tableId, keyTuple, dynamicSchemaIndex, oldFieldLength, dataToPush); - - // Compute start index for the splice event - uint256 start = oldFieldLength; - unchecked { - // (safe because it's a few uint40 values, which can't overflow uint48) - for (uint8 i; i < dynamicSchemaIndex; i++) { - start += encodedLengths.atIndex(i); - } - } - - // Emit event to notify indexers - emit StoreCore.StoreSpliceDynamicData({ + // Splice the dynamic data + _spliceDynamicData({ tableId: tableId, keyTuple: keyTuple, - start: uint48(start), - deleteCount: uint40(0), + dynamicFieldIndex: dynamicFieldIndex, + startWithinField: uint40(previousFieldLength), + deleteCount: 0, data: dataToPush, - encodedLengths: encodedLengths.unwrap() + previousEncodedLengths: previousEncodedLengths }); } @@ -774,44 +796,24 @@ library StoreCoreInternal { uint8 fieldIndex, uint256 byteLengthToPop ) internal { - uint8 dynamicSchemaIndex = fieldIndex - uint8(fieldLayout.numStaticFields()); - - // Load dynamic data length from storage - uint256 dynamicDataLengthSlot = _getDynamicDataLengthLocation(tableId, keyTuple); - PackedCounter encodedLengths = PackedCounter.wrap(Storage.load({ storagePointer: dynamicDataLengthSlot })); - - // Update the encoded length - uint256 oldFieldLength = encodedLengths.atIndex(dynamicSchemaIndex); - encodedLengths = encodedLengths.setAtIndex(dynamicSchemaIndex, oldFieldLength - byteLengthToPop); - - // Set the new length - Storage.store({ storagePointer: dynamicDataLengthSlot, data: encodedLengths.unwrap() }); + uint8 dynamicFieldIndex = fieldIndex - uint8(fieldLayout.numStaticFields()); - // Data can be left unchanged, push/set do not assume storage to be 0s + // Load the previous length of the field to set from storage to compute where to start to push + PackedCounter previousEncodedLengths = _loadEncodedDynamicDataLength(tableId, keyTuple); + uint40 previousFieldLength = uint40(previousEncodedLengths.atIndex(dynamicFieldIndex)); - // Compute start index for the splice event - uint256 start; - unchecked { - // (safe because it's a few uint40 values, which can't overflow uint48) - start = oldFieldLength; - for (uint8 i; i < dynamicSchemaIndex; i++) { - start += encodedLengths.atIndex(i); - } - start -= byteLengthToPop; - } - - // Emit event to notify indexers - emit StoreCore.StoreSpliceDynamicData({ + // Splice the dynamic data + _spliceDynamicData({ tableId: tableId, keyTuple: keyTuple, - start: uint48(start), + dynamicFieldIndex: dynamicFieldIndex, + startWithinField: uint40(previousFieldLength - byteLengthToPop), deleteCount: uint40(byteLengthToPop), data: new bytes(0), - encodedLengths: encodedLengths.unwrap() + previousEncodedLengths: previousEncodedLengths }); } - // startOffset is measured in bytes function _setDynamicFieldItem( bytes32 tableId, bytes32[] memory keyTuple, @@ -820,33 +822,14 @@ library StoreCoreInternal { uint256 startByteIndex, bytes memory dataToSet ) internal { - uint8 dynamicSchemaIndex = fieldIndex - uint8(fieldLayout.numStaticFields()); - - // Load dynamic data length from storage - uint256 dynamicSchemaLengthSlot = _getDynamicDataLengthLocation(tableId, keyTuple); - PackedCounter encodedLengths = PackedCounter.wrap(Storage.load({ storagePointer: dynamicSchemaLengthSlot })); - - // Set `dataToSet` at the given index - _setPartialDynamicData(tableId, keyTuple, dynamicSchemaIndex, startByteIndex, dataToSet); - - // Compute start index for the splice event - uint256 start; - unchecked { - // (safe because it's a few uint40 values, which can't overflow uint48) - start = startByteIndex; - for (uint8 i; i < dynamicSchemaIndex; i++) { - start += encodedLengths.atIndex(i); - } - } - - // Emit event to notify indexers - emit StoreCore.StoreSpliceDynamicData({ + _spliceDynamicData({ tableId: tableId, keyTuple: keyTuple, - start: uint48(start), + dynamicFieldIndex: fieldIndex - uint8(fieldLayout.numStaticFields()), + startWithinField: uint40(startByteIndex), deleteCount: uint40(dataToSet.length), data: dataToSet, - encodedLengths: encodedLengths.unwrap() + previousEncodedLengths: _loadEncodedDynamicDataLength(tableId, keyTuple) }); } @@ -857,7 +840,7 @@ library StoreCoreInternal { ************************************************************************/ /** - * Get full static data for the given tableId and key tuple, with the given static length + * Get full static data for the given table ID and key tuple, with the given static length */ function _getStaticData( bytes32 tableId, @@ -872,7 +855,7 @@ library StoreCoreInternal { } /** - * Get a single static field from the given tableId and key tuple, with the given value field layout. + * Get a single static field from the given table ID and key tuple, with the given value field layout. * Returns dynamic bytes memory in the size of the field. */ function _getStaticFieldBytes( @@ -960,53 +943,13 @@ library StoreCoreInternal { } /** - * Get the length of the dynamic data for the given value field layout and index + * Load the encoded dynamic data length from storage for the given table ID and key tuple */ function _loadEncodedDynamicDataLength( bytes32 tableId, bytes32[] memory keyTuple ) internal view returns (PackedCounter) { // Load dynamic data length from storage - uint256 dynamicDataLengthSlot = _getDynamicDataLengthLocation(tableId, keyTuple); - return PackedCounter.wrap(Storage.load({ storagePointer: dynamicDataLengthSlot })); - } - - /** - * Set the length of the dynamic data (in bytes) for the given value field layout and index - */ - function _setDynamicDataLengthAtIndex( - bytes32 tableId, - bytes32[] memory keyTuple, - uint8 dynamicSchemaIndex, // fieldIndex - numStaticFields - uint256 newLengthAtIndex - ) internal { - // Load dynamic data length from storage - uint256 dynamicDataLengthSlot = _getDynamicDataLengthLocation(tableId, keyTuple); - PackedCounter encodedLengths = PackedCounter.wrap(Storage.load({ storagePointer: dynamicDataLengthSlot })); - - // Update the encoded lengths - encodedLengths = encodedLengths.setAtIndex(dynamicSchemaIndex, newLengthAtIndex); - - // Set the new lengths - Storage.store({ storagePointer: dynamicDataLengthSlot, data: encodedLengths.unwrap() }); - } - - /** - * Modify a part of the dynamic field's data (without changing the field's length) - */ - function _setPartialDynamicData( - bytes32 tableId, - bytes32[] memory keyTuple, - uint8 dynamicSchemaIndex, - uint256 startByteIndex, - bytes memory partialData - ) internal { - uint256 dynamicDataLocation = _getDynamicDataLocation(tableId, keyTuple, dynamicSchemaIndex); - // start index is in bytes, whereas storage slots are in 32-byte words - dynamicDataLocation += startByteIndex / 32; - - // partial storage slot offset (there is no inherent offset, as each dynamic field starts at its own storage slot) - uint256 offset = startByteIndex % 32; - Storage.store({ storagePointer: dynamicDataLocation, offset: offset, data: partialData }); + return PackedCounter.wrap(Storage.load({ storagePointer: _getDynamicDataLengthLocation(tableId, keyTuple) })); } } diff --git a/packages/store/src/StoreHook.sol b/packages/store/src/StoreHook.sol index 0fff2ce06e..ca854c06fa 100644 --- a/packages/store/src/StoreHook.sol +++ b/packages/store/src/StoreHook.sol @@ -3,10 +3,74 @@ pragma solidity >=0.8.0; import { IStoreHook, STORE_HOOK_INTERFACE_ID } from "./IStoreHook.sol"; import { ERC165_INTERFACE_ID } from "./IERC165.sol"; +import { PackedCounter } from "./PackedCounter.sol"; +import { FieldLayout } from "./FieldLayout.sol"; abstract contract StoreHook is IStoreHook { // ERC-165 supportsInterface (see https://eips.ethereum.org/EIPS/eip-165) function supportsInterface(bytes4 interfaceId) public pure virtual returns (bool) { return interfaceId == STORE_HOOK_INTERFACE_ID || interfaceId == ERC165_INTERFACE_ID; } + + function onBeforeSetRecord( + bytes32, + bytes32[] memory, + bytes memory, + PackedCounter, + bytes memory, + FieldLayout + ) public virtual { + revert StoreHook_NotImplemented(); + } + + function onAfterSetRecord( + bytes32, + bytes32[] memory, + bytes memory, + PackedCounter, + bytes memory, + FieldLayout + ) public virtual { + revert StoreHook_NotImplemented(); + } + + function onBeforeSpliceStaticData(bytes32, bytes32[] memory, uint48, uint40, bytes memory) public virtual { + revert StoreHook_NotImplemented(); + } + + function onAfterSpliceStaticData(bytes32, bytes32[] memory, uint48, uint40, bytes memory) public virtual { + revert StoreHook_NotImplemented(); + } + + function onBeforeSpliceDynamicData( + bytes32, + bytes32[] memory, + uint8, + uint40, + uint40, + bytes memory, + PackedCounter + ) public virtual { + revert StoreHook_NotImplemented(); + } + + function onAfterSpliceDynamicData( + bytes32, + bytes32[] memory, + uint8, + uint40, + uint40, + bytes memory, + PackedCounter + ) public virtual { + revert StoreHook_NotImplemented(); + } + + function onBeforeDeleteRecord(bytes32, bytes32[] memory, FieldLayout) public virtual { + revert StoreHook_NotImplemented(); + } + + function onAfterDeleteRecord(bytes32, bytes32[] memory, FieldLayout) public virtual { + revert StoreHook_NotImplemented(); + } } diff --git a/packages/store/src/StoreSwitch.sol b/packages/store/src/StoreSwitch.sol index 2206a5e6ff..396b246010 100644 --- a/packages/store/src/StoreSwitch.sol +++ b/packages/store/src/StoreSwitch.sol @@ -125,6 +125,44 @@ library StoreSwitch { } } + function spliceStaticData( + bytes32 tableId, + bytes32[] memory keyTuple, + uint48 start, + uint40 deleteCount, + bytes memory data + ) internal { + address _storeAddress = getStoreAddress(); + if (_storeAddress == address(this)) { + StoreCore.spliceStaticData(tableId, keyTuple, start, deleteCount, data); + } else { + IStore(_storeAddress).spliceStaticData(tableId, keyTuple, start, deleteCount, data); + } + } + + function spliceDynamicData( + bytes32 tableId, + bytes32[] memory keyTuple, + uint8 dynamicFieldIndex, + uint40 startWithinField, + uint40 deleteCount, + bytes memory data + ) internal { + address _storeAddress = getStoreAddress(); + if (_storeAddress == address(this)) { + StoreCore.spliceDynamicData(tableId, keyTuple, dynamicFieldIndex, startWithinField, deleteCount, data); + } else { + IStore(_storeAddress).spliceDynamicData( + tableId, + keyTuple, + dynamicFieldIndex, + startWithinField, + deleteCount, + data + ); + } + } + function setField( bytes32 tableId, bytes32[] memory keyTuple, diff --git a/packages/store/src/storeHookTypes.sol b/packages/store/src/storeHookTypes.sol index e6a9f00718..d6d424591a 100644 --- a/packages/store/src/storeHookTypes.sol +++ b/packages/store/src/storeHookTypes.sol @@ -3,8 +3,28 @@ pragma solidity >=0.8.0; uint8 constant BEFORE_SET_RECORD = 1 << 0; uint8 constant AFTER_SET_RECORD = 1 << 1; -// TODO: do we need to differentiate between static and dynamic set field? -uint8 constant BEFORE_SET_FIELD = 1 << 2; -uint8 constant AFTER_SET_FIELD = 1 << 3; -uint8 constant BEFORE_DELETE_RECORD = 1 << 4; -uint8 constant AFTER_DELETE_RECORD = 1 << 5; +uint8 constant BEFORE_SPLICE_STATIC_DATA = 1 << 2; +uint8 constant AFTER_SPLICE_STATIC_DATA = 1 << 3; +uint8 constant BEFORE_SPLICE_DYNAMIC_DATA = 1 << 4; +uint8 constant AFTER_SPLICE_DYNAMIC_DATA = 1 << 5; +uint8 constant BEFORE_DELETE_RECORD = 1 << 6; +uint8 constant AFTER_DELETE_RECORD = 1 << 7; + +uint8 constant ALL = BEFORE_SET_RECORD | + AFTER_SET_RECORD | + BEFORE_SPLICE_STATIC_DATA | + AFTER_SPLICE_STATIC_DATA | + BEFORE_SPLICE_DYNAMIC_DATA | + AFTER_SPLICE_DYNAMIC_DATA | + BEFORE_DELETE_RECORD | + AFTER_DELETE_RECORD; + +uint8 constant BEFORE_ALL = BEFORE_SET_RECORD | + BEFORE_SPLICE_STATIC_DATA | + BEFORE_SPLICE_DYNAMIC_DATA | + BEFORE_DELETE_RECORD; + +uint8 constant AFTER_ALL = AFTER_SET_RECORD | + AFTER_SPLICE_STATIC_DATA | + AFTER_SPLICE_DYNAMIC_DATA | + AFTER_DELETE_RECORD; diff --git a/packages/store/test/EchoSubscriber.sol b/packages/store/test/EchoSubscriber.sol index e416d1753c..acaf3b50ab 100644 --- a/packages/store/test/EchoSubscriber.sol +++ b/packages/store/test/EchoSubscriber.sol @@ -15,8 +15,10 @@ contract EchoSubscriber is StoreHook { PackedCounter encodedLengths, bytes memory dynamicData, FieldLayout fieldLayout - ) public { - emit HookCalled(abi.encode(tableId, keyTuple, staticData, encodedLengths, dynamicData, fieldLayout)); + ) public override { + emit HookCalled( + abi.encodeCall(this.onBeforeSetRecord, (tableId, keyTuple, staticData, encodedLengths, dynamicData, fieldLayout)) + ); } function onAfterSetRecord( @@ -26,35 +28,71 @@ contract EchoSubscriber is StoreHook { PackedCounter encodedLengths, bytes memory dynamicData, FieldLayout fieldLayout - ) public { - emit HookCalled(abi.encode(tableId, keyTuple, staticData, encodedLengths, dynamicData, fieldLayout)); + ) public override { + emit HookCalled( + abi.encodeCall(this.onAfterSetRecord, (tableId, keyTuple, staticData, encodedLengths, dynamicData, fieldLayout)) + ); } - function onBeforeSetField( + function onBeforeSpliceStaticData( bytes32 tableId, bytes32[] memory keyTuple, - uint8 fieldIndex, + uint48 start, + uint40 deleteCount, + bytes memory data + ) public override { + emit HookCalled(abi.encodeCall(this.onBeforeSpliceStaticData, (tableId, keyTuple, start, deleteCount, data))); + } + + function onAfterSpliceStaticData( + bytes32 tableId, + bytes32[] memory keyTuple, + uint48 start, + uint40 deleteCount, + bytes memory data + ) public override { + emit HookCalled(abi.encodeCall(this.onAfterSpliceStaticData, (tableId, keyTuple, start, deleteCount, data))); + } + + function onBeforeSpliceDynamicData( + bytes32 tableId, + bytes32[] memory keyTuple, + uint8 dynamicFieldIndex, + uint40 startWithinField, + uint40 deleteCount, bytes memory data, - FieldLayout fieldLayout - ) public { - emit HookCalled(abi.encode(tableId, keyTuple, fieldIndex, data, fieldLayout)); + PackedCounter encodedLengths + ) public override { + emit HookCalled( + abi.encodeCall( + this.onBeforeSpliceDynamicData, + (tableId, keyTuple, dynamicFieldIndex, startWithinField, deleteCount, data, encodedLengths) + ) + ); } - function onAfterSetField( + function onAfterSpliceDynamicData( bytes32 tableId, bytes32[] memory keyTuple, - uint8 fieldIndex, + uint8 dynamicFieldIndex, + uint40 startWithinField, + uint40 deleteCount, bytes memory data, - FieldLayout fieldLayout - ) public { - emit HookCalled(abi.encode(tableId, keyTuple, fieldIndex, data, fieldLayout)); + PackedCounter encodedLengths + ) public override { + emit HookCalled( + abi.encodeCall( + this.onAfterSpliceDynamicData, + (tableId, keyTuple, dynamicFieldIndex, startWithinField, deleteCount, data, encodedLengths) + ) + ); } - function onBeforeDeleteRecord(bytes32 tableId, bytes32[] memory keyTuple, FieldLayout fieldLayout) public { - emit HookCalled(abi.encode(tableId, keyTuple, fieldLayout)); + function onBeforeDeleteRecord(bytes32 tableId, bytes32[] memory keyTuple, FieldLayout fieldLayout) public override { + emit HookCalled(abi.encodeCall(this.onBeforeDeleteRecord, (tableId, keyTuple, fieldLayout))); } - function onAfterDeleteRecord(bytes32 tableId, bytes32[] memory keyTuple, FieldLayout fieldLayout) public { - emit HookCalled(abi.encode(tableId, keyTuple, fieldLayout)); + function onAfterDeleteRecord(bytes32 tableId, bytes32[] memory keyTuple, FieldLayout fieldLayout) public override { + emit HookCalled(abi.encodeCall(this.onAfterDeleteRecord, (tableId, keyTuple, fieldLayout))); } } diff --git a/packages/store/test/MirrorSubscriber.sol b/packages/store/test/MirrorSubscriber.sol index 38681feb23..3c86958cd6 100644 --- a/packages/store/test/MirrorSubscriber.sol +++ b/packages/store/test/MirrorSubscriber.sol @@ -11,7 +11,7 @@ import { Schema } from "../src/Schema.sol"; bytes32 constant indexerTableId = keccak256("indexer.tableId"); contract MirrorSubscriber is StoreHook { - bytes32 _tableId; + bytes32 public _tableId; constructor( bytes32 tableId, @@ -28,47 +28,54 @@ contract MirrorSubscriber is StoreHook { function onBeforeSetRecord( bytes32 tableId, bytes32[] memory keyTuple, - bytes calldata staticData, + bytes memory staticData, PackedCounter encodedLengths, - bytes calldata dynamicData, + bytes memory dynamicData, FieldLayout fieldLayout - ) public { + ) public override { if (tableId != _tableId) revert("invalid table"); StoreSwitch.setRecord(indexerTableId, keyTuple, staticData, encodedLengths, dynamicData, fieldLayout); } - function onAfterSetRecord( + function onBeforeSpliceStaticData( bytes32 tableId, bytes32[] memory keyTuple, - bytes calldata staticData, - PackedCounter encodedLengths, - bytes calldata dynamicData, - FieldLayout fieldLayout - ) public { - // NOOP + uint48 start, + uint40 deleteCount, + bytes memory data + ) public override { + if (tableId != _tableId) revert("invalid tableId"); + StoreSwitch.spliceStaticData(indexerTableId, keyTuple, start, deleteCount, data); } - function onBeforeSetField( + function onBeforeSpliceDynamicData( bytes32 tableId, bytes32[] memory keyTuple, - uint8 fieldIndex, + uint8 dynamicFieldIndex, + uint40 startWithinField, + uint40 deleteCount, bytes memory data, - FieldLayout fieldLayout - ) public { - if (tableId != tableId) revert("invalid tableId"); - StoreSwitch.setField(indexerTableId, keyTuple, fieldIndex, data, fieldLayout); + PackedCounter + ) public override { + if (tableId != _tableId) revert("invalid tableId"); + StoreSwitch.spliceDynamicData(indexerTableId, keyTuple, dynamicFieldIndex, startWithinField, deleteCount, data); } - function onAfterSetField(bytes32, bytes32[] memory, uint8, bytes memory, FieldLayout) public { - // NOOP + function onAfterSpliceDynamicData( + bytes32 tableId, + bytes32[] memory keyTuple, + uint8 dynamicFieldIndex, + uint40 startWithinField, + uint40 deleteCount, + bytes memory data, + PackedCounter + ) public override { + if (tableId != _tableId) revert("invalid tableId"); + StoreSwitch.spliceDynamicData(indexerTableId, keyTuple, dynamicFieldIndex, startWithinField, deleteCount, data); } - function onBeforeDeleteRecord(bytes32 tableId, bytes32[] memory keyTuple, FieldLayout fieldLayout) public { + function onBeforeDeleteRecord(bytes32 tableId, bytes32[] memory keyTuple, FieldLayout fieldLayout) public override { if (tableId != tableId) revert("invalid tableId"); StoreSwitch.deleteRecord(indexerTableId, keyTuple, fieldLayout); } - - function onAfterDeleteRecord(bytes32 tableId, bytes32[] memory keyTuple, FieldLayout fieldLayout) public { - // NOOP - } } diff --git a/packages/store/test/RevertSubscriber.sol b/packages/store/test/RevertSubscriber.sol index 7c2ae092f7..7d706405d4 100644 --- a/packages/store/test/RevertSubscriber.sol +++ b/packages/store/test/RevertSubscriber.sol @@ -13,7 +13,7 @@ contract RevertSubscriber is StoreHook { PackedCounter, bytes memory, FieldLayout - ) public pure { + ) public pure override { revert("onBeforeSetRecord"); } @@ -24,23 +24,47 @@ contract RevertSubscriber is StoreHook { PackedCounter, bytes memory, FieldLayout - ) public pure { + ) public pure override { revert("onAfterSetRecord"); } - function onBeforeSetField(bytes32, bytes32[] memory, uint8, bytes memory, FieldLayout) public pure { - revert("onBeforeSetField"); + function onBeforeSpliceStaticData(bytes32, bytes32[] memory, uint48, uint40, bytes memory) public pure override { + revert("onBeforeSpliceStaticData"); } - function onAfterSetField(bytes32, bytes32[] memory, uint8, bytes memory, FieldLayout) public pure { - revert("onAfterSetField"); + function onAfterSpliceStaticData(bytes32, bytes32[] memory, uint48, uint40, bytes memory) public pure override { + revert("onAfterSpliceStaticData"); } - function onBeforeDeleteRecord(bytes32, bytes32[] memory, FieldLayout) public pure { + function onBeforeSpliceDynamicData( + bytes32, + bytes32[] memory, + uint8, + uint40, + uint40, + bytes memory, + PackedCounter + ) public pure override { + revert("onBeforeSpliceDynamicData"); + } + + function onAfterSpliceDynamicData( + bytes32, + bytes32[] memory, + uint8, + uint40, + uint40, + bytes memory, + PackedCounter + ) public pure override { + revert("onAfterSpliceDynamicData"); + } + + function onBeforeDeleteRecord(bytes32, bytes32[] memory, FieldLayout) public pure override { revert("onBeforeDeleteRecord"); } - function onAfterDeleteRecord(bytes32, bytes32[] memory, FieldLayout) public pure { + function onAfterDeleteRecord(bytes32, bytes32[] memory, FieldLayout) public pure override { revert("onAfterDeleteRecord"); } } diff --git a/packages/store/test/StoreCore.t.sol b/packages/store/test/StoreCore.t.sol index 32b03d19cb..132b198940 100644 --- a/packages/store/test/StoreCore.t.sol +++ b/packages/store/test/StoreCore.t.sol @@ -14,14 +14,16 @@ import { StoreMock } from "../test/StoreMock.sol"; import { IStoreErrors } from "../src/IStoreErrors.sol"; import { IStore } from "../src/IStore.sol"; import { StoreSwitch } from "../src/StoreSwitch.sol"; +import { IStoreHook } from "../src/IStoreHook.sol"; import { Tables, TablesTableId } from "../src/codegen/index.sol"; import { FieldLayoutEncodeHelper } from "./FieldLayoutEncodeHelper.sol"; -import { BEFORE_SET_RECORD, AFTER_SET_RECORD, BEFORE_SET_FIELD, AFTER_SET_FIELD, BEFORE_DELETE_RECORD, AFTER_DELETE_RECORD } from "../src/storeHookTypes.sol"; +import { BEFORE_SET_RECORD, AFTER_SET_RECORD, BEFORE_SPLICE_STATIC_DATA, AFTER_SPLICE_STATIC_DATA, BEFORE_SPLICE_DYNAMIC_DATA, AFTER_SPLICE_DYNAMIC_DATA, BEFORE_DELETE_RECORD, AFTER_DELETE_RECORD, ALL, BEFORE_ALL, AFTER_ALL } from "../src/storeHookTypes.sol"; import { SchemaEncodeHelper } from "./SchemaEncodeHelper.sol"; import { StoreMock } from "./StoreMock.sol"; import { MirrorSubscriber, indexerTableId } from "./MirrorSubscriber.sol"; import { RevertSubscriber } from "./RevertSubscriber.sol"; import { EchoSubscriber } from "./EchoSubscriber.sol"; +import { setDynamicDataLengthAtIndex } from "./setDynamicDataLengthAtIndex.sol"; struct TestStruct { uint128 firstData; @@ -185,7 +187,7 @@ contract StoreCoreTest is Test, StoreMock { keyTuple[0] = bytes32("some key"); // Set dynamic data length of dynamic index 0 - StoreCoreInternal._setDynamicDataLengthAtIndex(tableId, keyTuple, 0, 10); + setDynamicDataLengthAtIndex(tableId, keyTuple, 0, 10); PackedCounter encodedLength = StoreCoreInternal._loadEncodedDynamicDataLength(tableId, keyTuple); assertEq(encodedLength.atIndex(0), 10); @@ -193,7 +195,7 @@ contract StoreCoreTest is Test, StoreMock { assertEq(encodedLength.total(), 10); // Set dynamic data length of dynamic index 1 - StoreCoreInternal._setDynamicDataLengthAtIndex(tableId, keyTuple, 1, 99); + setDynamicDataLengthAtIndex(tableId, keyTuple, 1, 99); encodedLength = StoreCoreInternal._loadEncodedDynamicDataLength(tableId, keyTuple); assertEq(encodedLength.atIndex(0), 10); @@ -201,7 +203,7 @@ contract StoreCoreTest is Test, StoreMock { assertEq(encodedLength.total(), 109); // Reduce dynamic data length of dynamic index 0 again - StoreCoreInternal._setDynamicDataLengthAtIndex(tableId, keyTuple, 0, 5); + setDynamicDataLengthAtIndex(tableId, keyTuple, 0, 5); encodedLength = StoreCoreInternal._loadEncodedDynamicDataLength(tableId, keyTuple); assertEq(encodedLength.atIndex(0), 5); @@ -413,6 +415,10 @@ contract StoreCoreTest is Test, StoreMock { ); } + //////////////// + // Static data + //////////////// + _data.firstDataBytes = bytes16(0x0102030405060708090a0b0c0d0e0f10); // Create keyTuple @@ -428,10 +434,6 @@ contract StoreCoreTest is Test, StoreMock { // Set first field IStore(this).setField(_data.tableId, keyTuple, 0, _data.firstDataPacked, _data.fieldLayout); - //////////////// - // Static data - //////////////// - // Get first field bytes memory loadedData = IStore(this).getField(_data.tableId, keyTuple, 0, _data.fieldLayout); @@ -771,13 +773,13 @@ contract StoreCoreTest is Test, StoreMock { data.loadedData = IStore(this).getField(data.tableId, data.keyTuple, 1, fieldLayout); // Verify loaded data is correct - assertEq(SliceLib.fromBytes(data.loadedData).decodeArray_uint32().length, 2 + 1); - assertEq(data.loadedData.length, data.newSecondDataBytes.length); - assertEq(data.loadedData, data.newSecondDataBytes); + assertEq(SliceLib.fromBytes(data.loadedData).decodeArray_uint32().length, 2 + 1, "1"); + assertEq(data.loadedData.length, data.newSecondDataBytes.length, "2"); + assertEq(data.loadedData, data.newSecondDataBytes, "3"); // Verify none of the other fields were impacted - assertEq(bytes32(IStore(this).getField(data.tableId, data.keyTuple, 0, fieldLayout)), data.firstDataBytes); - assertEq(IStore(this).getField(data.tableId, data.keyTuple, 2, fieldLayout), data.thirdDataBytes); + assertEq(bytes32(IStore(this).getField(data.tableId, data.keyTuple, 0, fieldLayout)), data.firstDataBytes, "4"); + assertEq(IStore(this).getField(data.tableId, data.keyTuple, 2, fieldLayout), data.thirdDataBytes, "5"); // Create data to push { @@ -814,13 +816,13 @@ contract StoreCoreTest is Test, StoreMock { data.loadedData = IStore(this).getField(data.tableId, data.keyTuple, 2, fieldLayout); // Verify loaded data is correct - assertEq(SliceLib.fromBytes(data.loadedData).decodeArray_uint32().length, 3 + 10); - assertEq(data.loadedData.length, data.newThirdDataBytes.length); - assertEq(data.loadedData, data.newThirdDataBytes); + assertEq(SliceLib.fromBytes(data.loadedData).decodeArray_uint32().length, 3 + 10, "6"); + assertEq(data.loadedData.length, data.newThirdDataBytes.length, "7"); + assertEq(data.loadedData, data.newThirdDataBytes, "8"); // Verify none of the other fields were impacted - assertEq(bytes32(IStore(this).getField(data.tableId, data.keyTuple, 0, fieldLayout)), data.firstDataBytes); - assertEq(IStore(this).getField(data.tableId, data.keyTuple, 1, fieldLayout), data.newSecondDataBytes); + assertEq(bytes32(IStore(this).getField(data.tableId, data.keyTuple, 0, fieldLayout)), data.firstDataBytes, "9"); + assertEq(IStore(this).getField(data.tableId, data.keyTuple, 1, fieldLayout), data.newSecondDataBytes, "10"); } struct TestUpdateInFieldData { @@ -1031,7 +1033,7 @@ contract StoreCoreTest is Test, StoreMock { new string[](1) ); - IStore(this).registerStoreHook(tableId, subscriber, BEFORE_SET_RECORD | BEFORE_SET_FIELD | BEFORE_DELETE_RECORD); + IStore(this).registerStoreHook(tableId, subscriber, BEFORE_ALL); bytes memory staticData = abi.encodePacked(bytes16(0x0102030405060708090a0b0c0d0e0f10)); @@ -1062,46 +1064,34 @@ contract StoreCoreTest is Test, StoreMock { keyTuple[0] = "some key"; // Register table's value schema - FieldLayout fieldLayout = FieldLayoutEncodeHelper.encode(16, 0); - Schema valueSchema = SchemaEncodeHelper.encode(SchemaType.UINT128); - IStore(this).registerTable(tableId, fieldLayout, defaultKeySchema, valueSchema, new string[](1), new string[](1)); + FieldLayout fieldLayout = FieldLayoutEncodeHelper.encode(16, 1); + Schema valueSchema = SchemaEncodeHelper.encode(SchemaType.UINT128, SchemaType.STRING); + IStore(this).registerTable(tableId, fieldLayout, defaultKeySchema, valueSchema, new string[](1), new string[](2)); // Create a RevertSubscriber and an EchoSubscriber RevertSubscriber revertSubscriber = new RevertSubscriber(); EchoSubscriber echoSubscriber = new EchoSubscriber(); // Register both subscribers - IStore(this).registerStoreHook( - tableId, - revertSubscriber, - BEFORE_SET_RECORD | - AFTER_SET_RECORD | - BEFORE_SET_FIELD | - AFTER_SET_FIELD | - BEFORE_DELETE_RECORD | - AFTER_DELETE_RECORD - ); + IStore(this).registerStoreHook(tableId, revertSubscriber, ALL); // Register both subscribers - IStore(this).registerStoreHook( - tableId, - echoSubscriber, - BEFORE_SET_RECORD | - AFTER_SET_RECORD | - BEFORE_SET_FIELD | - AFTER_SET_FIELD | - BEFORE_DELETE_RECORD | - AFTER_DELETE_RECORD - ); + IStore(this).registerStoreHook(tableId, echoSubscriber, ALL); - bytes memory data = abi.encodePacked(bytes16(0x0102030405060708090a0b0c0d0e0f10)); + bytes memory staticData = abi.encodePacked(bytes16(0x0102030405060708090a0b0c0d0e0f10)); + bytes memory dynamicData = abi.encodePacked(bytes("some string")); + PackedCounter encodedLengths = PackedCounterLib.pack(dynamicData.length); // Expect a revert when the RevertSubscriber's onBeforeSetRecord hook is called vm.expectRevert(bytes("onBeforeSetRecord")); - IStore(this).setRecord(tableId, keyTuple, data, PackedCounter.wrap(bytes32(0)), new bytes(0), fieldLayout); + IStore(this).setRecord(tableId, keyTuple, staticData, encodedLengths, dynamicData, fieldLayout); - // Expect a revert when the RevertSubscriber's onBeforeSetField hook is called - vm.expectRevert(bytes("onBeforeSetField")); - IStore(this).setField(tableId, keyTuple, 0, data, fieldLayout); + // Expect a revert when the RevertSubscriber's onBeforeSpliceStaticData hook is called + vm.expectRevert(bytes("onBeforeSpliceStaticData")); + IStore(this).setField(tableId, keyTuple, 0, staticData, fieldLayout); + + // Expect a revert when the RevertSubscriber's hook onBeforeSpliceDynamicData is called + vm.expectRevert(bytes("onBeforeSpliceDynamicData")); + IStore(this).setField(tableId, keyTuple, 1, dynamicData, fieldLayout); // Expect a revert when the RevertSubscriber's onBeforeDeleteRecord hook is called vm.expectRevert(bytes("onBeforeDeleteRecord")); @@ -1112,31 +1102,67 @@ contract StoreCoreTest is Test, StoreMock { // Expect a HookCalled event to be emitted when the EchoSubscriber's onBeforeSetRecord hook is called vm.expectEmit(true, true, true, true); - emit HookCalled(abi.encode(tableId, keyTuple, data, PackedCounter.wrap(bytes32(0)), new bytes(0), fieldLayout)); + emit HookCalled( + abi.encodeCall( + IStoreHook.onBeforeSetRecord, + (tableId, keyTuple, staticData, encodedLengths, dynamicData, fieldLayout) + ) + ); // Expect a HookCalled event to be emitted when the EchoSubscriber's onAfterSetRecord hook is called vm.expectEmit(true, true, true, true); - emit HookCalled(abi.encode(tableId, keyTuple, data, PackedCounter.wrap(bytes32(0)), new bytes(0), fieldLayout)); + emit HookCalled( + abi.encodeCall( + IStoreHook.onAfterSetRecord, + (tableId, keyTuple, staticData, encodedLengths, dynamicData, fieldLayout) + ) + ); + + IStore(this).setRecord(tableId, keyTuple, staticData, encodedLengths, dynamicData, fieldLayout); + + // Expect a HookCalled event to be emitted when the EchoSubscriber's onBeforeSpliceStaticData hook is called + vm.expectEmit(true, true, true, true); + emit HookCalled( + abi.encodeCall(IStoreHook.onBeforeSpliceStaticData, (tableId, keyTuple, 0, uint40(staticData.length), staticData)) + ); + + // Expect a HookCalled event to be emitted when the EchoSubscriber's onAfterSpliceStaticData hook is called + vm.expectEmit(true, true, true, true); + emit HookCalled( + abi.encodeCall(IStoreHook.onAfterSpliceStaticData, (tableId, keyTuple, 0, uint40(staticData.length), staticData)) + ); - IStore(this).setRecord(tableId, keyTuple, data, PackedCounter.wrap(bytes32(0)), new bytes(0), fieldLayout); + IStore(this).setField(tableId, keyTuple, 0, staticData, fieldLayout); - // Expect a HookCalled event to be emitted when the EchoSubscriber's onBeforeSetField hook is called + // Expect a HookCalled event to be emitted when the EchoSubscriber's onBeforeSpliceDynamicData hook is called vm.expectEmit(true, true, true, true); - emit HookCalled(abi.encode(tableId, keyTuple, uint8(0), data, fieldLayout)); + emit HookCalled( + abi.encodeCall( + IStoreHook.onBeforeSpliceDynamicData, + (tableId, keyTuple, 0, 0, uint40(dynamicData.length), dynamicData, encodedLengths) + ) + ); - // Expect a HookCalled event to be emitted when the EchoSubscriber's onAfterSetField hook is called + // Expect a HookCalled event to be emitted when the EchoSubscriber's onAfterSpliceStaticData hook is called vm.expectEmit(true, true, true, true); - emit HookCalled(abi.encode(tableId, keyTuple, uint8(0), data, fieldLayout)); + emit HookCalled( + abi.encodeCall( + IStoreHook.onAfterSpliceDynamicData, + (tableId, keyTuple, 0, 0, uint40(dynamicData.length), dynamicData, encodedLengths) + ) + ); + + IStore(this).setField(tableId, keyTuple, 1, dynamicData, fieldLayout); - IStore(this).setField(tableId, keyTuple, 0, data, fieldLayout); + // TODO: add tests for hooks being called for all other dynamic operations // Expect a HookCalled event to be emitted when the EchoSubscriber's onBeforeDeleteRecord hook is called vm.expectEmit(true, true, true, true); - emit HookCalled(abi.encode(tableId, keyTuple, fieldLayout)); + emit HookCalled(abi.encodeCall(IStoreHook.onBeforeDeleteRecord, (tableId, keyTuple, fieldLayout))); // Expect a HookCalled event to be emitted when the EchoSubscriber's onAfterDeleteRecord hook is called vm.expectEmit(true, true, true, true); - emit HookCalled(abi.encode(tableId, keyTuple, fieldLayout)); + emit HookCalled(abi.encodeCall(IStoreHook.onAfterDeleteRecord, (tableId, keyTuple, fieldLayout))); IStore(this).deleteRecord(tableId, keyTuple, fieldLayout); } @@ -1167,7 +1193,7 @@ contract StoreCoreTest is Test, StoreMock { new string[](2) ); - IStore(this).registerStoreHook(tableId, subscriber, BEFORE_SET_RECORD | BEFORE_SET_FIELD | BEFORE_DELETE_RECORD); + IStore(this).registerStoreHook(tableId, subscriber, BEFORE_ALL); uint32[] memory arrayData = new uint32[](1); arrayData[0] = 0x01020304; diff --git a/packages/store/test/StoreCoreGas.t.sol b/packages/store/test/StoreCoreGas.t.sol index cd8227872d..219b3b8c64 100644 --- a/packages/store/test/StoreCoreGas.t.sol +++ b/packages/store/test/StoreCoreGas.t.sol @@ -18,7 +18,8 @@ import { FieldLayoutEncodeHelper } from "./FieldLayoutEncodeHelper.sol"; import { SchemaEncodeHelper } from "./SchemaEncodeHelper.sol"; import { StoreMock } from "./StoreMock.sol"; import { MirrorSubscriber } from "./MirrorSubscriber.sol"; -import { BEFORE_SET_RECORD, AFTER_SET_RECORD, BEFORE_SET_FIELD, AFTER_SET_FIELD, BEFORE_DELETE_RECORD, AFTER_DELETE_RECORD } from "../src/storeHookTypes.sol"; +import { BEFORE_SET_RECORD, AFTER_SET_RECORD, BEFORE_SPLICE_STATIC_DATA, AFTER_SPLICE_STATIC_DATA, BEFORE_SPLICE_DYNAMIC_DATA, AFTER_SPLICE_DYNAMIC_DATA, BEFORE_DELETE_RECORD, AFTER_DELETE_RECORD, BEFORE_ALL } from "../src/storeHookTypes.sol"; +import { setDynamicDataLengthAtIndex } from "./setDynamicDataLengthAtIndex.sol"; struct TestStruct { uint128 firstData; @@ -112,17 +113,17 @@ contract StoreCoreGasTest is Test, GasReporter, StoreMock { // Set dynamic data length of dynamic index 0 startGasReport("set dynamic length of dynamic index 0"); - StoreCoreInternal._setDynamicDataLengthAtIndex(tableId, keyTuple, 0, 10); + setDynamicDataLengthAtIndex(tableId, keyTuple, 0, 10); endGasReport(); // Set dynamic data length of dynamic index 1 startGasReport("set dynamic length of dynamic index 1"); - StoreCoreInternal._setDynamicDataLengthAtIndex(tableId, keyTuple, 1, 99); + setDynamicDataLengthAtIndex(tableId, keyTuple, 1, 99); endGasReport(); // Reduce dynamic data length of dynamic index 0 again startGasReport("reduce dynamic length of dynamic index 0"); - StoreCoreInternal._setDynamicDataLengthAtIndex(tableId, keyTuple, 0, 5); + setDynamicDataLengthAtIndex(tableId, keyTuple, 0, 5); endGasReport(); } @@ -612,7 +613,7 @@ contract StoreCoreGasTest is Test, GasReporter, StoreMock { ); startGasReport("register subscriber"); - StoreCore.registerStoreHook(tableId, subscriber, BEFORE_SET_RECORD | BEFORE_SET_FIELD | BEFORE_DELETE_RECORD); + StoreCore.registerStoreHook(tableId, subscriber, BEFORE_ALL); endGasReport(); bytes memory staticData = abi.encodePacked(bytes16(0x0102030405060708090a0b0c0d0e0f10)); @@ -654,7 +655,7 @@ contract StoreCoreGasTest is Test, GasReporter, StoreMock { ); startGasReport("register subscriber"); - StoreCore.registerStoreHook(tableId, subscriber, BEFORE_SET_RECORD | BEFORE_SET_FIELD | BEFORE_DELETE_RECORD); + StoreCore.registerStoreHook(tableId, subscriber, BEFORE_ALL); endGasReport(); uint32[] memory arrayData = new uint32[](1); diff --git a/packages/store/test/StoreHook.t.sol b/packages/store/test/StoreHook.t.sol index e8264e5aef..3060df585d 100644 --- a/packages/store/test/StoreHook.t.sol +++ b/packages/store/test/StoreHook.t.sol @@ -11,7 +11,7 @@ import { Hook, HookLib } from "../src/Hook.sol"; import { IStoreHook } from "../src/IStore.sol"; import { PackedCounter } from "../src/PackedCounter.sol"; import { FieldLayout } from "../src/FieldLayout.sol"; -import { BEFORE_SET_RECORD, AFTER_SET_RECORD, BEFORE_SET_FIELD, AFTER_SET_FIELD, BEFORE_DELETE_RECORD, AFTER_DELETE_RECORD } from "../src/storeHookTypes.sol"; +import { BEFORE_SET_RECORD, AFTER_SET_RECORD, BEFORE_SPLICE_STATIC_DATA, AFTER_SPLICE_STATIC_DATA, BEFORE_SPLICE_DYNAMIC_DATA, AFTER_SPLICE_DYNAMIC_DATA, BEFORE_DELETE_RECORD, AFTER_DELETE_RECORD, ALL, BEFORE_ALL, AFTER_ALL } from "../src/storeHookTypes.sol"; contract StoreHookTest is Test, GasReporter { event HookCalled(bytes); @@ -30,20 +30,24 @@ contract StoreHookTest is Test, GasReporter { function testEncodeBitmap() public { assertEq(BEFORE_SET_RECORD, uint8(0x01), "0b00000001"); assertEq(AFTER_SET_RECORD, uint8(0x02), "0b00000010"); - assertEq(BEFORE_SET_FIELD, uint8(0x04), "0b00000100"); - assertEq(AFTER_SET_FIELD, uint8(0x08), "0b00001000"); - assertEq(BEFORE_DELETE_RECORD, uint8(0x10), "0b00010000"); - assertEq(AFTER_DELETE_RECORD, uint8(0x20), "0b00100000"); + assertEq(BEFORE_SPLICE_STATIC_DATA, uint8(0x04), "0b00000100"); + assertEq(AFTER_SPLICE_STATIC_DATA, uint8(0x08), "0b00001000"); + assertEq(BEFORE_SPLICE_DYNAMIC_DATA, uint8(0x10), "0b00010000"); + assertEq(AFTER_SPLICE_DYNAMIC_DATA, uint8(0x20), "0b00100000"); + assertEq(BEFORE_DELETE_RECORD, uint8(0x40), "0b01000000"); + assertEq(AFTER_DELETE_RECORD, uint8(0x80), "0b10000000"); assertEq( BEFORE_SET_RECORD | AFTER_SET_RECORD | - BEFORE_SET_FIELD | - AFTER_SET_FIELD | + BEFORE_SPLICE_STATIC_DATA | + AFTER_SPLICE_STATIC_DATA | + BEFORE_SPLICE_DYNAMIC_DATA | + AFTER_SPLICE_DYNAMIC_DATA | BEFORE_DELETE_RECORD | AFTER_DELETE_RECORD, - uint8(0x3f), - "0b00111111" + uint8(0xff), + "0b11111111" ); } @@ -54,13 +58,42 @@ contract StoreHookTest is Test, GasReporter { address(echoSubscriber), BEFORE_SET_RECORD | AFTER_SET_RECORD | - BEFORE_SET_FIELD | - AFTER_SET_FIELD | + BEFORE_SPLICE_STATIC_DATA | + AFTER_SPLICE_STATIC_DATA | + BEFORE_SPLICE_DYNAMIC_DATA | + AFTER_SPLICE_DYNAMIC_DATA | BEFORE_DELETE_RECORD | AFTER_DELETE_RECORD ) ), - bytes21(abi.encodePacked(echoSubscriber, uint8(0x3f))) + bytes21(abi.encodePacked(echoSubscriber, uint8(0xff))) + ); + } + + function testShorthands() public { + assertEq( + ALL, + BEFORE_SET_RECORD | + AFTER_SET_RECORD | + BEFORE_SPLICE_STATIC_DATA | + AFTER_SPLICE_STATIC_DATA | + BEFORE_SPLICE_DYNAMIC_DATA | + AFTER_SPLICE_DYNAMIC_DATA | + BEFORE_DELETE_RECORD | + AFTER_DELETE_RECORD, + "0b11111111" + ); + + assertEq( + BEFORE_ALL, + BEFORE_SET_RECORD | BEFORE_SPLICE_STATIC_DATA | BEFORE_SPLICE_DYNAMIC_DATA | BEFORE_DELETE_RECORD, + "0b01010101" + ); + + assertEq( + AFTER_ALL, + AFTER_SET_RECORD | AFTER_SPLICE_STATIC_DATA | AFTER_SPLICE_DYNAMIC_DATA | AFTER_DELETE_RECORD, + "0b10101010" ); } @@ -68,16 +101,20 @@ contract StoreHookTest is Test, GasReporter { address hookAddress, bool enableBeforeSetRecord, bool enableAfterSetRecord, - bool enableBeforeSetField, - bool enableAfterSetField, + bool enableBeforeSpliceStaticData, + bool enableAfterSpliceStaticData, + bool enableBeforeSpliceDynamicData, + bool enableAfterSpliceDynamicData, bool enableBeforeDeleteRecord, bool enableAfterDeleteRecord ) public { uint8 enabledHooks = 0; if (enableBeforeSetRecord) enabledHooks |= BEFORE_SET_RECORD; if (enableAfterSetRecord) enabledHooks |= AFTER_SET_RECORD; - if (enableBeforeSetField) enabledHooks |= BEFORE_SET_FIELD; - if (enableAfterSetField) enabledHooks |= AFTER_SET_FIELD; + if (enableBeforeSpliceStaticData) enabledHooks |= BEFORE_SPLICE_STATIC_DATA; + if (enableAfterSpliceStaticData) enabledHooks |= AFTER_SPLICE_STATIC_DATA; + if (enableBeforeSpliceDynamicData) enabledHooks |= BEFORE_SPLICE_DYNAMIC_DATA; + if (enableAfterSpliceDynamicData) enabledHooks |= AFTER_SPLICE_DYNAMIC_DATA; if (enableBeforeDeleteRecord) enabledHooks |= BEFORE_DELETE_RECORD; if (enableAfterDeleteRecord) enabledHooks |= AFTER_DELETE_RECORD; @@ -88,7 +125,7 @@ contract StoreHookTest is Test, GasReporter { } function testIsEnabled() public { - Hook storeHook = HookLib.encode(address(echoSubscriber), BEFORE_SET_FIELD | AFTER_DELETE_RECORD); + Hook storeHook = HookLib.encode(address(echoSubscriber), BEFORE_SPLICE_STATIC_DATA | AFTER_DELETE_RECORD); startGasReport("check if store hook is enabled"); storeHook.isEnabled(BEFORE_SET_RECORD); @@ -96,8 +133,10 @@ contract StoreHookTest is Test, GasReporter { assertFalse(storeHook.isEnabled(BEFORE_SET_RECORD), "BEFORE_SET_RECORD"); assertFalse(storeHook.isEnabled(AFTER_SET_RECORD), "AFTER_SET_RECORD"); - assertTrue(storeHook.isEnabled(BEFORE_SET_FIELD), "BEFORE_SET_FIELD"); - assertFalse(storeHook.isEnabled(AFTER_SET_FIELD), "AFTER_SET_FIELD"); + assertTrue(storeHook.isEnabled(BEFORE_SPLICE_STATIC_DATA), "BEFORE_SPLICE_STATIC_DATA"); + assertFalse(storeHook.isEnabled(AFTER_SPLICE_STATIC_DATA), "AFTER_SPLICE_STATIC_DATA"); + assertFalse(storeHook.isEnabled(BEFORE_SPLICE_DYNAMIC_DATA), "BEFORE_SPLICE_DYNAMIC_DATA"); + assertFalse(storeHook.isEnabled(AFTER_SPLICE_DYNAMIC_DATA), "AFTER_SPLICE_DYNAMIC_DATA"); assertFalse(storeHook.isEnabled(BEFORE_DELETE_RECORD), "BEFORE_DELETE_RECORD"); assertTrue(storeHook.isEnabled(AFTER_DELETE_RECORD), "AFTER_DELETE_RECORD"); } @@ -106,16 +145,20 @@ contract StoreHookTest is Test, GasReporter { address hookAddress, bool enableBeforeSetRecord, bool enableAfterSetRecord, - bool enableBeforeSetField, - bool enableAfterSetField, + bool enableBeforeSpliceStaticData, + bool enableAfterSpliceStaticData, + bool enableBeforeSpliceDynamicData, + bool enableAfterSpliceDynamicData, bool enableBeforeDeleteRecord, bool enableAfterDeleteRecord ) public { uint8 enabledHooks = 0; if (enableBeforeSetRecord) enabledHooks |= BEFORE_SET_RECORD; if (enableAfterSetRecord) enabledHooks |= AFTER_SET_RECORD; - if (enableBeforeSetField) enabledHooks |= BEFORE_SET_FIELD; - if (enableAfterSetField) enabledHooks |= AFTER_SET_FIELD; + if (enableBeforeSpliceStaticData) enabledHooks |= BEFORE_SPLICE_STATIC_DATA; + if (enableAfterSpliceStaticData) enabledHooks |= AFTER_SPLICE_STATIC_DATA; + if (enableBeforeSpliceDynamicData) enabledHooks |= BEFORE_SPLICE_DYNAMIC_DATA; + if (enableAfterSpliceDynamicData) enabledHooks |= AFTER_SPLICE_DYNAMIC_DATA; if (enableBeforeDeleteRecord) enabledHooks |= BEFORE_DELETE_RECORD; if (enableAfterDeleteRecord) enabledHooks |= AFTER_DELETE_RECORD; @@ -123,14 +166,16 @@ contract StoreHookTest is Test, GasReporter { assertEq(storeHook.isEnabled(BEFORE_SET_RECORD), enableBeforeSetRecord); assertEq(storeHook.isEnabled(AFTER_SET_RECORD), enableAfterSetRecord); - assertEq(storeHook.isEnabled(BEFORE_SET_FIELD), enableBeforeSetField); - assertEq(storeHook.isEnabled(AFTER_SET_FIELD), enableAfterSetField); + assertEq(storeHook.isEnabled(BEFORE_SPLICE_STATIC_DATA), enableBeforeSpliceStaticData); + assertEq(storeHook.isEnabled(AFTER_SPLICE_STATIC_DATA), enableAfterSpliceStaticData); + assertEq(storeHook.isEnabled(BEFORE_SPLICE_DYNAMIC_DATA), enableBeforeSpliceDynamicData); + assertEq(storeHook.isEnabled(AFTER_SPLICE_DYNAMIC_DATA), enableAfterSpliceDynamicData); assertEq(storeHook.isEnabled(BEFORE_DELETE_RECORD), enableBeforeDeleteRecord); assertEq(storeHook.isEnabled(AFTER_DELETE_RECORD), enableAfterDeleteRecord); } function testGetAddress() public { - Hook storeHook = HookLib.encode(address(echoSubscriber), BEFORE_SET_FIELD); + Hook storeHook = HookLib.encode(address(echoSubscriber), BEFORE_SPLICE_STATIC_DATA); startGasReport("get store hook address"); storeHook.getAddress(); @@ -146,7 +191,12 @@ contract StoreHookTest is Test, GasReporter { bytes memory emptyDynamicData = new bytes(0); vm.expectEmit(true, true, true, true); - emit HookCalled(abi.encode(tableId, key, staticData, encodedLengths, emptyDynamicData, fieldLayout)); + emit HookCalled( + abi.encodeCall( + echoSubscriber.onBeforeSetRecord, + (tableId, key, staticData, encodedLengths, emptyDynamicData, fieldLayout) + ) + ); startGasReport("call an enabled hook"); if (storeHook.isEnabled(BEFORE_SET_RECORD)) { IStoreHook(storeHook.getAddress()).onBeforeSetRecord( diff --git a/packages/store/test/StoreMock.sol b/packages/store/test/StoreMock.sol index 9b515b4af5..761fbf282a 100644 --- a/packages/store/test/StoreMock.sol +++ b/packages/store/test/StoreMock.sol @@ -29,7 +29,30 @@ contract StoreMock is IStore, StoreRead { StoreCore.setRecord(tableId, keyTuple, staticData, encodedLengths, dynamicData, fieldLayout); } - // Set partial data at schema index + // Splice data in the static part of the record + function spliceStaticData( + bytes32 tableId, + bytes32[] calldata keyTuple, + uint48 start, + uint40 deleteCount, + bytes calldata data + ) public virtual { + StoreCore.spliceStaticData(tableId, keyTuple, start, deleteCount, data); + } + + // Splice data in the dynamic part of the record + function spliceDynamicData( + bytes32 tableId, + bytes32[] calldata keyTuple, + uint8 dynamicFieldIndex, + uint40 startWithinField, + uint40 deleteCount, + bytes calldata data + ) public virtual { + StoreCore.spliceDynamicData(tableId, keyTuple, dynamicFieldIndex, startWithinField, deleteCount, data); + } + + // Set partial data at field index function setField( bytes32 tableId, bytes32[] calldata keyTuple, @@ -40,7 +63,7 @@ contract StoreMock is IStore, StoreRead { StoreCore.setField(tableId, keyTuple, fieldIndex, data, fieldLayout); } - // Push encoded items to the dynamic field at schema index + // Push encoded items to the dynamic field at field index function pushToField( bytes32 tableId, bytes32[] calldata keyTuple, @@ -51,7 +74,7 @@ contract StoreMock is IStore, StoreRead { StoreCore.pushToField(tableId, keyTuple, fieldIndex, dataToPush, fieldLayout); } - // Pop byte length from the dynamic field at schema index + // Pop byte length from the dynamic field at field index function popFromField( bytes32 tableId, bytes32[] calldata keyTuple, @@ -62,7 +85,7 @@ contract StoreMock is IStore, StoreRead { StoreCore.popFromField(tableId, keyTuple, fieldIndex, byteLengthToPop, fieldLayout); } - // Change encoded items within the dynamic field at schema index + // Change encoded items within the dynamic field at field index function updateInField( bytes32 tableId, bytes32[] calldata keyTuple, diff --git a/packages/store/test/setDynamicDataLengthAtIndex.sol b/packages/store/test/setDynamicDataLengthAtIndex.sol new file mode 100644 index 0000000000..10bb2c9872 --- /dev/null +++ b/packages/store/test/setDynamicDataLengthAtIndex.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import { PackedCounter } from "../src/PackedCounter.sol"; +import { StoreCoreInternal } from "../src/StoreCore.sol"; +import { Storage } from "../src/Storage.sol"; + +/** + * Test helper function to set the length of the dynamic data (in bytes) for the given value field layout and index + */ +function setDynamicDataLengthAtIndex( + bytes32 tableId, + bytes32[] memory keyTuple, + uint8 dynamicFieldIndex, // fieldIndex - numStaticFields + uint256 newLengthAtIndex +) { + // Load dynamic data length from storage + uint256 dynamicDataLengthSlot = StoreCoreInternal._getDynamicDataLengthLocation(tableId, keyTuple); + PackedCounter encodedLengths = PackedCounter.wrap(Storage.load({ storagePointer: dynamicDataLengthSlot })); + + // Update the encoded lengths + encodedLengths = encodedLengths.setAtIndex(dynamicFieldIndex, newLengthAtIndex); + + // Set the new lengths + Storage.store({ storagePointer: dynamicDataLengthSlot, data: encodedLengths.unwrap() }); +} diff --git a/packages/world/gas-report.json b/packages/world/gas-report.json index fe406020c8..c021f1084d 100644 --- a/packages/world/gas-report.json +++ b/packages/world/gas-report.json @@ -39,73 +39,73 @@ "file": "test/KeysInTableModule.t.sol", "test": "testInstallComposite", "name": "install keys in table module", - "gasUsed": 1414994 + "gasUsed": 1413512 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "install keys in table module", - "gasUsed": 1414994 + "gasUsed": 1413512 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallGas", "name": "set a record on a table with keysInTableModule installed", - "gasUsed": 158881 + "gasUsed": 157414 }, { "file": "test/KeysInTableModule.t.sol", "test": "testInstallSingleton", "name": "install keys in table module", - "gasUsed": 1414994 + "gasUsed": 1413512 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "install keys in table module", - "gasUsed": 1414994 + "gasUsed": 1413512 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "change a composite record on a table with keysInTableModule installed", - "gasUsed": 21960 + "gasUsed": 22025 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookCompositeGas", "name": "delete a composite record on a table with keysInTableModule installed", - "gasUsed": 172418 + "gasUsed": 157762 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "install keys in table module", - "gasUsed": 1414994 + "gasUsed": 1413512 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "change a record on a table with keysInTableModule installed", - "gasUsed": 20682 + "gasUsed": 20747 }, { "file": "test/KeysInTableModule.t.sol", "test": "testSetAndDeleteRecordHookGas", "name": "delete a record on a table with keysInTableModule installed", - "gasUsed": 88978 + "gasUsed": 84152 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testGetKeysWithValueGas", "name": "install keys with value module", - "gasUsed": 654197 + "gasUsed": 652783 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testGetKeysWithValueGas", "name": "Get list of keys with a given value", - "gasUsed": 5351 + "gasUsed": 5338 }, { "file": "test/KeysWithValueModule.t.sol", @@ -117,264 +117,264 @@ "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "install keys with value module", - "gasUsed": 654197 + "gasUsed": 652783 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testInstall", "name": "set a record on a table with KeysWithValueModule installed", - "gasUsed": 135351 + "gasUsed": 134041 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "install keys with value module", - "gasUsed": 654197 + "gasUsed": 652783 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "change a record on a table with KeysWithValueModule installed", - "gasUsed": 104926 + "gasUsed": 104450 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetAndDeleteRecordHook", "name": "delete a record on a table with KeysWithValueModule installed", - "gasUsed": 33805 + "gasUsed": 33978 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "install keys with value module", - "gasUsed": 654197 + "gasUsed": 652783 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "set a field on a table with KeysWithValueModule installed", - "gasUsed": 140853 + "gasUsed": 145891 }, { "file": "test/KeysWithValueModule.t.sol", "test": "testSetField", "name": "change a field on a table with KeysWithValueModule installed", - "gasUsed": 105611 + "gasUsed": 110650 }, { "file": "test/query.t.sol", "test": "testCombinedHasHasValueNotQuery", "name": "CombinedHasHasValueNotQuery", - "gasUsed": 101600 + "gasUsed": 101456 }, { "file": "test/query.t.sol", "test": "testCombinedHasHasValueQuery", "name": "CombinedHasHasValueQuery", - "gasUsed": 51669 + "gasUsed": 51595 }, { "file": "test/query.t.sol", "test": "testCombinedHasNotQuery", "name": "CombinedHasNotQuery", - "gasUsed": 127476 + "gasUsed": 127301 }, { "file": "test/query.t.sol", "test": "testCombinedHasQuery", "name": "CombinedHasQuery", - "gasUsed": 80997 + "gasUsed": 80857 }, { "file": "test/query.t.sol", "test": "testCombinedHasValueNotQuery", "name": "CombinedHasValueNotQuery", - "gasUsed": 82351 + "gasUsed": 82233 }, { "file": "test/query.t.sol", "test": "testCombinedHasValueQuery", "name": "CombinedHasValueQuery", - "gasUsed": 14966 + "gasUsed": 14940 }, { "file": "test/query.t.sol", "test": "testHasQuery", "name": "HasQuery", - "gasUsed": 17988 + "gasUsed": 17953 }, { "file": "test/query.t.sol", "test": "testHasQuery1000Keys", "name": "HasQuery with 1000 keys", - "gasUsed": 5901561 + "gasUsed": 5901526 }, { "file": "test/query.t.sol", "test": "testHasQuery100Keys", "name": "HasQuery with 100 keys", - "gasUsed": 550255 + "gasUsed": 550220 }, { "file": "test/query.t.sol", "test": "testHasValueQuery", "name": "HasValueQuery", - "gasUsed": 7154 + "gasUsed": 7141 }, { "file": "test/query.t.sol", "test": "testNotValueQuery", "name": "NotValueQuery", - "gasUsed": 45265 + "gasUsed": 45191 }, { "file": "test/StandardDelegationsModule.t.sol", "test": "testCallFromCallboundDelegation", "name": "register a callbound delegation", - "gasUsed": 113920 + "gasUsed": 113954 }, { "file": "test/StandardDelegationsModule.t.sol", "test": "testCallFromCallboundDelegation", "name": "call a system via a callbound delegation", - "gasUsed": 33554 + "gasUsed": 33517 }, { "file": "test/StandardDelegationsModule.t.sol", "test": "testCallFromTimeboundDelegation", "name": "register a timebound delegation", - "gasUsed": 108384 + "gasUsed": 108418 }, { "file": "test/StandardDelegationsModule.t.sol", "test": "testCallFromTimeboundDelegation", "name": "call a system via a timebound delegation", - "gasUsed": 26666 + "gasUsed": 26642 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstall", "name": "install unique entity module", - "gasUsed": 678867 + "gasUsed": 678987 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstall", "name": "get a unique entity nonce (non-root module)", - "gasUsed": 51180 + "gasUsed": 51263 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstallRoot", "name": "installRoot unique entity module", - "gasUsed": 668940 + "gasUsed": 669072 }, { "file": "test/UniqueEntityModule.t.sol", "test": "testInstallRoot", "name": "get a unique entity nonce (root module)", - "gasUsed": 51180 + "gasUsed": 51263 }, { "file": "test/World.t.sol", "test": "testCall", "name": "call a system via the World", - "gasUsed": 12380 + "gasUsed": 12356 }, { "file": "test/World.t.sol", "test": "testCallFromUnlimitedDelegation", "name": "register an unlimited delegation", - "gasUsed": 50260 + "gasUsed": 50265 }, { "file": "test/World.t.sol", "test": "testCallFromUnlimitedDelegation", "name": "call a system via an unlimited delegation", - "gasUsed": 12729 + "gasUsed": 12705 }, { "file": "test/World.t.sol", "test": "testDeleteRecord", "name": "Delete record", - "gasUsed": 8846 + "gasUsed": 8888 }, { "file": "test/World.t.sol", "test": "testPushToField", "name": "Push data to the table", - "gasUsed": 88225 + "gasUsed": 86650 }, { "file": "test/World.t.sol", "test": "testRegisterFallbackSystem", "name": "Register a fallback system", - "gasUsed": 58889 + "gasUsed": 58852 }, { "file": "test/World.t.sol", "test": "testRegisterFallbackSystem", "name": "Register a root fallback system", - "gasUsed": 52206 + "gasUsed": 52169 }, { "file": "test/World.t.sol", "test": "testRegisterFunctionSelector", "name": "Register a function selector", - "gasUsed": 79483 + "gasUsed": 79446 }, { "file": "test/World.t.sol", "test": "testRegisterNamespace", "name": "Register a new namespace", - "gasUsed": 122753 + "gasUsed": 122816 }, { "file": "test/World.t.sol", "test": "testRegisterRootFunctionSelector", "name": "Register a root function selector", - "gasUsed": 74124 + "gasUsed": 74087 }, { "file": "test/World.t.sol", "test": "testRegisterTable", "name": "Register a new table in the namespace", - "gasUsed": 641595 + "gasUsed": 641696 }, { "file": "test/World.t.sol", "test": "testSetField", "name": "Write data to a table field", - "gasUsed": 37131 + "gasUsed": 37171 }, { "file": "test/World.t.sol", "test": "testSetRecord", "name": "Write data to the table", - "gasUsed": 35125 + "gasUsed": 35165 }, { "file": "test/WorldDynamicUpdate.t.sol", "test": "testPopFromField", "name": "pop 1 address (cold)", - "gasUsed": 25429 + "gasUsed": 24401 }, { "file": "test/WorldDynamicUpdate.t.sol", "test": "testPopFromField", "name": "pop 1 address (warm)", - "gasUsed": 14541 + "gasUsed": 13547 }, { "file": "test/WorldDynamicUpdate.t.sol", "test": "testUpdateInField", "name": "updateInField 1 item (cold)", - "gasUsed": 28784 + "gasUsed": 25012 }, { "file": "test/WorldDynamicUpdate.t.sol", "test": "testUpdateInField", "name": "updateInField 1 item (warm)", - "gasUsed": 17989 + "gasUsed": 14217 } ] diff --git a/packages/world/src/World.sol b/packages/world/src/World.sol index 15315eec16..73464c011b 100644 --- a/packages/world/src/World.sol +++ b/packages/world/src/World.sol @@ -117,6 +117,35 @@ contract World is StoreRead, IStoreData, IWorldKernel { StoreCore.setRecord(tableId, keyTuple, staticData, encodedLengths, dynamicData, fieldLayout); } + function spliceStaticData( + bytes32 tableId, + bytes32[] calldata keyTuple, + uint48 start, + uint40 deleteCount, + bytes calldata data + ) public virtual { + // Require access to the namespace or name + AccessControl.requireAccess(tableId, msg.sender); + + // Splice the static data + StoreCore.spliceStaticData(tableId, keyTuple, start, deleteCount, data); + } + + function spliceDynamicData( + bytes32 tableId, + bytes32[] calldata keyTuple, + uint8 dynamicFieldIndex, + uint40 startWithinField, + uint40 deleteCount, + bytes calldata data + ) public virtual { + // Require access to the namespace or name + AccessControl.requireAccess(tableId, msg.sender); + + // Splice the dynamic data + StoreCore.spliceDynamicData(tableId, keyTuple, dynamicFieldIndex, startWithinField, deleteCount, data); + } + /** * Write a field in the table at the given tableId. * Requires the caller to have access to the table's namespace or name (encoded in the tableId). diff --git a/packages/world/src/modules/keysintable/KeysInTableHook.sol b/packages/world/src/modules/keysintable/KeysInTableHook.sol index 4a9c518988..69113cc5a8 100644 --- a/packages/world/src/modules/keysintable/KeysInTableHook.sol +++ b/packages/world/src/modules/keysintable/KeysInTableHook.sol @@ -48,30 +48,33 @@ contract KeysInTableHook is StoreHook { PackedCounter, bytes memory, FieldLayout - ) public { + ) public override { handleSet(tableId, keyTuple); } - function onAfterSetRecord( + function onAfterSpliceStaticData( bytes32 tableId, bytes32[] memory keyTuple, - bytes memory, - PackedCounter, - bytes memory, - FieldLayout - ) public { - // NOOP - } - - function onBeforeSetField(bytes32 tableId, bytes32[] memory keyTuple, uint8, bytes memory, FieldLayout) public { - // NOOP + uint48, + uint40, + bytes memory + ) public override { + handleSet(tableId, keyTuple); } - function onAfterSetField(bytes32 tableId, bytes32[] memory keyTuple, uint8, bytes memory, FieldLayout) public { + function onAfterSpliceDynamicData( + bytes32 tableId, + bytes32[] memory keyTuple, + uint8, + uint40, + uint40, + bytes memory, + PackedCounter + ) public override { handleSet(tableId, keyTuple); } - function onBeforeDeleteRecord(bytes32 tableId, bytes32[] memory keyTuple, FieldLayout) public { + function onBeforeDeleteRecord(bytes32 tableId, bytes32[] memory keyTuple, FieldLayout) public override { bytes32 keysHash = keccak256(abi.encode(keyTuple)); (bool has, uint40 index) = UsedKeysIndex.get(tableId, keysHash); @@ -135,8 +138,4 @@ contract KeysInTableHook is StoreHook { } } } - - function onAfterDeleteRecord(bytes32 tableId, bytes32[] memory keyTuple, FieldLayout fieldLayout) public { - // NOOP - } } diff --git a/packages/world/src/modules/keysintable/KeysInTableModule.sol b/packages/world/src/modules/keysintable/KeysInTableModule.sol index b775ea020a..cd6d0b9f7d 100644 --- a/packages/world/src/modules/keysintable/KeysInTableModule.sol +++ b/packages/world/src/modules/keysintable/KeysInTableModule.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import { BEFORE_SET_RECORD, AFTER_SET_FIELD, BEFORE_DELETE_RECORD } from "@latticexyz/store/src/storeHookTypes.sol"; +import { BEFORE_SET_RECORD, AFTER_SPLICE_STATIC_DATA, AFTER_SPLICE_DYNAMIC_DATA, BEFORE_DELETE_RECORD } from "@latticexyz/store/src/storeHookTypes.sol"; import { ResourceType } from "../core/tables/ResourceType.sol"; import { Resource } from "../../common.sol"; @@ -95,7 +95,11 @@ contract KeysInTableModule is Module { (success, returnData) = address(world).delegatecall( abi.encodeCall( world.registerStoreHook, - (sourceTableId, hook, BEFORE_SET_RECORD | AFTER_SET_FIELD | BEFORE_DELETE_RECORD) + ( + sourceTableId, + hook, + BEFORE_SET_RECORD | AFTER_SPLICE_STATIC_DATA | AFTER_SPLICE_DYNAMIC_DATA | BEFORE_DELETE_RECORD + ) ) ); } diff --git a/packages/world/src/modules/keyswithvalue/KeysWithValueHook.sol b/packages/world/src/modules/keyswithvalue/KeysWithValueHook.sol index a16a09208e..d666a3f4fa 100644 --- a/packages/world/src/modules/keyswithvalue/KeysWithValueHook.sol +++ b/packages/world/src/modules/keyswithvalue/KeysWithValueHook.sol @@ -6,6 +6,7 @@ import { Bytes } from "@latticexyz/store/src/Bytes.sol"; import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; import { PackedCounter } from "@latticexyz/store/src/PackedCounter.sol"; +import { Tables } from "@latticexyz/store/src/codegen/tables/Tables.sol"; import { IBaseWorld } from "../../interfaces/IBaseWorld.sol"; import { ResourceSelector } from "../../ResourceSelector.sol"; @@ -37,11 +38,11 @@ contract KeysWithValueHook is StoreHook { PackedCounter encodedLengths, bytes memory dynamicData, FieldLayout fieldLayout - ) public { + ) public override { bytes32 targetTableId = getTargetTableSelector(MODULE_NAMESPACE, sourceTableId); // Get the previous value - bytes32 previousValue = _getRecordValue(sourceTableId, keyTuple, fieldLayout); + bytes32 previousValue = _getRecordValueHash(sourceTableId, keyTuple, fieldLayout); // Remove the key from the list of keys with the previous value _removeKeyFromList(targetTableId, keyTuple[0], previousValue); @@ -56,59 +57,82 @@ contract KeysWithValueHook is StoreHook { KeysWithValue.push(targetTableId, keccak256(data), keyTuple[0]); } - function onAfterSetRecord( + function onBeforeSpliceStaticData( bytes32 sourceTableId, bytes32[] memory keyTuple, - bytes memory staticData, - PackedCounter encodedLengths, - bytes memory dynamicData, - FieldLayout fieldLayout - ) public { - // NOOP + uint48, + uint40, + bytes memory + ) public override { + // Remove the key from the list of keys with the previous value + FieldLayout fieldLayout = FieldLayout.wrap(Tables.getFieldLayout(sourceTableId)); + bytes32 previousValue = _getRecordValueHash(sourceTableId, keyTuple, fieldLayout); + bytes32 targetTableId = getTargetTableSelector(MODULE_NAMESPACE, sourceTableId); + _removeKeyFromList(targetTableId, keyTuple[0], previousValue); } - function onBeforeSetField( + function onAfterSpliceStaticData( + bytes32 sourceTableId, + bytes32[] memory keyTuple, + uint48, + uint40, + bytes memory + ) public override { + // Add the key to the list of keys with the new value + FieldLayout fieldLayout = FieldLayout.wrap(Tables.getFieldLayout(sourceTableId)); + bytes32 newValue = _getRecordValueHash(sourceTableId, keyTuple, fieldLayout); + bytes32 targetTableId = getTargetTableSelector(MODULE_NAMESPACE, sourceTableId); + KeysWithValue.push(targetTableId, newValue, keyTuple[0]); + } + + function onBeforeSpliceDynamicData( bytes32 sourceTableId, bytes32[] memory keyTuple, uint8, + uint40, + uint40, bytes memory, - FieldLayout fieldLayout - ) public { + PackedCounter + ) public override { // Remove the key from the list of keys with the previous value - bytes32 previousValue = _getRecordValue(sourceTableId, keyTuple, fieldLayout); + FieldLayout fieldLayout = FieldLayout.wrap(Tables.getFieldLayout(sourceTableId)); + bytes32 previousValue = _getRecordValueHash(sourceTableId, keyTuple, fieldLayout); bytes32 targetTableId = getTargetTableSelector(MODULE_NAMESPACE, sourceTableId); _removeKeyFromList(targetTableId, keyTuple[0], previousValue); } - function onAfterSetField( + function onAfterSpliceDynamicData( bytes32 sourceTableId, bytes32[] memory keyTuple, uint8, + uint40, + uint40, bytes memory, - FieldLayout fieldLayout - ) public { + PackedCounter + ) public override { // Add the key to the list of keys with the new value - bytes32 newValue = _getRecordValue(sourceTableId, keyTuple, fieldLayout); + FieldLayout fieldLayout = FieldLayout.wrap(Tables.getFieldLayout(sourceTableId)); + bytes32 newValue = _getRecordValueHash(sourceTableId, keyTuple, fieldLayout); bytes32 targetTableId = getTargetTableSelector(MODULE_NAMESPACE, sourceTableId); KeysWithValue.push(targetTableId, newValue, keyTuple[0]); } - function onBeforeDeleteRecord(bytes32 sourceTableId, bytes32[] memory keyTuple, FieldLayout fieldLayout) public { + function onBeforeDeleteRecord( + bytes32 sourceTableId, + bytes32[] memory keyTuple, + FieldLayout fieldLayout + ) public override { // Remove the key from the list of keys with the previous value - bytes32 previousValue = _getRecordValue(sourceTableId, keyTuple, fieldLayout); + bytes32 previousValue = _getRecordValueHash(sourceTableId, keyTuple, fieldLayout); bytes32 targetTableId = getTargetTableSelector(MODULE_NAMESPACE, sourceTableId); _removeKeyFromList(targetTableId, keyTuple[0], previousValue); } - function onAfterDeleteRecord(bytes32 sourceTableId, bytes32[] memory keyTuple, FieldLayout fieldLayout) public { - // NOOP - } - - function _getRecordValue( + function _getRecordValueHash( bytes32 sourceTableId, bytes32[] memory keyTuple, FieldLayout fieldLayout - ) internal view returns (bytes32 previousValue) { + ) internal view returns (bytes32 valueHash) { (bytes memory staticData, PackedCounter encodedLengths, bytes memory dynamicData) = _world().getRecord( sourceTableId, keyTuple, diff --git a/packages/world/src/modules/keyswithvalue/KeysWithValueModule.sol b/packages/world/src/modules/keyswithvalue/KeysWithValueModule.sol index 453450cfaf..65774a0093 100644 --- a/packages/world/src/modules/keyswithvalue/KeysWithValueModule.sol +++ b/packages/world/src/modules/keyswithvalue/KeysWithValueModule.sol @@ -2,7 +2,7 @@ pragma solidity >=0.8.0; import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; -import { BEFORE_SET_RECORD, BEFORE_SET_FIELD, AFTER_SET_FIELD, BEFORE_DELETE_RECORD } from "@latticexyz/store/src/storeHookTypes.sol"; +import { BEFORE_SET_RECORD, BEFORE_SPLICE_STATIC_DATA, AFTER_SPLICE_STATIC_DATA, BEFORE_SPLICE_DYNAMIC_DATA, AFTER_SPLICE_DYNAMIC_DATA, BEFORE_DELETE_RECORD } from "@latticexyz/store/src/storeHookTypes.sol"; import { Module } from "../../Module.sol"; import { IBaseWorld } from "../../interfaces/IBaseWorld.sol"; @@ -55,7 +55,16 @@ contract KeysWithValueModule is Module { (bool success, bytes memory returnData) = address(world).delegatecall( abi.encodeCall( world.registerStoreHook, - (sourceTableId, hook, BEFORE_SET_RECORD | BEFORE_SET_FIELD | AFTER_SET_FIELD | BEFORE_DELETE_RECORD) + ( + sourceTableId, + hook, + BEFORE_SET_RECORD | + BEFORE_SPLICE_STATIC_DATA | + AFTER_SPLICE_STATIC_DATA | + BEFORE_SPLICE_DYNAMIC_DATA | + AFTER_SPLICE_DYNAMIC_DATA | + BEFORE_DELETE_RECORD + ) ) ); if (!success) revertWithBytes(returnData); diff --git a/packages/world/src/systemHookTypes.sol b/packages/world/src/systemHookTypes.sol index 0f578dfed5..d23fb25ab5 100644 --- a/packages/world/src/systemHookTypes.sol +++ b/packages/world/src/systemHookTypes.sol @@ -3,3 +3,5 @@ pragma solidity >=0.8.0; uint8 constant BEFORE_CALL_SYSTEM = 1 << 0; uint8 constant AFTER_CALL_SYSTEM = 1 << 1; + +uint8 constant ALL = BEFORE_CALL_SYSTEM | AFTER_CALL_SYSTEM; diff --git a/packages/world/test/SystemHook.t.sol b/packages/world/test/SystemHook.t.sol index bcc9f66784..f191923d93 100644 --- a/packages/world/test/SystemHook.t.sol +++ b/packages/world/test/SystemHook.t.sol @@ -5,7 +5,7 @@ import "forge-std/Test.sol"; import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol"; import { Hook, HookLib } from "@latticexyz/store/src/Hook.sol"; -import { BEFORE_CALL_SYSTEM, AFTER_CALL_SYSTEM } from "../src/systemHookTypes.sol"; +import { BEFORE_CALL_SYSTEM, AFTER_CALL_SYSTEM, ALL } from "../src/systemHookTypes.sol"; import { ISystemHook } from "../src/interfaces/ISystemHook.sol"; contract SystemHookTest is Test, GasReporter { @@ -45,4 +45,8 @@ contract SystemHookTest is Test, GasReporter { assertEq(systemHook.getAddress(), hookAddress); assertEq(systemHook.getBitmap(), enabledHooksBitmap); } + + function testShorthand() public { + assertEq(ALL, BEFORE_CALL_SYSTEM | AFTER_CALL_SYSTEM); + } } diff --git a/packages/world/test/World.t.sol b/packages/world/test/World.t.sol index 6150e2b751..9194959b2d 100644 --- a/packages/world/test/World.t.sol +++ b/packages/world/test/World.t.sol @@ -16,7 +16,7 @@ import { PackedCounter } from "@latticexyz/store/src/PackedCounter.sol"; import { SchemaEncodeHelper } from "@latticexyz/store/test/SchemaEncodeHelper.sol"; import { Tables, TablesTableId } from "@latticexyz/store/src/codegen/index.sol"; import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; -import { BEFORE_SET_RECORD, AFTER_SET_RECORD, BEFORE_SET_FIELD, AFTER_SET_FIELD, BEFORE_DELETE_RECORD, AFTER_DELETE_RECORD } from "@latticexyz/store/src/storeHookTypes.sol"; +import { ALL, BEFORE_SET_RECORD, AFTER_SET_RECORD, BEFORE_SPLICE_STATIC_DATA, AFTER_SPLICE_STATIC_DATA, BEFORE_SPLICE_DYNAMIC_DATA, AFTER_SPLICE_DYNAMIC_DATA, BEFORE_DELETE_RECORD, AFTER_DELETE_RECORD } from "@latticexyz/store/src/storeHookTypes.sol"; import { RevertSubscriber } from "@latticexyz/store/test/RevertSubscriber.sol"; import { EchoSubscriber } from "@latticexyz/store/test/EchoSubscriber.sol"; @@ -832,16 +832,7 @@ contract WorldTest is Test, GasReporter { // Register a new hook IStoreHook tableHook = new EchoSubscriber(); - world.registerStoreHook( - tableId, - tableHook, - BEFORE_SET_RECORD | - AFTER_SET_RECORD | - BEFORE_SET_FIELD | - AFTER_SET_FIELD | - BEFORE_DELETE_RECORD | - AFTER_DELETE_RECORD - ); + world.registerStoreHook(tableId, tableHook, ALL); // Prepare data to write to the table bytes memory staticData = abi.encodePacked(true); @@ -849,31 +840,47 @@ contract WorldTest is Test, GasReporter { // Expect the hook to be notified when a record is written (once before and once after the record is written) vm.expectEmit(true, true, true, true); emit HookCalled( - abi.encode(tableId, singletonKey, staticData, PackedCounter.wrap(bytes32(0)), new bytes(0), fieldLayout) + abi.encodeCall( + IStoreHook.onBeforeSetRecord, + (tableId, singletonKey, staticData, PackedCounter.wrap(bytes32(0)), new bytes(0), fieldLayout) + ) ); vm.expectEmit(true, true, true, true); emit HookCalled( - abi.encode(tableId, singletonKey, staticData, PackedCounter.wrap(bytes32(0)), new bytes(0), fieldLayout) + abi.encodeCall( + IStoreHook.onAfterSetRecord, + (tableId, singletonKey, staticData, PackedCounter.wrap(bytes32(0)), new bytes(0), fieldLayout) + ) ); world.setRecord(tableId, singletonKey, staticData, PackedCounter.wrap(bytes32(0)), new bytes(0), fieldLayout); - // Expect the hook to be notified when a field is written (once before and once after the field is written) + // Expect the hook to be notified when a static field is written (once before and once after the field is written) vm.expectEmit(true, true, true, true); - emit HookCalled(abi.encode(tableId, singletonKey, uint8(0), staticData, fieldLayout)); + emit HookCalled( + abi.encodeCall( + IStoreHook.onBeforeSpliceStaticData, + (tableId, singletonKey, 0, uint40(staticData.length), staticData) + ) + ); vm.expectEmit(true, true, true, true); - emit HookCalled(abi.encode(tableId, singletonKey, uint8(0), staticData, fieldLayout)); + emit HookCalled( + abi.encodeCall( + IStoreHook.onAfterSpliceStaticData, + (tableId, singletonKey, 0, uint40(staticData.length), staticData) + ) + ); world.setField(tableId, singletonKey, 0, staticData, fieldLayout); // Expect the hook to be notified when a record is deleted (once before and once after the field is written) vm.expectEmit(true, true, true, true); - emit HookCalled(abi.encode(tableId, singletonKey, fieldLayout)); + emit HookCalled(abi.encodeCall(IStoreHook.onBeforeDeleteRecord, (tableId, singletonKey, fieldLayout))); vm.expectEmit(true, true, true, true); - emit HookCalled(abi.encode(tableId, singletonKey, fieldLayout)); + emit HookCalled(abi.encodeCall(IStoreHook.onAfterDeleteRecord, (tableId, singletonKey, fieldLayout))); world.deleteRecord(tableId, singletonKey, fieldLayout); @@ -884,12 +891,7 @@ contract WorldTest is Test, GasReporter { world.registerStoreHook( tableId, IStoreHook(address(world)), // the World contract does not implement the store hook interface - BEFORE_SET_RECORD | - AFTER_SET_RECORD | - BEFORE_SET_FIELD | - AFTER_SET_FIELD | - BEFORE_DELETE_RECORD | - AFTER_DELETE_RECORD + ALL ); } @@ -903,28 +905,10 @@ contract WorldTest is Test, GasReporter { // Register a new RevertSubscriber IStoreHook revertSubscriber = new RevertSubscriber(); - world.registerStoreHook( - tableId, - revertSubscriber, - BEFORE_SET_RECORD | - AFTER_SET_RECORD | - BEFORE_SET_FIELD | - AFTER_SET_FIELD | - BEFORE_DELETE_RECORD | - AFTER_DELETE_RECORD - ); + world.registerStoreHook(tableId, revertSubscriber, ALL); // Register a new EchoSubscriber IStoreHook echoSubscriber = new EchoSubscriber(); - world.registerStoreHook( - tableId, - echoSubscriber, - BEFORE_SET_RECORD | - AFTER_SET_RECORD | - BEFORE_SET_FIELD | - AFTER_SET_FIELD | - BEFORE_DELETE_RECORD | - AFTER_DELETE_RECORD - ); + world.registerStoreHook(tableId, echoSubscriber, ALL); // Prepare data to write to the table bytes memory staticData = abi.encodePacked(true); @@ -933,8 +917,8 @@ contract WorldTest is Test, GasReporter { vm.expectRevert(bytes("onBeforeSetRecord")); world.setRecord(tableId, singletonKey, staticData, PackedCounter.wrap(bytes32(0)), new bytes(0), fieldLayout); - // Expect a revert when the RevertSubscriber's onBeforeSetField hook is called - vm.expectRevert(bytes("onBeforeSetField")); + // Expect a revert when the RevertSubscriber's onBeforeSpliceStaticData hook is called + vm.expectRevert(bytes("onBeforeSpliceStaticData")); world.setField(tableId, singletonKey, 0, staticData, fieldLayout); // Expect a revert when the RevertSubscriber's onBeforeDeleteRecord hook is called @@ -947,31 +931,47 @@ contract WorldTest is Test, GasReporter { // Expect the hook to be notified when a record is written (once before and once after the record is written) vm.expectEmit(true, true, true, true); emit HookCalled( - abi.encode(tableId, singletonKey, staticData, PackedCounter.wrap(bytes32(0)), new bytes(0), fieldLayout) + abi.encodeCall( + IStoreHook.onBeforeSetRecord, + (tableId, singletonKey, staticData, PackedCounter.wrap(bytes32(0)), new bytes(0), fieldLayout) + ) ); vm.expectEmit(true, true, true, true); emit HookCalled( - abi.encode(tableId, singletonKey, staticData, PackedCounter.wrap(bytes32(0)), new bytes(0), fieldLayout) + abi.encodeCall( + IStoreHook.onAfterSetRecord, + (tableId, singletonKey, staticData, PackedCounter.wrap(bytes32(0)), new bytes(0), fieldLayout) + ) ); world.setRecord(tableId, singletonKey, staticData, PackedCounter.wrap(bytes32(0)), new bytes(0), fieldLayout); - // Expect the hook to be notified when a field is written (once before and once after the field is written) + // Expect the hook to be notified when a static field is written (once before and once after the field is written) vm.expectEmit(true, true, true, true); - emit HookCalled(abi.encode(tableId, singletonKey, uint8(0), staticData, fieldLayout)); + emit HookCalled( + abi.encodeCall( + IStoreHook.onBeforeSpliceStaticData, + (tableId, singletonKey, 0, uint40(staticData.length), staticData) + ) + ); vm.expectEmit(true, true, true, true); - emit HookCalled(abi.encode(tableId, singletonKey, uint8(0), staticData, fieldLayout)); + emit HookCalled( + abi.encodeCall( + IStoreHook.onAfterSpliceStaticData, + (tableId, singletonKey, 0, uint40(staticData.length), staticData) + ) + ); world.setField(tableId, singletonKey, 0, staticData, fieldLayout); // Expect the hook to be notified when a record is deleted (once before and once after the field is written) vm.expectEmit(true, true, true, true); - emit HookCalled(abi.encode(tableId, singletonKey, fieldLayout)); + emit HookCalled(abi.encodeCall(IStoreHook.onBeforeDeleteRecord, (tableId, singletonKey, fieldLayout))); vm.expectEmit(true, true, true, true); - emit HookCalled(abi.encode(tableId, singletonKey, fieldLayout)); + emit HookCalled(abi.encodeCall(IStoreHook.onAfterDeleteRecord, (tableId, singletonKey, fieldLayout))); world.deleteRecord(tableId, singletonKey, fieldLayout); }