Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(world): simplify access control to namespaces instead of routes #467

Merged
merged 27 commits into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2e4d394
feat(world): add registerNamespace
alvrs Mar 7, 2023
3148139
feat(world): use namespace for registerTable
alvrs Mar 7, 2023
727dd07
refactor(world): replace all route based logic with namespace based l…
alvrs Mar 7, 2023
989680c
test(world): update tests to use namespaces (wip)
alvrs Mar 8, 2023
cf1ccd4
feat(store): make error logs more expressive
alvrs Mar 8, 2023
40245f9
test(world): fix testConstructor
alvrs Mar 8, 2023
a0e907c
fix(world): fix setMetadata
alvrs Mar 8, 2023
7457f24
test(world): fix testRegisterSystem test
alvrs Mar 8, 2023
1e7fea6
test(world): fix testRegisterTable
alvrs Mar 8, 2023
ba78b77
Merge branch 'main' into alvrs/namespaces
alvrs Mar 8, 2023
dcf6e57
chore(world): remove unused tables/schemas
alvrs Mar 8, 2023
8f38145
chore(world): add TODO comment
alvrs Mar 8, 2023
5173d5b
feat(world): add ResourceTable and prevent duplicate resource selectors
alvrs Mar 8, 2023
4435185
feat(cli): update deploy-v2 and tablegen to namespaces
alvrs Mar 9, 2023
cb0e850
refactor(world): using ResourceSelector for bytes32
alvrs Mar 9, 2023
79af020
refactor(world): using ResourceSelector for bytes32 in tests
alvrs Mar 9, 2023
25cc03f
chore: update tablegen and gas-reports
alvrs Mar 9, 2023
97f785f
chore: self-review
alvrs Mar 9, 2023
5f57381
Merge branch 'main' into alvrs/namespaces
alvrs Mar 9, 2023
fd01e9c
chore: update codegen files in cli
alvrs Mar 9, 2023
7cc5841
docs(world): remove redundant comment
alvrs Mar 9, 2023
3d6396a
refactor(store): using TableId for uint256
alvrs Mar 9, 2023
fa48ffa
feat(store): change schema table id to cleartext id
alvrs Mar 9, 2023
7bf2d0e
chore: update gas-report
alvrs Mar 9, 2023
23f268c
chore: update comment
alvrs Mar 9, 2023
e2787e0
feat(store): set namespace store tables to mudstore
alvrs Mar 9, 2023
941e72f
Merge branch 'main' into alvrs/namespaces
alvrs Mar 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"))));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does abi.encodePacked leave padding for an empty bytes16?

Copy link
Member

@holic holic Mar 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also are we able to make library calls when building these constants? it would be nice to do like

uint256 constant _tableId = TableId.from("", "Table1");

and if not, maybe a uint type alias?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does abi.encodePacked leave padding for an empty bytes16?

yes, bytes16 always takes 16 bytes in abi.encodePacked, independent of its content

also are we able to make library calls when building these constants?

Unfortunately not - we're lucky abi.encodePacked is an exception that is considered "constant", we can't even do bitshifts in constants

and if not, maybe a uint type alias?

Like type TableId is uint256? I think we could do that. Can you elaborate on why you'd prefer that over just uint256?

Copy link
Member

@holic holic Mar 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the type alias wouldn't matter if we can't call methods on that alias to help construct the ID at definition time. So it's not much better.

Though a type alias might be more useful when importing the constant and using the table ID in things like logging. If we pass around table IDs as TableId, then those reverts you have here could be

revert TableNotFound(tableId.unwrap(), tableId.toString());

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The slightly annoying thing with a custom TableId type is uint256 can't be implicitly converted to TableId, so we'd have to do TableId.wrap(tableId) in all consuming libraries that call any Store function. For simplicity my preference would be to keep it uint256.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay think I found a good compromise: with a library TableId and using TableId for uint256 in StoreCore we can do tableId.toString(), without the overhead of a custom type

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this affect all uints? Wondering if we should be more explicit like castToString.

Copy link
Member Author

@alvrs alvrs Mar 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this means inside of StoreCore you could call the toString method on all uint256, yes. But it's only local to StoreCore and as long as we don't do that it should not be a problem (I don't see any other reason to call toString here). We can also just use TableId.toString and remove using for if you think it's an issue.

Copy link
Member

@holic holic Mar 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just personally used toString on uint specifically to translate e.g. 123 into "123". Same is true for a couple of common Solidity libraries: https://github.com/transmissions11/solmate/blob/main/src/utils/LibString.sol#L4-L8

So I'm just a little wary of overlapping naming. I'll personally need to make a mental leap to remind myself that this is more of a cast operation.

But not blocking, since this is internal!

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