Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/01-prerequisites.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# Prerequisites

Before starting the tutorial, make sure you have the following tools installed.
Before starting the tutorial, make sure you have the following tools installed on your machine.

<Warning>
This tutorial is intended for macOS and Linux systems. Other systems may have additional requirements.
</Warning>

## Go

The example chain requires Go 1.25 or higher.
The example chain requires Go 1.26 or higher.

```bash
go version
# go version go1.25.0 linux/amd64 # Linux
# go version go1.25.0 darwin/arm64 # macOS
# go version go1.26.5 linux/amd64 # Linux
# go version go1.26.5 darwin/arm64 # macOS
```

If Go is not installed, download it from [go.dev/dl](https://go.dev/dl).
Expand Down
4 changes: 2 additions & 2 deletions docs/02-quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ This shows that the fee to increment the counter is stored as a module parameter
```yaml
params:
add_cost:
- amount: "100"
denom: stake
- amount: "100"
denom: stake
max_add_value: "100"
```

Expand Down
27 changes: 24 additions & 3 deletions docs/03-build-a-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@ Store the counter keeper on `ExampleApp` so the rest of the app can reference it

```go
// counter tutorial app wiring 2: add the counter keeper field below
CounterKeeper *counterkeeper.Keeper
CounterKeeper *counterkeeper.Keeper
```

### 3. Store Key
Expand Down Expand Up @@ -716,10 +716,31 @@ Open a second terminal and submit a transaction that adds `4` to the counter:
exampled tx counter add 4 --from alice --chain-id demo --yes
```

If the transaction succeeds, the response should include `code: 0`, which means the chain accepted and executed the transaction without an application error:
If the transaction succeeds, the response should include `code: 0`, which means the chain accepted the
transaction and it passed validation without an application error:

```
```text
code: 0
codespace: ""
data: ""
events: []
gas_used: "0"
gas_wanted: "0"
height: "0"
info: ""
logs: []
raw_log: ""
timestamp: ""
tx: null
txhash: 548D95784704575A347140E05A3ED84A05067DF4AD43F8E6FA20C94FAE8430E0
```

This is the broadcast acknowledgement, returned before the transaction is in a block, so `height: "0"`
and the empty fields are expected rather than a sign of failure. To see the executed result, query the
transaction by its hash:

```bash
exampled query tx <txhash>
```

### Query the chain
Expand Down
82 changes: 62 additions & 20 deletions docs/04-counter-walkthrough.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,12 @@ func (m msgServer) UpdateParams(ctx context.Context, msg *types.MsgUpdateParams)
return nil, sdkerrors.Wrapf(govtypes.ErrInvalidSigner,
"invalid authority; expected %s, got %s", m.authority, msg.Authority)
}
return &types.MsgUpdateParamsResponse{}, m.SetParams(ctx, msg.Params)

if err := m.SetParams(ctx, msg.Params); err != nil {
return nil, err
}

return &types.MsgUpdateParamsResponse{}, nil
}
```

Expand All @@ -139,6 +144,23 @@ authority: authtypes.NewModuleAddress(govtypes.ModuleName).String(),

This pattern, storing authority in the keeper and checking it in `MsgServer`, is the standard Cosmos SDK approach to governance-gated configuration.

To point a module at a different authority, `NewKeeper` accepts functional options. `WithAuthority` replaces the default after the keeper is built:

```go
// x/counter/keeper/keeper.go
type Options func(k *Keeper)

// WithAuthority sets a custom authority on the module. This allows developers to set accounts other than the
// governance module to control this module's params.
func WithAuthority(authority string) Options {
return func(k *Keeper) {
k.authority = authority
}
}
```

Most chains keep the governance default, so `app.go` passes no options.


## Expected keepers and fee collection

Expand Down Expand Up @@ -175,6 +197,8 @@ app.CounterKeeper = counterkeeper.NewKeeper(
)
```

