Skip to content

Commit

Permalink
feat(world): simplify access control to namespaces instead of routes (#…
Browse files Browse the repository at this point in the history
…467)

* feat(world): add registerNamespace

* feat(world): use namespace for registerTable

* refactor(world): replace all route based logic with namespace based logic

* feat(store): make error logs more expressive

* fix(world): fix setMetadata

* chore(world): remove unused tables/schemas

* feat(world): add ResourceType and prevent duplicate resource selectors

* feat(cli): update deploy-v2 and tablegen to namespaces

* chore: update tablegen and gas-reports

* feat(store): change schema table id to cleartext id

---------

Co-authored-by: dk1a <dk1a@protonmail.com>
  • Loading branch information
alvrs and dk1a authored Mar 9, 2023
1 parent 9cc89ac commit 945f2ef
Show file tree
Hide file tree
Showing 42 changed files with 1,442 additions and 1,331 deletions.
2 changes: 1 addition & 1 deletion packages/cli/contracts/src/tables/Table1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { PackedCounter, PackedCounterLib } from "@latticexyz/store/src/PackedCou
// Import user types
import { Enum1, Enum2 } from "./../types.sol";

uint256 constant _tableId = uint256(keccak256("/Table1"));
uint256 constant _tableId = uint256(bytes32(abi.encodePacked(bytes16(""), bytes16("Table1"))));
uint256 constant Table1TableId = _tableId;

