latticexyz / mud

MUD is a framework for building autonomous worlds
https://mud.dev
MIT License
753 stars 187 forks source link

Modular systems / system to system calls #319

Closed alvrs closed 1 year ago

alvrs commented 1 year ago

Problem description

Systems encapsulate logic. It would be nice if it was possible to use systems as modular building blocks to create higher level systems, but when a system is called from another system, any access control logic related to msg.sender fails, because the calling system is the new msg.sender.

A common workaround is to encapsulate logic in libraries instead, use those libraries as building blocks and use systems only as entry points to the application. However, this comes with two issues:

  1. It is hard to keep track of which components the libraries need write access to. Write access control to components happens on the level of systems, not libraries, therefore every system using a library needs to be granted write access to the components the library writes to.
  2. This workaround works for development, when the deployer of the components and systems is the same, so systems can be granted write access to any component. But it is not possible for third party developers to use the logic encapsulated in libraries as building blocks for their own higher-level systems (because they're lacking write access to the required components).

Example of a situation where it would be nice to use systems as building blocks, but we run into the issue of msg.sender being different:

Approaches

1. ~access control via tx.origin~

2. Subsystems

3. Global entry point

dk1a commented 1 year ago

Back when I made #268 I myself wasn't sure subsystems were useful. Now I actually have a bunch of examples, so here's an updated pitch.

System to system calls aren't common. Small projects wouldn't need them at all. Libraries are great, use them whenever possible.

Open this deploy.json, scroll down to CycleCombatSystem. It uses some components and CombatSubsystem, which in turn uses some components, DurationSubsystem and EffectSubsystem All these subsystems are also used by other (sub)systems, not just CycleCombatSystem. (See #303 for details on EffectSubsystem and DurationSubsystem and callbacks and them using each other)

What's a subsystem? Just a system that inherits OwnableWritable, whereas normally systems inherit only Ownable. Subsystems are systems.

Why do I need OwnableWritable? I don't want users to call subsystems. They're basically stateful libraries. Components already have authorizeWriter, lets reuse it.

Why not just use Systems then?

  1. You'd have to add OwnableWritable (authorizeWriter etc) to the required systems yourself. Inconvenient boilerplate.
  2. writeAccess in deploy.json. Subsystems as primitves can be built into cli. You don't wanna modify LibDeploy.ejs manually every time.
  3. Subsystem is a different name. It makes it clear at glance what's external and what's internal.

Why not just use libraries?

  1. Often I do.
  2. Can't, contracts have a size limit.
  3. Callbacks (see #303) are impossible with libs.
  4. I don't even know how many components CycleCombatSystem uses if you unroll the subsystems. Manually managing huge writeAccesses is very inconvenient.
  5. Libraries don't have writeAccess. Subsystems do. You can easily see which components/subsystems a subsystem writes to. You have to read the whole library to know what it writes to.
  6. Statefulness can be convenient. I don't need to pass world or components to a subsystem.

Subsystem examples: ERC721BaseSubsystem (a special case, this one can't be a library not for any of the above reasons, but because it has to be stateful due to events) ERC1155BaseSubsystem (same thing) ScopedDurationSubsystem EffectSubsystem CombatSubsystem (this is a perfect example of "Can't, contracts have a size limit". It also has 2 dedicated libs, to keep it less bloated) EquipmentSubsystem (btw this one isn't aware of items or characters or anything. It's a really abstract utility to equip stuff to slots) RandomMapSubsystem (this is an example of a subsystem I think may be better of as a library instead, I'm not sure yet)

alvrs commented 1 year ago

@dk1a thanks for the updated pitch / explanation of subsystems and how they compare to regular systems and libraries. I just updated the issue description with a more detailed problem description and potential solutions.

We can definitely agree on the usefulness of "stateful libraries" or calling systems from other systems. In my opinion the subsystem approach perfectly solves the "first party" issues (adding the possibility for stateful libraries for a developer's own systems), but I'd like to think about whether we can come up with a solution that would give the same benefits to third party developers (using existing systems as modules to create more complex higher level systems).

Curious to hear your thoughts on this.

dk1a commented 1 year ago

@alvrs Interesting, I've never thought of that side of the problem in relation to subsystems. I haven't really gotten there yet with my game (I just use msg.sender atm), but what I've been planning is to have more of a bottom-up approach. So just don't use simple permissions.

Lets say you have a character entity, and a MoveSystem. Then you have an ApprovalComponent with mapping(characterEntity=>approvedAddress=>approvedSystem) (The implementation details are hazy, the mapping can be done e.g. by hashing 2 of 3 or something like that) Then you have some convenient utility to set and check generic permissions (it can check msg.sender too). Then if you wanna use PathSystem, call something like approve(getAddressById(PathSystemID), MoveSystemID, charEntity) Maybe add some utility to recs or std-client that can largely automate this.

approvedAddress instead of approvedSystemId maybe avoids upgradeability phishing, but not really (it could call an upgradeable system, so is it even worth it?)

my details may be off, haven't actually written any code for this

dk1a commented 1 year ago

The main point of that whole approval thing is, it's ok if you get phished. You character will just move incorrectly for a while. The attacker doesn't get full permission to do whatever

dk1a commented 1 year ago

Kind of related: https://blog.curio.gg/how-we-built-this-treaty-technical-overview/

alvrs commented 1 year ago

Very interesting idea! It got me thinking about how we could use a general approval pattern to solve this issue (modular systems) as well as the session wallet issue (to move on from pure burner wallets). Created a proposal in #327, lmk what you think!

alvrs commented 1 year ago

Closing this since it's pre v2 (we basically implemented subsystems, and the global entry point is part of #1124)