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

Library auto-generation for non-root Modules #2319

Open
MerkleBoy opened this issue Feb 27, 2024 · 4 comments · May be fixed by #2461
Open

Library auto-generation for non-root Modules #2319

MerkleBoy opened this issue Feb 27, 2024 · 4 comments · May be fixed by #2461

Comments

@MerkleBoy
Copy link

MerkleBoy commented Feb 27, 2024

Below, a proposal for a steamlined way to call non-root Systems in a way that abstracts away the namespace it's deployed onto:

One of the main concern I have with registering some System methods as World-function selectors are the following:

  • First, registering those methods as World-function selectors has limits since the available space is only bytes4 (~4,294,967,295 possible function selectors), and so by the time we reach 65536 registered method in the World, the collision chance will be about 50% (birthday paradox). Even though this number seems high, it's really not that high once a given World's "builder" scene takes off.
    So, if we had a way that makes not registering function selectors at the World level a viable alternative to build and interact with it, e.g., libraries to inline world.call(SystemId, abi.encodeCall(...)) for them, it would be a preferable approach.

  • Secondly, calling methods through World-function selectors implies that, in the event a Module is registered more than once, it will break that interface. Right now, MUD's CLI auto-generates name-spaced method ( myNamespace__foo() ), so if someone was building on top of those methods, and the module ends up being redeployed somewhere else, it will break the interface.

Transforming a System interface into it's world.call(..) equivalent is pretty straightforward, right now it needs to be done by hand, which could lead to mismatching errors if said interfaces have to be updated; so following that logic we need something like mud worldgen or mud tablegen, so that libraries can be auto-generated in one command line.

mud modulegen perhaps ?

Let's study a dummy system DummySystem to see how it plays out :

export default mudConfig({
    namespace: "MySystem_v0",
    systems: {
        DummySystem: {
            name: "DummySystem,
            openAccess: true,
        },
    },
    tables: {
        MyTable: {
            keySchema: {
                a: "uint256",
            }
            valueSchema: {
                b: "uint256",
            },
            storeArgument: true,
            tableIdArgument: true,
        },
    },
});
// DummySystem.sol
contract DummySystem is System {
  function foo(uint256 a, uint256 b) public {
    MyTable.set(_namespace().myTableTableId(), a, b);
  }

  function bar(uint256 a) public returns (uint256 b) {
    return MyTable.get(_namespace().myTableId(), a);
  }
}
// Utils.sol
library Utils {
  using WorldResourceIdInstance for ResourceId;

  function dummySystemId(bytes14 namespace) internal pure returns (ResourceId) {
    return WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: namespace, name: DUMMY_SYSTEM_NAME });
  }
  
  function myTableId(bytes14 namespace) internal pure returns (ResourceId) {
    return WorldResourceIdLib.encode({ typeId: RESOURCE_TABLE, namespace: namespace, name: MY_TABLE_NAME });
  }
}
// DummyLib.sol
import { Utils } from "./Utils.sol";

interface IDummySystem {
  function foo(uint256 a, uint256 b) external;
  function bar(uint256 a) external returns (uint256 b);
}

library DummyLib {
  using Utils for bytes14;

  struct World {
    IBaseWorld iface;
    bytes14 namespace;
  }

  function foo(World memory world, uint256 a, uint256 b) internal {
    world.iface.call(world.namespace.dummySystemId(),
      abi.encodeCall(IDummySystem.foo,
        (a, b)
      )
    );
  }

  function bar(World memory world, uint256 a) internal returns (uint256 b) {
    bytes memory result = world.iface.call(world.namespace.dummySystemId(),
      abi.encodeCall(IDummySystem.bar,
        (a)
      )
    );
    return abi.decode(result, (uint256));
  }
}

From there, is is possible to import DummyLib anywhere, and call our DummySystem methods like this:

// DummyTest.t.sol
import { DummyLib } from "../src/DummyLib.sol";

contract DummyTest is Test {
  using Utils for bytes14;   
  using DummyLib for DummyLib.World;
  using WorldResourceIdInstance for ResourceId;

  IBaseWorld baseWorld;
  DummyLib.World dummy;
  DummyModule dummyModule;
  
  function setup() public {
    // Setting up a Base World and install Dummy System on it through a Module contract
    baseWorld = IBaseWorld(address(new World()));
    baseWorld.initialize(createCoreModule()); // doing some installation stuff, not getting into that it was meant to be sudo code
    DummyModule module = new DummyModule();
    baseWorld.installModule(module, abi.encode(DUMMY_NAMESPACE));
    StoreSwitch.setStoreAddress(address(baseWorld));
    
    // This is the interesting part
    dummy = DummyLib.World(baseWorld, DUMMY_NAMESPACE);
  }

  function testFoo() public {
    dummy.foo(123, 456); //just works !

    assertEq(MyTable.get(DUMMY_NAMESPACE.myTableId(), 123), 456); // true
  }

  function testBar() public {
    testFoo();
    uint2356 result = dummy.bar(123);
    assertEq(result, 456); // true
  }
}

Granted, it needs a little bit of setting things up, but it's fairly straightforward once that's done.

The issue boils down to this: come up with a code-generation script to make a library out of a given System contract ( or its interface), in the same way as shown above

The biggest advantage of doing this is that the interface for Dummy (or rather, the syntax to call Dummy's methods) becomes decoupled from the namespace it's deployed onto, which makes that system fully reusable. It's just a matter of deploying the module on your namespace, and initializing your DummyLib.World structure with the right IBaseWorld and namespace parameters.

Instead of having to use the worldgen interfaces, which are constructed with an appended namespace to each methods (which breaks it if you decide to re-deploy it on another namespace), this bypasses completely World-level function selectors, since we make a direct call to the related system instead

@MerkleBoy
Copy link
Author

fixed typos / error in the speudo code

@holic
Copy link
Member

holic commented Mar 11, 2024

MUD now supports public/linked libraries (#1910) which might help with some of this.

@dk1a dk1a linked a pull request Mar 18, 2024 that will close this issue
@dk1a
Copy link
Member

dk1a commented Mar 18, 2024

An interesting idea, I made a little proof of concept in #2461
@MerkleBoy feel free to use the code for ur own PR if you'd like, it's far from finished, and I made it to mirror the current worldgen rather than generalizing for all worlds/namespaces

@MerkleBoy
Copy link
Author

An interesting idea, I made a little proof of concept in #2461
@MerkleBoy feel free to use the code for ur own PR if you'd like, it's far from finished, and I made it to mirror the current worldgen rather than generalizing for all worlds/namespaces

Very cool to see it so soon !

We can keep the discussion there from now on, as you mentioned, there are a few technical limitations like encodeCall not being to handle overloaded methods (perhaps it could be circumvented by creating "singleton" interfaces for thoses ?), and anything related to namespacing/world

Thank you for this PoC it's looking good already 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: No status
Development

Successfully merging a pull request may close this issue.

3 participants