struct Table1Data {
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/contracts/test/Tablegen.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
pragma solidity >=0.8.0;

import "forge-std/Test.sol";
import {StoreView} from "@latticexyz/store/src/StoreView.sol";
import {Table1, Table1Data} from "../src/tables/Table1.sol";
import {Enum1, Enum2} from "../src/types.sol";
import { StoreView } from "@latticexyz/store/src/StoreView.sol";
import { Table1, Table1Data } from "../src/tables/Table1.sol";
import { Enum1, Enum2 } from "../src/types.sol";

contract TablegenTest is Test, StoreView {
function testTable1SetAndGet() public {
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/config/commonSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
validateRoute,
validateSingleLevelRoute,
validateUncapitalizedName,
validateSelector,
} from "./validation.js";

/** Capitalized names of objects, like tables and systems */
Expand All @@ -34,3 +35,6 @@ export const EthereumAddress = z.string().superRefine(validateEthereumAddress);
export const StaticSchemaType = z
.nativeEnum(SchemaType)
.refine((arg) => getStaticByteLength(arg) > 0, "SchemaType must be static");

/** A selector for namespace/file/resource */
export const Selector = z.string().superRefine(validateSelector);
25 changes: 12 additions & 13 deletions packages/cli/src/config/loadWorldConfig.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { z, ZodError } from "zod";
import { fromZodErrorCustom, UnrecognizedSystemErrorFactory } from "../utils/errors.js";
import { BaseRoute, EthereumAddress, ObjectName } from "./commonSchemas.js";
import { EthereumAddress, ObjectName, Selector } from "./commonSchemas.js";
import { loadConfig } from "./loadConfig.js";

const SystemName = ObjectName;
const SystemRoute = BaseRoute.optional();
const SystemAccessList = z.array(SystemName.or(EthereumAddress)).default([]);

// The system config is a combination of a route config and access config
// The system config is a combination of a fileSelector config and access config
const SystemConfig = z.intersection(
z.object({
route: SystemRoute,
fileSelector: Selector,
}),
z.discriminatedUnion("openAccess", [
z.object({
Expand All @@ -25,7 +24,7 @@ const SystemConfig = z.intersection(

// The parsed world config is the result of parsing the user config
export const WorldConfig = z.object({
baseRoute: BaseRoute.default(""),
namespace: Selector.default(""),
worldContractName: z.string().optional(),
overrideSystems: z.record(SystemName, SystemConfig).default({}),
excludeSystems: z.array(SystemName).default([]),
Expand All @@ -39,13 +38,13 @@ export const WorldConfig = z.object({
* @param config optional SystemConfig object, if none is provided the default config is used
* @param existingContracts optional list of existing contract names, used to validate system names in the access list. If not provided, no validation is performed.
* @returns ResolvedSystemConfig object
* Default value for route is `/${systemName}`
* Default value for fileSelector is `systemName`
* Default value for openAccess is true
* Default value for accessListAddresses is []
* Default value for accessListSystems is []
*/
export function resolveSystemConfig(systemName: string, config?: SystemUserConfig, existingContracts?: string[]) {
const route = config?.route ?? `/${systemName}`;
const fileSelector = config?.fileSelector ?? systemName;
const openAccess = config?.openAccess ?? true;
const accessListAddresses: string[] = [];
const accessListSystems: string[] = [];
Expand All @@ -64,7 +63,7 @@ export function resolveSystemConfig(systemName: string, config?: SystemUserConfi
}
}

return { route, openAccess, accessListAddresses, accessListSystems };
return { fileSelector, openAccess, accessListAddresses, accessListSystems };
}

/**
Expand Down Expand Up @@ -131,8 +130,8 @@ export async function parseWorldConfig(config: unknown) {
// zod doesn't preserve doc comments
export type SystemUserConfig =
| {
/** The system will be deployed at `baseRoute + route` */
route?: string;
/** The full resource selector consists of namespace and fileSelector */
fileSelector?: string;
} & (
| {
/** If openAccess is true, any address can call the system */
Expand All @@ -148,13 +147,13 @@ export type SystemUserConfig =

// zod doesn't preserve doc comments
export interface WorldUserConfig {
/** The base route to register tables and systems at. Defaults to the root route (empty string) */
baseRoute?: string;
/** The namespace to register tables and systems at. Defaults to the root namespace (empty string) */
namespace?: string;
/** The name of the World contract to deploy. If no name is provided, a vanilla World is deployed */
worldContractName?: string;
/**
* Contracts named *System will be deployed by default
* as public systems at `baseRoute/ContractName`, unless overridden
* as public systems at `namespace/ContractName`, unless overridden
*
* The key is the system name (capitalized).
* The value is a SystemConfig object.
Expand Down
26 changes: 15 additions & 11 deletions packages/cli/src/config/parseStoreConfig.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SchemaType } from "@latticexyz/schema-type";
import { RefinementCtx, z, ZodIssueCode } from "zod";
import { BaseRoute, ObjectName, OrdinaryRoute, StaticSchemaType, UserEnum, ValueName } from "./commonSchemas.js";
import { ObjectName, OrdinaryRoute, Selector, StaticSchemaType, UserEnum, ValueName } from "./commonSchemas.js";
import { getDuplicates } from "./validation.js";

const TableName = ObjectName;
Expand All @@ -22,7 +22,7 @@ const Schema = z
const TableDataFull = z
.object({
directory: OrdinaryRoute.default("/tables"),
route: BaseRoute.optional(),
fileSelector: Selector.optional(),
tableIdArgument: z.boolean().default(false),
storeArgument: z.boolean().default(false),
primaryKeys: PrimaryKeys,
Expand All @@ -48,18 +48,18 @@ const TableDataShorthand = FieldData.transform((fieldData) => {
});

const TablesRecord = z.record(TableName, z.union([TableDataShorthand, TableDataFull])).transform((tables) => {
// default route depends on tableName
// default fileSelector depends on tableName
for (const tableName of Object.keys(tables)) {
const table = tables[tableName];
table.route ??= `/${tableName}`;
table.fileSelector ??= tableName;

tables[tableName] = table;
}
return tables as Record<string, RequireKeys<typeof tables[string], "route">>;
return tables as Record<string, RequireKeys<(typeof tables)[string], "fileSelector">>;
});

const StoreConfigUnrefined = z.object({
baseRoute: BaseRoute.default(""),
namespace: Selector.default(""),
storeImportPath: z.string().default("@latticexyz/store/src/"),
tables: TablesRecord,
userTypes: z
Expand All @@ -74,8 +74,8 @@ export const StoreConfig = StoreConfigUnrefined.superRefine(validateStoreConfig)

// zod doesn't preserve doc comments
export interface StoreUserConfig {
/** The base route prefix for table ids. Default is "" (empty string) */
baseRoute?: string;
/** The namespace for table ids. Default is "" (empty string) */
namespace?: string;
/** Path for store package imports. Default is "@latticexyz/store/src/" */
storeImportPath?: string;
/**
Expand All @@ -95,8 +95,12 @@ export interface StoreUserConfig {
interface FullTableConfig {
/** Output directory path for the file. Default is "/tables" */
directory?: string;
/** Route is used to register the table and construct its id. The table id will be keccak256(concat(baseRoute,route)). Default is "/<tableName>" */
route?: string;
/**
* The fileSelector is used with the namespace to register the table and construct its id.
* The table id will be uint256(bytes32(abi.encodePacked(bytes16(namespace), bytes16(fileSelector)))).
* Default is "<tableName>"
* */
fileSelector?: string;
/** Make methods accept `tableId` argument instead of it being a hardcoded constant. Default is false */
tableIdArgument?: boolean;
/** Include methods that accept a manual `IStore` argument. Default is false. */
Expand Down Expand Up @@ -171,4 +175,4 @@ function validateIfUserType(
}
}

type RequireKeys<T extends Record<string, unknown>, P extends string> = T & Required<Pick<T, P>>;
type RequireKeys<T extends Record<string, unknown>, P extends string> = T & Required<Pick<T, P>>;
15 changes: 15 additions & 0 deletions packages/cli/src/config/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,18 @@ export function getDuplicates<T>(array: T[]) {
}
return [...duplicates];
}

export function validateSelector(name: string, ctx: RefinementCtx) {
if (name.length > 16) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: `Selector must be <= 16 characters`,
});
}
if (!/^\w*$/.test(name)) {
ctx.addIssue({
code: ZodIssueCode.custom,
message: `Selector must contain only alphanumeric & underscore characters`,
});
}
}
6 changes: 3 additions & 3 deletions packages/cli/src/render-solidity/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ export function renderArguments(args: (string | undefined)[]) {
return internalRenderList(",", filteredArgs, (arg) => arg);
}

export function renderCommonData({ staticRouteData, primaryKeys }: RenderTableOptions) {
export function renderCommonData({ staticResourceData, primaryKeys }: RenderTableOptions) {
// static route means static tableId as well, and no tableId arguments
const _tableId = staticRouteData ? "" : "_tableId";
const _typedTableId = staticRouteData ? "" : "uint256 _tableId";
const _tableId = staticResourceData ? "" : "_tableId";
const _typedTableId = staticResourceData ? "" : "uint256 _tableId";

const _keyArgs = renderArguments(primaryKeys.map(({ name }) => name));
const _typedKeyArgs = renderArguments(primaryKeys.map(({ name, typeWithLocation }) => `${typeWithLocation} ${name}`));
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/src/render-solidity/renderTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { renderRecordMethods } from "./record.js";
import { RenderTableOptions } from "./types.js";

export function renderTable(options: RenderTableOptions) {
const { imports, libraryName, structName, staticRouteData, storeImportPath, fields, withRecordMethods } = options;
const { imports, libraryName, structName, staticResourceData, storeImportPath, fields, withRecordMethods } = options;

const { _typedTableId, _typedKeyArgs, _primaryKeysDefinition } = renderCommonData(options);

Expand Down Expand Up @@ -33,11 +33,11 @@ ${
}
${
!staticRouteData
!staticResourceData
? ""
: `
uint256 constant _tableId = uint256(keccak256("${staticRouteData.baseRoute + staticRouteData.subRoute}"));
uint256 constant ${staticRouteData.tableIdName} = _tableId;
uint256 constant _tableId = uint256(bytes32(abi.encodePacked(bytes16("${staticResourceData.namespace}"), bytes16("${staticResourceData.fileSelector}"))));
uint256 constant ${staticResourceData.tableIdName} = _tableId;
`
}
Expand Down
10 changes: 5 additions & 5 deletions packages/cli/src/render-solidity/renderTablesFromConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,15 @@ export function renderTablesFromConfig(config: StoreConfig, srcDirectory: string
const dynamicFields = fields.filter(({ isDynamic }) => isDynamic) as RenderTableDynamicField[];

// With tableIdArgument: tableId is a dynamic argument for each method
// Without tableIdArgument: tableId is a file-level constant generated from `staticRouteData`
const staticRouteData = (() => {
// Without tableIdArgument: tableId is a file-level constant generated from `staticResourceData`
const staticResourceData = (() => {
if (tableData.tableIdArgument) {
return;
} else {
return {
tableIdName: tableName + "TableId",
baseRoute: config.baseRoute,
subRoute: tableData.route,
namespace: config.namespace,
fileSelector: tableData.fileSelector,
};
}
})();
Expand All @@ -94,7 +94,7 @@ export function renderTablesFromConfig(config: StoreConfig, srcDirectory: string
imports,
libraryName: tableName,
structName: withStruct ? tableName + "Data" : undefined,
staticRouteData,
staticResourceData,
storeImportPath,
primaryKeys,
fields,
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/src/render-solidity/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface RenderTableOptions {
/** Name of the struct to render. If undefined, struct and its methods aren't rendered. */
structName?: string;
/** Data used to statically registed the table. If undefined, all methods receive `_tableId` as an argument. */
staticRouteData?: StaticRouteData;
staticResourceData?: StaticResourceData;
/** Path for store package imports */
storeImportPath: string;
primaryKeys: RenderTablePrimaryKey[];
Expand All @@ -24,11 +24,11 @@ export interface ImportDatum {
path: string;
}

export interface StaticRouteData {
export interface StaticResourceData {
/** Name of the table id constant to render. */
tableIdName: string;
baseRoute: string;
subRoute: string;
namespace: string;
fileSelector: string;
}

export interface RenderTableType {
Expand Down
Loading

0 comments on commit 945f2ef

Please sign in to comment.