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.
The steps below follow the same example where:
Module name:
examplemodNew parameter name:
new_parameterDefault value:
int64(42)
Substitute these example values with your own when following the steps.
At any point, you can run:
```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
MsgUpdateParam MessageUse ignite to scaffold a new MsgUpdateParam message for the module.
Temporary Workaround Required:
Rename the local go module in go.mod (see related comments there)
go mod tidyignite scaffold ...make proto_regenRestore the original go.mod
go mod tidymake ignite_buildand/or (re)start/build localnet
ignite scaffold message update-param --module examplemod --signer authority name as_type --response paramsUpdate MsgUpdateParam and MsgUpdateParamResponse Fields
MsgUpdateParam and MsgUpdateParamResponse FieldsUpdate 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()
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:
use the
keeper_testpackage (i.e.package keeper_test).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?
