Introduction

MEV Plus is an innovative sidecar ecosystem designed to enhance the capabilities of consensus layer validators by facilitating communication with external applications. It introduces a modular architecture, allowing for the integration of various applications or modules, each serving a specific purpose within the MEV Plus ecosystem. This tutorial will guide you through the process of writing a module for MEV Plus, ensuring it meets the core service requirements, and making it publicly available on GitHub for the Go package manager to index.

Prerequisites

  • Basic understanding of Go programming language.
  • Go 1.20+ installed on your system.
  • A GitHub account for hosting your module.

Step 1: The MEV Plus Service Interface

Before writing your module, it’s crucial to understand the MEV Plus Service interface. Your module must implement the following methods:

  • Name() string: Returns the name of the module.

  • Start() error: Starts the module lifecycles. You can start any non-blocking microservice or internal logic here. MEV Plus would call this method after calling Configure.

  • Stop() error: Stops the module lifecycles. Implement logic to stop any microservices, goRoutines, servers, etc. This is called by MEV Plus on shutdown or if anything fails.

  • ConnectCore(coreClient *Client, pingId string) error: Connects the module to the MEV Plus core service. MEV Plus would create a secure communication client during start up and pass this to your module as the coreClient as well as a on-time generated pingId for you to use to connect your module to MEV Plus’ core communication channels. Store this coreClient in a struct/map/registry within your module in order to communicate with the core and other modules. In order for MEV Plus to relay communication securely to your module, within the ConnectCore method, you must implement a core_ping call to pass the unique pingId back to MEV Plus that on runtime no other module can spoof and is used to determine your module being up and running and securely available to communicate across the ecosystem.

// Store the coreClient provided in your service struct
ts.coreClient = coreClient

// Send a ping back to the core using the unique pingId to authenticate
err := ts.coreClient.Ping(pingId)
if err != nil {
	return err
}
  • Configure(moduleFlags common.ModuleFlags) error: Configures the module with the provided flags. Your module provides a cli command to MEV Plus with any attached flags, this is made available to a user when running MEV Plus’ CLI app. Any flags provided can be set by the user when running MEV Plus and is securely provided to each module as a string value for any flags set. Your module flags and arguments and values are never shared across modules and thus no other code, module, or operation can access private data passed as an argument to your module flags.

  • CliCommand() *cli.Command: Returns the CLI command for the module, allowing MEV Plus to parse the flags. The Command has a name and this name is required to be the unique name of your module/package this name MUST also be the name string that Name() string returns.

These methods are not exposed to other moules so the configuration, control and communication between your module and the MEV Plus ecosystem is handled securely and ONLY by the MEV Plus core.

Step 2: Prepare Your Module’s GitHub

  1. Create a GitHub Repository: Go to GitHub and create a new repository for your module. You may keep this private during development, however this should be made public once you want to release your module

  2. Obtain Your Module Package URL: This would be the github url of your repository without the https:// prefix. Eg. github.com/restaking-cloud/native-delegation-for-plus

Step 3: Writing Your Module

Create a new Go module

Initialize a new Go module by running

go mod init <module-package-url>

in your project directory. The module package url is gotten from the action in Step 2.

Implement the MEV Plus Service Interface

Create a new Go file (e.g., service.go) in the root of your repository/directory and implement the MEV Plus Service interface. The entire interface must be implemented in the same file. You may define other structs, and methods, however ensure the struct that implements the MEV Plus interface is in the same file as the interface methods.

Also give the service file a package name of which should be your module’s name. Not your module package url. In this tutorial we would be creating a module with name myTest

Here’s a basic template:

package myTest

import (
	"github.com/pon-network/mev-plus/core/common"
       coreCommon "github.com/pon-network/mev-plus/core/common"
	"github.com/urfave/cli/v2"
       "<module-package-url>/config"
)

type myTestService struct {
       
	// create a field to store your unique MEV Plus coreClient for communication

	coreClient *coreCommon.Client
	

	// Add any extra private fields for your module  here for data your want the instance to keep
}

func (ts *myTestService) Name() string {
	return config.ModuleName
}

func (ts *myTestService) Start() error {
	// Implement module start logic here
	return nil
}

func (ts *myTestService) Stop() error {
	// Implement module stop logic here
	return nil
}

func (ts *myTestService) ConnectCore(coreClient *common.Client, pingId string) error {
	
	ts.coreClient = coreClient

	err := ts.coreClient.Ping(pingId)
	if err != nil {
		return err
	}

	return nil
}

func (ts *myTestService) Configure(moduleFlags common.ModuleFlags) error {
	// Implement module configuration logic here
	return nil
}

func (ts *myTestService) CliCommand() *cli.Command {
	// Return the CLI command for your module
	return config.NewCommand()
}

The moduleName and CliCommand are returned from your config directory in your module’s code. It is recommended to define your module configurations in a config directory within your repository and import it into the root service.go file for use.

moduleFlags passed to Configure is a mapping of <set_module_flag_name> (string) to <module_flag_value> (string)

ONLY flags a user sets for your module would be passed to your module. If your module’s logic has no reason to boot up when a user has not activated the module or passed any flags, you can perform the check here to choose what happens when MEV Plus then calls Start after configuring your module with Configure.

