Adding Onchain Module Parameters

Adding Onchain Module Parameters

Adding a new onchain module parameter involves multiple steps to ensure the parameter is properly integrated into the system. This guide walks through a generic approach, illustrated by adding a parameter to the proof module.

See https://github.com/pokt-network/poktroll/pull/595 for a real-world example.

TODO_POST_MAINNET(@bryanchriswhite): Once the next version of ignite is out, leverage: https://github.com/ignite/cli/issues/3684#issuecomment-2299796210

At any point, you can run:

Run tests

```bash go test ./x/examplemod/... ``` to check whether everything is working or to locate outstanding necessary changes. ``` {% endhint %}

{% stepper %}

{% step %}

If the Module Doesn't Already Support a MsgUpdateParam Message

In order to support individual parameter updates, the module MUST have a MsgUpdateParam message. If the module doesn't support this message, add it.

Scaffold the MsgUpdateParam Message

Use ignite to scaffold a new MsgUpdateParam message for the module.

Temporary Workaround Required:

  1. Rename the local go module in go.mod (see related comments there)

  2. go mod tidy

  3. ignite scaffold ...

  4. make proto_regen

  5. Restore the original go.mod

  6. go mod tidy

  7. make ignite_build and/or (re)start/build localnet

ignite scaffold message update-param --module examplemod --signer authority name as_type --response params

Update MsgUpdateParam and MsgUpdateParamResponse Fields

Update the module's tx.proto (e.g. proto/pocket/examplemod/tx.proto) to include comments and protobuf options:

+ // MsgUpdateParam is the Msg/UpdateParam request type to update a single param.
  message MsgUpdateParam {
    option (cosmos.msg.v1.signer) = "authority";
-   string authority = 1;
+
+   // authority is the address that controls the module (defaults to x/gov unless overwritten).
+   string authority = 1  [(cosmos_proto.scalar) = "cosmos.AddressString"];
+
    string name      = 2;
-   string asType    = 3;
+   oneof as_type {
+     // Add `as_<type>` fields for each type in this module's Params type; e.g.:
+     // int64 as_int64 = 3 [(gogoproto.jsontag) = "as_int64"];
+     // bytes as_bytes = 4 [(gogoproto.jsontag) = "as_bytes"];
+     // cosmos.base.v1beta1.Coin as_coin = 5 [(gogoproto.jsontag) = "as_coin"];
+   }
  }

  message MsgUpdateParamResponse {
-   string params = 1;
+   Params params = 1;
  }

Comment Out AutoCLI

When scaffolding, generated code is added to x/examplemod/module/autocli.go. Since governance parameters aren't updated via pocketd CLI, comment out the generated Tx command lines:

  // ...
  Tx: &autocliv1.ServiceCommandDescriptor{
      Service:              modulev1.Msg_ServiceDesc.ServiceName,
      EnhanceCustomCommand: true, // only required if you want to use the custom command
      RpcCommandOptions:    []*autocliv1.RpcCommandOptions{
          // ...
+         //  {
+         //    RpcMethod:      "UpdateParam",
+         //    Use:            "update-param [name] [as-type]",
+         //    Short:          "Send a update-param tx",
+         /     PositionalArgs: []*autocliv1.PositionalArgDescriptor{{ProtoField: "name"}, {ProtoField: "asType"}},
+         //  },
          // this line is used by ignite scaffolding # autocli/tx
      },
  },
  // ...

Update the DAO Genesis Authorizations JSON File

Add a grant to tools/scripts/authz/localnet_genesis_authorizations.json with the authorization.msg typeURL for this module's MsgUpdateParam:

{
  "granter": "pokt10d07y265gmmuvt4z0w9aw880jnsr700j8yv32t",
  "grantee": "pokt1eeeksh2tvkh7wzmfrljnhw4wrhs55lcuvmekkw",
  "authorization": {
    "@type": "/cosmos.authz.v1beta1.GenericAuthorization",
    "msg": "/pocket.examplemod.MsgUpdateParam"
  },
  "expiration": "2500-01-01T00:00:00Z"
},

Update the NewMsgUpdateParam Constructor and MsgUpdateParam#ValidateBasic()

Prepare x/examplemod/types/message_update_param.go:

- func NewMsgUpdateParam(authority string, name string, asType string) *MsgUpdateParam {
+ func NewMsgUpdateParam(authority string, name string, asType any) (*MsgUpdateParam, error) {
+   var asTypeIface isMsgUpdateParam_AsType
+
+   switch t := asType.(type) {
+   default:
+     return nil, ExamplemodParamInvalid.Wrapf("unexpected param value type: %T", asType)
+   }
+
    return &MsgUpdateParam{
      Authority: authority,
      Name: name,
-     AsType: asType,
-   }
+     AsType: asTypeIface,
+   }, nil
  }

  func (msg *MsgUpdateParam) ValidateBasic() error {
    _, err := cosmostypes.AccAddressFromBech32(msg.Authority)
    if err != nil {
      return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid authority address (%s)", err)
    }
-
-   return nil
+
+   // Parameter value MUST NOT be nil.
+   if msg.AsType == nil {
+     return ErrExamplemodParamInvalid.Wrap("missing param AsType")
+   }
+
+   // Parameter name MUST be supported by this module.
+   switch msg.Name {
+   default:
+     return ErrExamplemodParamInvalid.Wrapf("unsupported param %q", msg.Name)
+   }
  }

Update the Module's msgServer#UpdateParam() Handler

Prepare x/examplemod/keeper/msg_server_update_param.go to handle updates by type:

- func (k msgServer) UpdateParam(goCtx context.Context, msg *examplemodtypes.MsgUpdateParam) (*examplemodtypes.MsgUpdateParamResponse, error) {
-   ctx := sdk.UnwrapSDKContext(goCtx)
-
-   // TODO: Handling the message
-   _ = ctx
-
+ // UpdateParam updates a single parameter in the proof module and returns
+ // all active parameters.
+ func (k msgServer) UpdateParam(ctx context.Context, msg *examplemodtypes.MsgUpdateParam) (*examplemodtypes.MsgUpdateParamResponse, error) {
+   logger := k.logger.With(
+     "method", "UpdateParam",
+     "param_name", msg.Name,
+   )
+
+   if err := msg.ValidateBasic(); err != nil {
+     return nil, status.Error(codes.InvalidArgument, err.Error())
+   }
+
+   if k.GetAuthority() != msg.Authority {
+     return nil, status.Error(
+       codes.PermissionDenied,
+       examplemodtypes.ErrExamplemodInvalidSigner.Wrapf(
+         "invalid authority; expected %s, got %s",
+         k.GetAuthority(), msg.Authority,
+       ).Error(),
+     )
+   }
+
+   params := k.GetParams(ctx)
+
+   switch msg.Name {
+   default:
+     return nil, status.Error(
+       codes.InvalidArgument,
+       examplemodtypes.ErrExamplemodParamInvalid.Wrapf("unsupported param %q", msg.Name).Error(),
+     )
+   }
+
+   // Perform a global validation on all params, which includes the updated param.
+   if err := params.Validate(); err != nil {
+     return nil, status.Error(codes.InvalidArgument, err.Error())
+   }
+
+   if err := k.SetParams(ctx, params); err != nil {
+     err = fmt.Errorf("unable to set params: %w", err)
+     logger.Error(err.Error())
+     return nil, status.Error(codes.Internal, err.Error())
+   }
+
+   updatedParams := k.GetParams(ctx)
+
+   return &types.MsgUpdateParamResponse{
+     Params: &updatedParams,
+   }, nil
  }

Update Module's Params Test Suite ModuleParamConfig

Add MsgUpdateParam & MsgUpdateParamResponse to the module's ModuleParamConfig#ParamsMsg in testutil/integration/suites/param_config.go:

  ExamplemodModuleParamConfig = ModuleParamConfig{
    ParamsMsgs: ModuleParamsMessages{
      MsgUpdateParams:         gatewaytypes.MsgUpdateParams{},
      MsgUpdateParamsResponse: gatewaytypes.MsgUpdateParamsResponse{},
+     MsgUpdateParam:          gatewaytypes.MsgUpdateParam{},
+     MsgUpdateParamResponse:  gatewaytypes.MsgUpdateParamResponse{},
      QueryParamsRequest:      gatewaytypes.QueryParamsRequest{},
      QueryParamsResponse:     gatewaytypes.QueryParamsResponse{},
    },
    // ...
  }

{% endstep %}

{% step %}

Define the Parameter in the Protocol Buffers File

Define the new parameter in the module's params.proto (e.g., proto/pocket/examplemod/params.proto):

  message Params {
    // Other existing parameters...

+   // Description of the new parameter.
+   int64 new_parameter = 3 [(gogoproto.jsontag) = "new_parameter", (gogoproto.moretags) = "yaml:\"new_parameter\""];
  }

{% hint style="warning" %} Be sure to update the gogoproto.jsontag and gogoproto.moretags option values to match the new parameter name! {% endhint %}

{% hint style="info" %} Don't forget to run: {% code title="Regenerate protobuf code" %}

make proto_regen

{% endhint %}

{% endstep %}

{% step %}

Update the Default Parameter Values

Go Source Defaults

In x/examplemod/types/params.go, define the key, param name, default, and include it in NewParams and DefaultParams:

  var (
    // Other existing parameter keys, names, and defaults...

+   KeyNewParameter = []byte("NewParameter")
+   ParamNewParameter = "new_parameter"
+   DefaultNewParameter int64 = 42
  )

  func NewParams(
    // Other existing parameters...
+   newParameter int64,
  ) Params {
    return Params{
      // Other existing parameters...
+     NewParameter: newParameter,
    }
  }

  func DefaultParams() Params {
    return NewParams(
      // Other existing default parameters...
+     DefaultNewParameter,
    )
  }

Genesis Configuration Parameter Defaults

Add the new parameter to the genesis configuration file (e.g., config.yml):

genesis:
  examplemod:
    params:
      # Other existing parameters...

+     new_parameter: 42

{% endstep %}

{% step %}

Parameter Validation

Define a Validation Function

Implement a validation function in x/examplemod/types/params.go:

// ValidateNewParameter validates the NewParameter param.
func ValidateNewParameter(newParamAny any) error {
  newParam, ok := newParamAny.(int64)
  if !ok {
    return ErrExamplemodParamInvalid.Wrapf("invalid parameter type: %T", newParamAny)
  }

  // Any additional validation...

  return nil
}

Call it in Params#Validate()

Integrate the validator into Params#Validate():

func (params *Params) Validate() error {
  // ...
  if err := ValidateNewParameter(params.NewParameter); err != nil {
    return err
  }
  // ...
}

Add a ParamSetPair to ParamSetPairs()

Include the pair in ParamSetPairs():

func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs {
  return paramtypes.ParamSetPairs{
    // Other existing param set pairs...

+   paramtypes.NewParamSetPair(
+     KeyNewParameter,
+     &p.NewParameter,
+     ValidateNewParameter,
+   ),
  }
}

{% endstep %}

{% step %}

Add Parameter Case to Switch Statements

MsgUpdateParam#ValidateBasic()

Add the parameter type and name to switch statements in NewMsgUpdateParam() and MsgUpdateParam#ValidateBasic():

  func NewMsgUpdateParam(authority string, name string, asType any) (*MsgUpdateParam, error) {
    // ...
    switch t := asType.(type) {
+   case int64:
+     asTypeIface = &MsgUpdateParam_AsCoin{AsInt64: t}
    default:
      return nil, ErrExamplemodParamInvalid.Wrapf("unexpected param value type: %T", asType))
    }
    // ...
  }

+ // ValidateBasic performs a basic validation of the MsgUpdateParam fields. It ensures:
+ // 1. The parameter name is supported.
+ // 2. The parameter type matches the expected type for a given parameter name.
+ // 3. The parameter value is valid (according to its respective validation function).
  func (msg *MsgUpdateParam) ValidateBasic() error {
    // ...
    switch msg.Name {
+   case ParamNewParameter:
+     if err := genericParamTypeIs[*MsgUpdateParam_AsInt64](msg); err != nil {
+       return err
+     }
+     return ValidateNewParameter(msg.GetAsInt64())
    default:
      return ErrExamplemodParamInvalid.Wrapf("unsupported param %q", msg.Name)
    }
  }
+
+ func genericParamTypeIs[T any](msg *MsgUpdateParam) error {
+   if _, ok := msg.AsType.(T); !ok {
+     return ErrParamInvalid.Wrapf(
+       "invalid type for param %q; expected %T, got %T",
+       msg.Name, *new(T), msg.AsType,
+     )
+   }
+
+   return nil
+ }

msgServer#UpdateParam()

Add the parameter name to the switch in msgServer#UpdateParam() (in x/examplemod/keeper/msg_server_update_param.go):

{% hint style="warning" %} Every error return from msgServer methods (e.g. UpdateParams) SHOULD be encapsulated in a gRPC status error. {% endhint %}

  switch msg.Name {
+ case examplemodtypes.ParamNewParameter:
+   logger = logger.with("param_value", msg.GetAsInt64())
+   params.NewParameter = msg.GetAsInt64()
  default:
    return nil, status.Error(
      codes.InvalidArgument,
      examplemodtypes.ErrExamplemodParamInvalid.Wrapf("unsupported param %q", msg.Name).Error(),
    )
  }

{% endstep %}

{% step %}

Update Unit Tests

Parameter Validation Tests

Add validation tests in x/examplemod/keeper/params_test.go:

func TestParams_ValidateNewParameter(t *testing.T) {
  tests := []struct {
    desc         string
    newParameter any
    expectedErr  error
  }{
    {
      desc: "invalid type",
      newParameter: "420",
      expectedErr: examplemodtypes.ErrExamplemodParamInvalid.Wrapf("invalid parameter type: string"),
    },
    {
      desc: "valid NewParameterName",
      newParameter: int64(420),
    },
  }

  for _, test := range tests {
    t.Run(test.desc, func(t *testing.T) {
      err := examplemodtypes.ValidateNewParameter(test.newParameter)
      if test.expectedErr != nil {
        require.Error(t, err)
        require.Contains(t, err.Error(), test.expectedErr.Error())
      } else {
        require.NoError(t, err)
      }
    })
  }
}

Parameter Update Tests

Add cases to x/examplemod/keeper/msg_update_params_test.go for invalid combinations and minimal params:

+ {
+   desc: "valid: send minimal params", // For parameters which MUST NEVER be their zero value or nil.
+   input: &examplemodtypes.MsgUpdateParams{
+     Authority: k.GetAuthority(),
+     Params: examplemodtypes.Params{
+       NewParameter: 42,
+     },
+   },
+   shouldError: false,
+ },

Add a unit test to exercise individually updating the new parameter in x/examplemod/keeper/msg_server_update_param_test.go:

func TestMsgUpdateParam_UpdateNewParameterOnly(t *testing.T) {
  var expectedNewParameter int64 = 420

  // Set the parameters to their default values
  k, msgSrv, ctx := setupMsgServer(t)
  defaultParams := examplemodtypes.DefaultParams()
  require.NoError(t, k.SetParams(ctx, defaultParams))

  // Ensure the default values are different from the new values we want to set
  require.NotEqual(t, expectedNewParameter, defaultParams.NewParameter)

  // Update the new parameter
  updateParamMsg := &examplemodtypes.MsgUpdateParam{
    Authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(),
    Name: examplemodtypes.ParamNewParameter,
    AsType: &examplemodtypes.MsgUpdateParam_AsInt64{AsInt64: expectedNewParameter},
  }
  res, err := msgSrv.UpdateParam(ctx, updateParamMsg)
  require.NoError(t, err)
  require.Equal(t, expectedNewParameter, res.Params.NewParameter)

  // Ensure the other parameters are unchanged
  testkeeper.AssertDefaultParamsEqualExceptFields(t, &defaultParams, res.Params, string(examplemodtypes.KeyNewParameter))
}

{% hint style="warning" %} If creating msg_server_update_param_test.go, be sure to:

  1. use the keeper_test package (i.e. package keeper_test).

  2. add the testutil keeper import: testkeeper "github.com/pokt-network/poktroll/testutil/keeper" {% endhint %}

Also update x/examplemod/types/message_update_param_test.go to use the new MsgUpdateParam#AsType fields and include cases covering invalid values:

func TestMsgUpdateParam_ValidateBasic(t *testing.T) {
  tests := []struct {
-     name string
+     desc string
      msg  MsgUpdateParam
-     err  error
+     expectedErr  error
  }{
    {
-       name: "invalid address",
+       desc: "invalid: authority address invalid",
        msg: MsgUpdateParam{
          Authority:  "invalid_address",
+         Name: "",   // Doesn't matter for this test
+         AsType:     &MsgUpdateParam_AsInt64{AsInt64: 0},
        },
-       err: sdkerrors.ErrInvalidAddress,
+       expectedErr: sdkerrors.ErrInvalidAddress,
    }, {
+       desc: "invalid: param name incorrect (non-existent)",
+       msg: MsgUpdateParam{
+         Authority: sample.AccAddress(),
+         Name:      "non_existent",
+         AsType:    &MsgUpdateParam_AsInt64{AsInt64: DefaultNewParameter},
+       },
+       expectedErr: ErrExamplemodParamInvalid,
    }, {
-       name: "valid address",
+       desc: "valid: correct address, param name, and type",
        msg: MsgUpdateParam{
          Authority: sample.AccAddress(),
+         Name: ParamNewParameter,
+         AsType: &MsgUpdateParam_AsInt64{AsInt64: DefaultNewParameter},
        },
      },
    }
    // ...
  }

{% endstep %}

{% step %}

Update the Parameter Integration Tests

Integration tests use ModuleParamConfig in testutil/integration/suites/param_configs.go to dynamically construct and send parameter update messages. When adding parameters, update the module's ModuleParamConfig.

Add a valid param

Update ModuleParamConfig#ValidParams to include a valid non-default value:

  ExamplemodModuleParamConfig = ModuleParamConfig{
      // ...
      ValidParams: examplemodtypes.Params{
+         NewParameter: 420,
      },
      // ...
  }

Check for as_ on MsgUpdateParam

Ensure an as_<type> field exists on MsgUpdateParam corresponding to the type (e.g., int64) in proto/pocket/examplemod/tx.proto:

 message MsgUpdateParam {
   ...
   oneof as_type {
-    // Add `as_<type>` fields for each type in this module's Params type; e.g.:
+    int64 as_int64 = 3 [(gogoproto.jsontag) = "as_int64"];
   }
 }

Update the module's ModuleParamConfig

Ensure all available as_<type> types for the module are present on ModuleParamConfig#ParamTypes:

  ExamplemodModuleParamConfig = ModuleParamConfig{
    // ...
    ValidParams: examplemodtypes.Params{},
+   ParamTypes: map[ParamType]any{
+     ParamTypeInt64: examplemodtypes.MsgUpdateParam_AsInt64{},
+   },
    DefaultParams:    examplemodtypes.DefaultParams(),
    // ...
  }

{% endstep %}

{% step %}

Update the Makefile and Supporting JSON Files

Update the Makefile

Add a new target in makefiles/params.mk:

.PHONY: params_update_examplemod_new_parameter
params_update_examplemod_new_parameter: ## Update the examplemod module new_parameter param
  pocketd tx authz exec ./tools/scripts/params/examplemod_new_parameter.json $(PARAM_FLAGS)

{% hint style="warning" %} Reminder to substitute examplemod and new_parameter with your module and param names! {% endhint %}

Create a new JSON File for the Individual Parameter Update

Create e.g. tools/scripts/params/proof_new_parameter_name.json:


json
{
  "body": {
    "messages": [
      {
        "@type": "/pocket.examplemod.MsgUpdateParam",
        "authority": "pokt10d07y265gmmuvt4z0w9aw880jnsr700j8yv32t",
        "name": "new_parameter",
        "as_int64": "42"
      }
    ]
  }
}

Update the JSON File for Updating All Parameters for the Module

Add the new parameter default to the module's MsgUpdateParams JSON file (e.g., proof_all.json):

  {
    "body": {
      "messages": [
        {
          "@type": "/pocket.examplemod.MsgUpdateParams",
          "authority": "pokt10d07y265gmmuvt4z0w9aw880jnsr700j8yv32t",
          "params": {
            // Other existing parameters...
+           "new_parameter": "42"
          }
        }
      ]
    }
  }

Was this helpful?