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 autogeneration for non-root modules #12

Open
MerkleBoy opened this issue Feb 27, 2024 · 1 comment
Open

Library autogeneration for non-root modules #12

MerkleBoy opened this issue Feb 27, 2024 · 1 comment

Comments

@MerkleBoy
Copy link
Contributor

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 MerkleBoy changed the title Library autogeneration Library autogeneration for non-root modules Feb 27, 2024
@MerkleBoy
Copy link
Contributor Author

MerkleBoy commented Feb 27, 2024

posted the issue on latticexyz/mud too since it can be generalized to a MUD feature
=> Library auto-generation for non-root Modules #2319

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

No branches or pull requests

1 participant