Step 4: Writing Your Module Configuration, Cli Commands and Flags

In MEV Plus, each module can define its own CLI command and flags to customize its behavior. As best practice, has a sub-directory within your module’s root and name this config, to define your module CLI commands, flags and configuration structs or defaults.

  1. Define Your Module Name

First, define a constant for your module name. This name will be used to prefix all your module’s flags and to ensure consistency across your module’s implementation.

const ModuleName = "myTest"
  1. Write Your Module’s CLI Command

Your module’s CLI command should be defined in a function that returns a *cli.Command. This function should be part of your module’s configuration package. The CLI command should include the module name, usage text, and any flags your module requires.

// In your config, define a module cli Command

func NewCommand() *cli.Command {
    return &cli.Command{
        Name:      ModuleName,
        Usage:     "Description of what using this module cli would enable”
        UsageText: "Description of the high-level what features and functionality your module provides to a user. This would be seen when a user lists their modules on their MEV Plus software”
        Category:  strings.ReplaceAll(strings.ToUpper(ModuleName), "_", " "),       
        Flags:     TestModuleFlags(),
    }
}

// List your module flags here to attach to your cli command

func TestModuleFalgs() []cli.Flag {
 return []cli.Flag{
  TestFalg1,
       TestFlag2,
       …
 }
}
  1. Define Your Module’s Flags

Flags are used to customize the behavior of your module. Each flag should be defined with a unique name that is prefixed by the module name, a description, and optionally, a default value. Flags should also be categorized using the uppercase version of your module name, just as done in the cli.Command to make them easily identifiable in the CLI help output.

var (
    TestFalg1 = &cli.StringFlag{
        Name:     ModuleName + "." + "eth1-private-key",
        Usage:    "Description of your flag and what it is or how it is used",
        Category: strings.ReplaceAll(strings.ToUpper(ModuleName), "_", " "),
    }
    // Define other flags similarly...
)

The cli package has a broad range of flag types, from uint, boolean, url, string flags. Flags also have the option of being set from environment variables, using the EnvVars field within the flag to pass a string list of environment variable keys to pass the flag value from.

  1. Return the ModuleName and *cli.Command in the Module Methods required by MEV Plus

To ensure that your module’s name is consistently returned by both the CliCommand and the Name method, use the ModuleName constant when defining these.

func (ts *myTestService) Name() string {
    return ModuleName
}

func (ts *myTestService) CliCommand() *cli.Command {
    return NewCommand()
}

Why Flags and Categories Are Important

  • Consistency: Prefixing each flag with the module name ensures that flags are unique and easily identifiable.
  • Categorization: Using the module name as a category makes it easier for users to find and understand the flags related to your module when they run the help command.
  • Customization: Flags allow users to customize the behavior of your module according to their needs, making your module more flexible and adaptable.

Remember to keep your module’s name consistent across all its implementations and to use the module name as a category for your flags to enhance usability.

Step 5: Making Your Module Methods Executable by Other Modules in MEV Plus

To make your module’s methods executable by other modules in MEV Plus, you need to ensure that your module exposes its functionality through the service struct/interface in the previous step to implement additional methods that would be exposed.

This involves defining methods that other modules can call and ensuring that these methods are accessible to other modules through the core client.

IMPORTANT Methods are only exposed to other modules if they have been defined ON the service struct (the same struct that implements, Name, Start, Stop, ConnectCore, Configure, CliCommand) that MEV Plus identifies and uses within your module. Methods are also ONLY made accessible to other modules if they are public and not private (start with an upper case letter and not lower case).

For a method to be compatible it must return either an error or an error & a result any other return values would not be deemed as a compatible method. The number of results returned if any must be 1, and there should always be an error return.

Implement the Additional Methods on the Service Struct/Interface

This involves writing the actual logic for each method that you’ve defined in the interface.

type myTestService struct {
    coreClient *core.Client
    // Other fields as needed
}

func (ts *myTestService) ExampleMethod1(payload []apiv1.SignedValidatorRegistration) error {
    // Implement the method logic here
}

func (ts *myTestService) ExampleMethod2(slot uint64, parentHash, pubkey string) ([]spec.VersionedSignedBuilderBid, error) {
    // Implement the method logic here
}

func (ts *myTestService) ExampleMethod3(payload *commonTypes.VersionedSignedBlindedBeaconBlock) ([]commonTypes.VersionedExecutionPayloadV2WithVersionName, error) {
    // Implement the method logic here
}

func (ts *myTestService) ExampleMethod4(test_arg string) (string, error) {
    // Implement the method logic here
}

func (ts *myTestService) ExampleMethod5() (*big.Int, error) {
    // Implement the method logic here
}

You can still have private functions and internal logic by using private functions in these methods, or defining methods on any other struct that is not the MEV plus service interface within your module.

Step 6: Communicating and Executing Module Methods

To communicate and execute methods in other modules, you can use the core client’s Call, CallContext or Notify methods. These methods allow you to call methods on other modules by specifying the module name and method name in the form:

<module_name>_<method_name>

