From cac35f5c1fde4ac68444c88efd0222e4eaecbdfd Mon Sep 17 00:00:00 2001 From: alvrs Date: Mon, 13 Mar 2023 19:34:00 -0500 Subject: [PATCH] feat(world): add InstalledModules table to store installed modules --- packages/world/gas-report.txt | 20 +-- packages/world/mud.config.mts | 14 +++ packages/world/src/World.sol | 18 +++ packages/world/src/constants.sol | 4 + packages/world/src/interfaces/IErrors.sol | 1 + packages/world/src/interfaces/IModule.sol | 7 ++ .../world/src/modules/core/CoreModule.sol | 12 +- .../registration/RegistrationModule.sol | 16 ++- .../registration/RegistrationSystem.sol | 2 +- .../world/src/tables/InstalledModules.sol | 119 ++++++++++++++++++ 10 files changed, 199 insertions(+), 14 deletions(-) create mode 100644 packages/world/src/tables/InstalledModules.sol diff --git a/packages/world/gas-report.txt b/packages/world/gas-report.txt index ed24481094..dcac38cd5a 100644 --- a/packages/world/gas-report.txt +++ b/packages/world/gas-report.txt @@ -1,11 +1,11 @@ -(test/World.t.sol) | Delete record [world.deleteRecord(namespace, file, singletonKey)]: 16038 -(test/World.t.sol) | Push data to the table [world.pushToField(namespace, file, keyTuple, 0, encodedData)]: 96409 -(test/World.t.sol) | Register a fallback system [bytes4 funcSelector1 = world.registerFunctionSelector(namespace, file, "", "")]: 80937 -(test/World.t.sol) | Register a root fallback system [bytes4 funcSelector2 = world.registerRootFunctionSelector(namespace, file, worldFunc, 0)]: 72163 -(test/World.t.sol) | Register a function selector [bytes4 functionSelector = world.registerFunctionSelector(namespace, file, "msgSender", "()")]: 101534 -(test/World.t.sol) | Register a new namespace [world.registerNamespace("test")]: 151628 -(test/World.t.sol) | Register a root function selector [bytes4 functionSelector = world.registerRootFunctionSelector(namespace, file, worldFunc, sysFunc)]: 96069 -(test/World.t.sol) | Register a new table in the namespace [bytes32 tableSelector = world.registerTable(namespace, table, schema, defaultKeySchema)]: 252155 -(test/World.t.sol) | Write data to a table field [world.setField(namespace, file, singletonKey, 0, abi.encodePacked(true))]: 44726 -(test/World.t.sol) | Set metadata [world.setMetadata(namespace, file, tableName, fieldNames)]: 277162 +(test/World.t.sol) | Delete record [world.deleteRecord(namespace, file, singletonKey)]: 16026 +(test/World.t.sol) | Push data to the table [world.pushToField(namespace, file, keyTuple, 0, encodedData)]: 96397 +(test/World.t.sol) | Register a fallback system [bytes4 funcSelector1 = world.registerFunctionSelector(namespace, file, "", "")]: 80940 +(test/World.t.sol) | Register a root fallback system [bytes4 funcSelector2 = world.registerRootFunctionSelector(namespace, file, worldFunc, 0)]: 72166 +(test/World.t.sol) | Register a function selector [bytes4 functionSelector = world.registerFunctionSelector(namespace, file, "msgSender", "()")]: 101537 +(test/World.t.sol) | Register a new namespace [world.registerNamespace("test")]: 151631 +(test/World.t.sol) | Register a root function selector [bytes4 functionSelector = world.registerRootFunctionSelector(namespace, file, worldFunc, sysFunc)]: 96072 +(test/World.t.sol) | Register a new table in the namespace [bytes32 tableSelector = world.registerTable(namespace, table, schema, defaultKeySchema)]: 252158 +(test/World.t.sol) | Write data to a table field [world.setField(namespace, file, singletonKey, 0, abi.encodePacked(true))]: 44714 +(test/World.t.sol) | Set metadata [world.setMetadata(namespace, file, tableName, fieldNames)]: 277165 (test/World.t.sol) | Write data to the table [Bool.set(tableId, world, true)]: 42598 \ No newline at end of file diff --git a/packages/world/mud.config.mts b/packages/world/mud.config.mts index 9f57100a39..70196643ce 100644 --- a/packages/world/mud.config.mts +++ b/packages/world/mud.config.mts @@ -78,6 +78,20 @@ const config: StoreUserConfig = { storeArgument: true, tableIdArgument: true, }, + InstalledModules: { + primaryKeys: { + namespace: SchemaType.BYTES16, + mdouleName: SchemaType.BYTES16, + }, + schema: { + moduleAddress: SchemaType.ADDRESS, + }, + // TODO: this is a workaround to use `getRecord` instead of `getField` in the autogen library, + // to allow using the table before it is registered. This is because `getRecord` passes the schema + // to store, while `getField` loads it from storage. Remove this once we have support for passing the + // schema in `getField` too. + dataStruct: true, + }, }, userTypes: { enums: { diff --git a/packages/world/src/World.sol b/packages/world/src/World.sol index e6f03676d9..d0368149e6 100644 --- a/packages/world/src/World.sol +++ b/packages/world/src/World.sol @@ -17,6 +17,7 @@ import { NamespaceOwner } from "./tables/NamespaceOwner.sol"; import { ResourceAccess } from "./tables/ResourceAccess.sol"; import { Systems } from "./tables/Systems.sol"; import { FunctionSelectors } from "./tables/FunctionSelectors.sol"; +import { InstalledModules } from "./tables/InstalledModules.sol"; import { IModule } from "./interfaces/IModule.sol"; import { IWorldCore } from "./interfaces/IWorldCore.sol"; @@ -43,12 +44,20 @@ contract World is Store, IWorldCore, IErrors { * Install the given module at the given namespace in the World. */ function installModule(IModule module, bytes16 namespace) public { + // Prevent the same module from being installed twice in the same namespace + if (InstalledModules.get(ROOT_NAMESPACE, module.getName()).moduleAddress != address(0)) { + revert ModuleAlreadyInstalled(ResourceSelector.from(namespace, module.getName()).toString()); + } + Call.withSender({ msgSender: msg.sender, target: address(module), funcSelectorAndArgs: abi.encodeWithSelector(IModule.install.selector, namespace), delegate: false }); + + // Register the module in the InstalledModules table + InstalledModules.set(namespace, module.getName(), address(module)); } /** @@ -58,12 +67,21 @@ contract World is Store, IWorldCore, IErrors { */ function installRootModule(IModule module) public { AccessControl.requireOwner(ROOT_NAMESPACE, ROOT_FILE, msg.sender); + + // Prevent the same module from being installed twice in the same namespace + if (InstalledModules.get(ROOT_NAMESPACE, module.getName()).moduleAddress != address(0)) { + revert ModuleAlreadyInstalled(ResourceSelector.from(ROOT_NAMESPACE, module.getName()).toString()); + } + Call.withSender({ msgSender: msg.sender, target: address(module), funcSelectorAndArgs: abi.encodeWithSelector(IModule.install.selector, ROOT_NAMESPACE), delegate: true // The module is delegate called so it can edit any table }); + + // Register the module in the InstalledModules table + InstalledModules.set(ROOT_NAMESPACE, module.getName(), address(module)); } /************************************************************************ diff --git a/packages/world/src/constants.sol b/packages/world/src/constants.sol index 2605133ba6..1eb7993ae1 100644 --- a/packages/world/src/constants.sol +++ b/packages/world/src/constants.sol @@ -3,3 +3,7 @@ pragma solidity >=0.8.0; bytes16 constant ROOT_NAMESPACE = 0; bytes16 constant ROOT_FILE = 0; + +// World modules +bytes16 constant CORE_MODULE_NAME = bytes16("core"); +bytes16 constant REGISTRATION_MODULE_NAME = bytes16("registration"); diff --git a/packages/world/src/interfaces/IErrors.sol b/packages/world/src/interfaces/IErrors.sol index fd3afadd15..d9c1bd611f 100644 --- a/packages/world/src/interfaces/IErrors.sol +++ b/packages/world/src/interfaces/IErrors.sol @@ -9,4 +9,5 @@ interface IErrors { error SystemExists(address system); error FunctionSelectorExists(bytes4 functionSelector); error FunctionSelectorNotFound(bytes4 functionSelector); + error ModuleAlreadyInstalled(string module); } diff --git a/packages/world/src/interfaces/IModule.sol b/packages/world/src/interfaces/IModule.sol index 4f15476a75..64e3aee182 100644 --- a/packages/world/src/interfaces/IModule.sol +++ b/packages/world/src/interfaces/IModule.sol @@ -3,6 +3,13 @@ pragma solidity >=0.8.0; import { IWorldCore } from "./IWorldCore.sol"; interface IModule { + error RequiredModuleNotFound(string resourceSelector); + + /** + * Return the module name as a bytes16. + */ + function getName() external view returns (bytes16 name); + /** * A module expects to be called via the World contract, and therefore installs itself on its `msg.sender`. */ diff --git a/packages/world/src/modules/core/CoreModule.sol b/packages/world/src/modules/core/CoreModule.sol index 46d2927f67..4c9ece6ce7 100644 --- a/packages/world/src/modules/core/CoreModule.sol +++ b/packages/world/src/modules/core/CoreModule.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity >=0.8.0; -import { ROOT_NAMESPACE } from "../../constants.sol"; +import { ROOT_NAMESPACE, CORE_MODULE_NAME } from "../../constants.sol"; import { WithMsgSender } from "../../WithMsgSender.sol"; import { IModule } from "../../interfaces/IModule.sol"; @@ -10,6 +10,7 @@ import { NamespaceOwner } from "../../tables/NamespaceOwner.sol"; import { ResourceAccess } from "../../tables/ResourceAccess.sol"; import { Systems } from "../../tables/Systems.sol"; import { FunctionSelectors } from "../../tables/FunctionSelectors.sol"; +import { InstalledModules } from "../../tables/InstalledModules.sol"; /** * The CoreModule registers internal World tables. @@ -21,9 +22,16 @@ import { FunctionSelectors } from "../../tables/FunctionSelectors.sol"; * added in `RegistrationModule`, which is installed after `CoreModule`. */ contract CoreModule is IModule, WithMsgSender { - function install(bytes16) external override { + function getName() public pure returns (bytes16) { + return CORE_MODULE_NAME; + } + + function install(bytes16) public override { NamespaceOwner.setMetadata(); + InstalledModules.registerSchema(); + InstalledModules.setMetadata(); + ResourceAccess.registerSchema(); ResourceAccess.setMetadata(); ResourceAccess.set(ROOT_NAMESPACE, _msgSender(), true); diff --git a/packages/world/src/modules/registration/RegistrationModule.sol b/packages/world/src/modules/registration/RegistrationModule.sol index 22989e0af7..6053b01917 100644 --- a/packages/world/src/modules/registration/RegistrationModule.sol +++ b/packages/world/src/modules/registration/RegistrationModule.sol @@ -4,12 +4,15 @@ pragma solidity >=0.8.0; import { RegistrationSystem } from "./RegistrationSystem.sol"; import { Call } from "../../Call.sol"; -import { ROOT_NAMESPACE } from "../../constants.sol"; +import { ROOT_NAMESPACE, REGISTRATION_MODULE_NAME } from "../../constants.sol"; import { WithMsgSender } from "../../WithMsgSender.sol"; import { Resource } from "../../types.sol"; +import { ResourceSelector } from "../../ResourceSelector.sol"; import { IModule } from "../../interfaces/IModule.sol"; +import { InstalledModules } from "../../tables/InstalledModules.sol"; + import { ResourceType } from "./tables/ResourceType.sol"; import { SystemRegistry } from "./tables/SystemRegistry.sol"; @@ -24,13 +27,24 @@ import { SystemRegistry } from "./tables/SystemRegistry.sol"; * If the module is delegatecalled, the StoreCore functions are used directly. */ contract RegistrationModule is IModule, WithMsgSender { + using ResourceSelector for bytes32; + // Since the RegistrationSystem only exists once per World and writes to // known tables, we can deploy it once and register it in multiple Worlds. address immutable registrationSystem = address(new RegistrationSystem()); bytes16 immutable registrationSystemFile = bytes16("registration"); + function getName() public pure returns (bytes16) { + return REGISTRATION_MODULE_NAME; + } + // The namespace argument is not used because the module is always installed in the root namespace function install(bytes16) public { + // Require the CoreModule to be installed in the root namespace + if (InstalledModules.get(ROOT_NAMESPACE, bytes16("core")).moduleAddress == address(0)) { + revert RequiredModuleNotFound(ResourceSelector.from(ROOT_NAMESPACE, bytes16("core")).toString()); + } + // Register tables required by RegistrationSystem SystemRegistry.registerSchema(); SystemRegistry.setMetadata(); diff --git a/packages/world/src/modules/registration/RegistrationSystem.sol b/packages/world/src/modules/registration/RegistrationSystem.sol index b58fd17178..c9b7711a5f 100644 --- a/packages/world/src/modules/registration/RegistrationSystem.sol +++ b/packages/world/src/modules/registration/RegistrationSystem.sol @@ -20,7 +20,7 @@ import { Systems } from "../../tables/Systems.sol"; import { FunctionSelectors } from "../../tables/FunctionSelectors.sol"; import { ISystemHook } from "../../interfaces/ISystemHook.sol"; -import { IErrors } from "../interfaces/IErrors.sol"; +import { IErrors } from "../../interfaces/IErrors.sol"; import { IRegistrationSystem } from "../../interfaces/systems/IRegistrationSystem.sol"; contract RegistrationSystem is System, IRegistrationSystem, IErrors { diff --git a/packages/world/src/tables/InstalledModules.sol b/packages/world/src/tables/InstalledModules.sol new file mode 100644 index 0000000000..9b62563d23 --- /dev/null +++ b/packages/world/src/tables/InstalledModules.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +/* Autogenerated file. Do not edit manually. */ + +// Import schema type +import { SchemaType } from "@latticexyz/schema-type/src/solidity/SchemaType.sol"; + +// Import store internals +import { IStore } from "@latticexyz/store/src/IStore.sol"; +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; +import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; +import { Bytes } from "@latticexyz/store/src/Bytes.sol"; +import { SliceLib } from "@latticexyz/store/src/Slice.sol"; +import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; +import { Schema, SchemaLib } from "@latticexyz/store/src/Schema.sol"; +import { PackedCounter, PackedCounterLib } from "@latticexyz/store/src/PackedCounter.sol"; + +uint256 constant _tableId = uint256(bytes32(abi.encodePacked(bytes16(""), bytes16("InstalledModules")))); +uint256 constant InstalledModulesTableId = _tableId; + +struct InstalledModulesData { + address moduleAddress; +} + +library InstalledModules { + /** Get the table's schema */ + function getSchema() internal pure returns (Schema) { + SchemaType[] memory _schema = new SchemaType[](1); + _schema[0] = SchemaType.ADDRESS; + + return SchemaLib.encode(_schema); + } + + function getKeySchema() internal pure returns (Schema) { + SchemaType[] memory _schema = new SchemaType[](2); + _schema[0] = SchemaType.BYTES16; + _schema[1] = SchemaType.BYTES16; + + return SchemaLib.encode(_schema); + } + + /** Get the table's metadata */ + function getMetadata() internal pure returns (string memory, string[] memory) { + string[] memory _fieldNames = new string[](1); + _fieldNames[0] = "moduleAddress"; + return ("InstalledModules", _fieldNames); + } + + /** Register the table's schema */ + function registerSchema() internal { + StoreSwitch.registerSchema(_tableId, getSchema(), getKeySchema()); + } + + /** Set the table's metadata */ + function setMetadata() internal { + (string memory _tableName, string[] memory _fieldNames) = getMetadata(); + StoreSwitch.setMetadata(_tableId, _tableName, _fieldNames); + } + + /** Get moduleAddress */ + function getModuleAddress(bytes16 namespace, bytes16 mdouleName) internal view returns (address moduleAddress) { + bytes32[] memory _primaryKeys = new bytes32[](2); + _primaryKeys[0] = bytes32((namespace)); + _primaryKeys[1] = bytes32((mdouleName)); + + bytes memory _blob = StoreSwitch.getField(_tableId, _primaryKeys, 0); + return (address(Bytes.slice20(_blob, 0))); + } + + /** Set moduleAddress */ + function setModuleAddress(bytes16 namespace, bytes16 mdouleName, address moduleAddress) internal { + bytes32[] memory _primaryKeys = new bytes32[](2); + _primaryKeys[0] = bytes32((namespace)); + _primaryKeys[1] = bytes32((mdouleName)); + + StoreSwitch.setField(_tableId, _primaryKeys, 0, abi.encodePacked((moduleAddress))); + } + + /** Get the full data */ + function get(bytes16 namespace, bytes16 mdouleName) internal view returns (InstalledModulesData memory _table) { + bytes32[] memory _primaryKeys = new bytes32[](2); + _primaryKeys[0] = bytes32((namespace)); + _primaryKeys[1] = bytes32((mdouleName)); + + bytes memory _blob = StoreSwitch.getRecord(_tableId, _primaryKeys, getSchema()); + return decode(_blob); + } + + /** Set the full data using individual values */ + function set(bytes16 namespace, bytes16 mdouleName, address moduleAddress) internal { + bytes memory _data = abi.encodePacked(moduleAddress); + + bytes32[] memory _primaryKeys = new bytes32[](2); + _primaryKeys[0] = bytes32((namespace)); + _primaryKeys[1] = bytes32((mdouleName)); + + StoreSwitch.setRecord(_tableId, _primaryKeys, _data); + } + + /** Set the full data using the data struct */ + function set(bytes16 namespace, bytes16 mdouleName, InstalledModulesData memory _table) internal { + set(namespace, mdouleName, _table.moduleAddress); + } + + /** Decode the tightly packed blob using this table's schema */ + function decode(bytes memory _blob) internal pure returns (InstalledModulesData memory _table) { + _table.moduleAddress = (address(Bytes.slice20(_blob, 0))); + } + + /* Delete all data for given keys */ + function deleteRecord(bytes16 namespace, bytes16 mdouleName) internal { + bytes32[] memory _primaryKeys = new bytes32[](2); + _primaryKeys[0] = bytes32((namespace)); + _primaryKeys[1] = bytes32((mdouleName)); + + StoreSwitch.deleteRecord(_tableId, _primaryKeys); + } +}