The full signature is `NewKeeper(storeService, cdc, bankKeeper, opts ...Options)`. The trailing options are how you override the default governance authority, covered in [the authority pattern](#the-authority-pattern) above.

### Try it

Submit an add transaction and the configured `AddCost` fee will be charged from the sender:
Expand Down Expand Up @@ -222,10 +246,6 @@ type Keeper struct {

```go
func (k *Keeper) AddCount(ctx context.Context, sender string, amount uint64) (uint64, error) {
if amount >= math.MaxUint64 {
return 0, ErrNumTooLarge
}

params, err := k.GetParams(ctx)
if err != nil {
return 0, err
Expand All @@ -235,6 +255,21 @@ func (k *Keeper) AddCount(ctx context.Context, sender string, amount uint64) (ui
return 0, ErrExceedsMaxAdd
}

count, err := k.GetCount(ctx)
if err != nil {
return 0, err
}

// Reject adds that would wrap the counter past the top of the uint64 range.
// Written as a subtraction so the check itself cannot overflow. MaxAddValue
// usually keeps amount small, but setting it to 0 disables that cap, so the
// result has to be checked here rather than inferred from the input.
if amount > math.MaxUint64-count {
return 0, ErrNumTooLarge
}

// Charge the user if add cost is set. All validation happens above, so a
// rejected add never reaches this point.
if !params.AddCost.IsZero() {
senderAddr, err := sdk.AccAddressFromBech32(sender)
if err != nil {
Expand All @@ -245,11 +280,6 @@ func (k *Keeper) AddCount(ctx context.Context, sender string, amount uint64) (ui
}
}

count, err := k.GetCount(ctx)
if err != nil {
return 0, err
}

newCount := count + amount
if err := k.counter.Set(ctx, newCount); err != nil {
return 0, err
Expand All @@ -269,14 +299,17 @@ func (k *Keeper) AddCount(ctx context.Context, sender string, amount uint64) (ui
}
```

Note the shape of the overflow guard. Go wraps silently on unsigned overflow, so `count + amount` exceeding the `uint64` range would leave the counter holding a smaller number with no error raised. Testing the input alone cannot catch that, because the value that overflows is the sum. Comparing `amount` against `math.MaxUint64 - count` tests the result while keeping the comparison itself inside the range. Any module doing unchecked arithmetic on user-supplied values needs the same treatment.

All the business logic, validation, fee charging, state mutation, events, and telemetry, lives in `AddCount`. The `MsgServer` stays thin:

```go
func (m msgServer) Add(ctx context.Context, req *types.MsgAddRequest) (*types.MsgAddResponse, error) {
newCount, err := m.AddCount(ctx, req.GetSender(), req.GetAdd())
func (m msgServer) Add(ctx context.Context, request *types.MsgAddRequest) (*types.MsgAddResponse, error) {
newCount, err := m.AddCount(ctx, request.GetSender(), request.GetAdd())
if err != nil {
return nil, err
}

return &types.MsgAddResponse{UpdatedCount: newCount}, nil
}
```
Expand Down Expand Up @@ -310,13 +343,14 @@ Rather than returning generic errors, `x/counter` defines named sentinel errors
```go
// keeper/errors.go
var (
ErrNumTooLarge = errors.Register("counter", 0, "requested integer to add is too large")
ErrExceedsMaxAdd = errors.Register("counter", 1, "add value exceeds max allowed")
ErrInsufficientFunds = errors.Register("counter", 2, "insufficient funds to pay add cost")
// Codes start at 2: code 0 is reserved for success and code 1 for internal errors.
ErrNumTooLarge = errors.Register("counter", 2, "requested integer to add is too large")
ErrExceedsMaxAdd = errors.Register("counter", 3, "add value exceeds max allowed")
ErrInsufficientFunds = errors.Register("counter", 4, "insufficient funds to pay add cost")
)
```

Registered errors produce structured error responses on-chain that clients can match against by code, not just by string. Each error code must be unique within the module and greater than zero (code `1` is reserved for internal SDK errors). To check whether an error is of a specific sentinel type, use `errors.Is(err, ErrInsufficientFunds)` — this works correctly even when the error has been wrapped with additional context via `errorsmod.Wrap` or `errorsmod.Wrapf`.
Registered errors produce structured error responses on-chain that clients can match against by code, not just by string. Each error code must be unique within the module and start at `2`: code `0` is the ABCI success code, and code `1` is reserved for internal errors. Registering an error as code `0` is accepted silently, but a transaction failing with it reports `code: 0`, which every client reads as success. To check whether an error is of a specific sentinel type, use `errors.Is(err, ErrInsufficientFunds)`. This works correctly even when the error has been wrapped with additional context via `errorsmod.Wrap` or `errorsmod.Wrapf`.

All validation — both stateless field checks and stateful business logic checks — should live in the `msgServer` method or the keeper function it calls. The older `ValidateBasic` method on message types is deprecated: prefer performing all validation inside the message server. If your message type does implement `ValidateBasic`, the SDK still calls it for backward compatibility, but new modules should not rely on it.

Expand Down Expand Up @@ -378,15 +412,23 @@ func (a AppModule) AutoCLIOptions() *autocliv1.ModuleOptions {
Service: "example.counter.Query",
EnhanceCustomCommand: true,
RpcCommandOptions: []*autocliv1.RpcCommandOptions{
{RpcMethod: "Count", Use: "count", Short: "Query the current counter value"},
{
RpcMethod: "Count",
Use: "count",
Short: "Query the current counter value",
},
},
},
Tx: &autocliv1.ServiceCommandDescriptor{
Service: "example.counter.Msg",
EnhanceCustomCommand: true,
RpcCommandOptions: []*autocliv1.RpcCommandOptions{
{RpcMethod: "Add", Use: "add [amount]", Short: "Add to the counter",
PositionalArgs: []*autocliv1.PositionalArgDescriptor{{ProtoField: "add"}}},
{
RpcMethod: "Add",
Use: "add [amount]",
Short: "Add to the counter",
PositionalArgs: []*autocliv1.PositionalArgDescriptor{{ProtoField: "add"}},
},
},
},
}
Expand Down Expand Up @@ -552,7 +594,7 @@ s.bankKeeper.SendCoinsFromAccountToModuleFn = func(...) error {

## Gas

`minimum-gas-prices` in `app.toml` sets the minimum fee a node requires before it will accept and relay a transaction. The local dev chain started by `make start` leaves this empty, so transactions are accepted with no fee beyond the `AddCost` module parameter.
`minimum-gas-prices` in `app.toml` sets the minimum fee a node requires before it will accept and relay a transaction. The local dev chain started by `make start` sets this to `0stake`, so transactions are accepted with no fee beyond the `AddCost` module parameter.

To require a minimum network fee, set it in `app.toml`:

Expand Down
Loading