Where the first letter of the method_name is lower cased.

The coreClient.Notify, coreClient.Call and coreClient.CallContext methods are used to interact with other modules in MEV Plus, allowing one module to call methods on another module or send notifications.These methods are part of the core client’s API, which facilitates inter-module communication. Let’s break down the different arguments for each method:

coreClient.Notify

The Notify method is used to send a notification to another module without expecting a response. It’s a way to trigger an action in another module without waiting for confirmation.

Notify(ctx context.Context, method string, notifyAll bool, notificationExclusion []string, args ...interface{}) error
  • ctx context.Context: This is the context for the operation. It can be used to cancel the operation if needed.

  • method string: The name of the method to call on the target module. This should be in the format <module_name>_<method_name>.

  • notifyAll bool: If set to true, the notification will be sent to all modules that have the specified method name as a public method on their service struct. If false, the notification will only be sent to the first module that matches the method name.

  • notificationExclusion []string: A list of module names to exclude from receiving the notification. This is useful when you want to notify all modules except for certain ones.

  • args ...interface{}: The arguments to pass to the method being called. These should match the parameters defined in the method signature of the target module.

Example usage:

// Using Notify to send a notification without waiting for a response

err := ts.coreClient.Notify(context.Background(), "anotherModule_someMethod", false, nil, arg1, arg2)
if err != nil {
    // Handle error in processing a notification
}

coreClient.Call

The Call method is used to call a method on another module and wait for a response. It’s a way to request data or perform an action in another module and receive a result.

Call(result interface{}, method string, notifyAll bool, notificationExclusion []string, args ...interface{}) error
  • result interface{}: A pointer to a variable where the result of the method call will be stored. This must be a pointer so that the method’s return value can be unmarshaled into it. If you don’t need the result, you can pass nil.

  • method string: The name of the method to call on the target module. This should be in the format <module_name>_<method_name>.

  • notifyAll bool: Similar to the Notify method, this determines whether the call should notify all modules with the specified method name.

  • notificationExclusion []string: A list of module names to exclude from receiving the notification. This is useful when you want to notify all modules except for certain ones.

  • args ...interface{}: The arguments to pass to the method being called. These should match the parameters defined in the method signature of the target module.

Example usage:

// Using Call to call a method and wait for a response

var result SomeType
err := ts.coreClient.Call(&result, "anotherModule_someMethod", false, nil, arg1, arg2)
if err != nil {
    // Handle error in execution of the method
}

// Use result

coreClient.CallContext

The coreClient.CallContext method is an extension of the coreClient.Call method, providing additional control over the context in which the call is made. This method allows you to specify a context that can be used to cancel the operation if needed, making it more flexible for scenarios where you might need to manage timeouts or other context-specific behaviors.

CallContext(ctx context.Context, result interface{}, method string, notifyAll bool, notificationExclusion []string, args ...interface{}) error
  • ctx context.Context: This is the context for the operation. It can be used to cancel the operation if needed. This context can also carry deadlines, timeouts, and other request-scoped values across API boundaries and between processes.

  • result interface{}: A pointer to a variable where the result of the method call will be stored. This must be a pointer so that the method’s return value can be unmarshaled into it. If you don’t need the result, you can pass nil.

  • method string: The name of the method to call on the target module. This should be in the format <module_name>_<method_name>.

  • notifyAll bool: Similar to the Notify method, this determines whether the call should also notify all other modules with the specified method name.

  • notificationExclusion []string: A list of module names to exclude from receiving the notification. This is useful when you want to notify all modules except for certain ones.

  • args ...interface{}: The arguments to pass to the method being called. These should match the parameters defined in the method signature of the target module.

Example usage:

// Create a context with a timeout

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // Ensure the cancel function is called to release resources


// Using CallContext to call a method with a context

var result SomeType
err := ts.coreClient.CallContext(ctx, &result, "anotherModule_someMethod", false, nil, arg1, arg2)
if err != nil {
    // Handle error
}

// Use result

Step 7: Making Your Module Public on GitHub

  1. Push Your Code: Push your module code to the GitHub repository. Make sure your repository is public so that the Go package manager can index it.

  2. Tag Your Release: Tag your module’s release version using semantic versioning (e.g., v1.0.0). This helps users to easily identify the version of your module they want to install.

  3. Document Your Module: Write a README file for your module, explaining its functionality, how to install it, and how to use it with MEV Plus.

Step 8: Installing Your Module in MEV Plus

Once your module is publicly available on GitHub, you can install it in MEV Plus using the module management commands provided by MEV Plus. For example:

go run mevPlus.go modules -install github.com/yourusername/[email protected]

Replace github.com/yourusername/[email protected] with the actual GitHub URL and version tag of your module, i.e your <module-package-url> you created in Step 2.

Conclusion

Writing a module for MEV Plus involves understanding the MEV Plus Service interface, implementing the required methods, knowing how to communicate with and execute other functions in other modules, knowing how to write methods such that your module can receive notifications or executions on your methods from events and processes throughout MEV Plus. By following these steps, you can create custom modules that extend the functionality of MEV Plus, enabling a wide range of applications and integrations.