Skip to content
Merged
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
2 changes: 1 addition & 1 deletion sdk/next/tutorials/example/00-overview.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Tutorial Intro
noindex: true
canonical: 'https://docs.cosmos.network/sdk/latest/tutorials/example/00-overview'
title: Tutorial Intro
description: Build a module from scratch, wire it into a chain, and run it locally, all in minutes.
---

Expand Down
10 changes: 5 additions & 5 deletions sdk/next/tutorials/example/01-prerequisites.mdx
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
---
title: Prerequisites
noindex: true
canonical: 'https://docs.cosmos.network/sdk/latest/tutorials/example/01-prerequisites'
title: Prerequisites
description: Install dependencies
---

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
6 changes: 3 additions & 3 deletions sdk/next/tutorials/example/02-quickstart.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Chain Quickstart
noindex: true
canonical: 'https://docs.cosmos.network/sdk/latest/tutorials/example/02-quickstart'
title: Chain Quickstart
description: Start a chain, submit a transaction, and query the result in minutes
---

Expand Down Expand Up @@ -67,8 +67,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
6 changes: 3 additions & 3 deletions sdk/next/tutorials/example/03-build-a-module.mdx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
---
title: Build a Module from Scratch
noindex: true
canonical: 'https://docs.cosmos.network/sdk/latest/tutorials/example/03-build-a-module'
title: Build a Module from Scratch
description: Build a simple counter module from scratch in minutes
---

In [quickstart](/sdk/next/tutorials/example/02-quickstart), you started a chain and submitted a transaction to increase the counter. In this tutorial, you'll build a simple counter module from scratch. It follows the same overall structure as the full `x/counter`, but uses a stripped-down version so you can focus on the core steps of building and wiring a module yourself.

By the end, you'll have built a working module and wired it into a running chain. For a deeper dive into how modules work in the Cosmos SDK, see [Intro to Modules](/sdk/next/learn/concepts/modules).
By the end, you'll have built a working module and wired it into a running chain. For a deeper dive into how modules work in the Cosmos SDK, see [Intro to Modules](/sdk/next/learn/concepts/modules).

<Note>
Before continuing, you must follow the [Prerequisites guide](/sdk/next/tutorials/example/01-prerequisites) to make sure everything is installed.
Expand Down Expand Up @@ -628,7 +628,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
84 changes: 63 additions & 21 deletions sdk/next/tutorials/example/04-counter-walkthrough.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Full Counter Module Walkthrough
noindex: true
canonical: 'https://docs.cosmos.network/sdk/latest/tutorials/example/04-counter-walkthrough'
title: Full Counter Module Walkthrough
---

If you came here from the module building tutorial, switch back to the `main` branch of the [`cosmos/example` repo](https://github.com/cosmos/example) first:
Expand Down Expand Up @@ -131,7 +131,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 @@ -143,6 +148,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 @@ -179,6 +201,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 @@ -226,10 +250,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 @@ -239,6 +259,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 @@ -249,11 +284,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 @@ -273,14 +303,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 @@ -314,13 +347,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 @@ -382,15 +416,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 @@ -556,7 +598,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
70 changes: 53 additions & 17 deletions sdk/next/tutorials/example/05-run-and-test.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Run, Test, and Configure
noindex: true
canonical: 'https://docs.cosmos.network/sdk/latest/tutorials/example/05-run-and-test'
title: Run, Test, and Configure
description: Learn how to run and test a chain
---

Expand Down Expand Up @@ -36,29 +36,59 @@ make start

Re-running `make start` resets state automatically. There is no separate reset command.

## Localnet (multi-validator)
## Localnet (multi-node)

Use localnet when you want a setup that is closer to a real network. It runs multiple validators in Docker so you can test multi-node behavior locally.
Localnet runs four nodes in Docker to give you a setup closer to a real network than the single-node chain. `scripts/localnet/init.sh` creates a genesis transaction for `node0` only, so the network is **one validator plus three full nodes**, not four validators. The chain ID is `example-localnet`, and each node has a single key named `validator` rather than the `alice` and `bob` accounts used by `make start`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Bold emphasis violates documentation style

This new paragraph uses bold emphasis for two phrases even though the repository prohibits bold and italic text in documentation, introducing formatting that maintainers must remove.

Suggested change
Localnet runs four nodes in Docker to give you a setup closer to a real network than the single-node chain. `scripts/localnet/init.sh` creates a genesis transaction for `node0` only, so the network is **one validator plus three full nodes**, not four validators. The chain ID is `example-localnet`, and each node has a single key named `validator` rather than the `alice` and `bob` accounts used by `make start`.
Localnet runs four nodes in Docker to give you a setup closer to a real network than the single-node chain. `scripts/localnet/init.sh` creates a genesis transaction for `node0` only, so the network is one validator plus three full nodes, not four validators. The chain ID is `example-localnet`, and each node has a single key named `validator` rather than the `alice` and `bob` accounts used by `make start`.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


For a multi-validator setup using Docker:
Before you begin, note that this section needs Docker running, and that the following host ports must be free: `26656`, `26657`, `1317`, `9090` for `node0`, then `26666`, `26667`, `1318`, `9091` for `node1`, `26676`, `26677`, `1319`, `9092` for `node2`, and `26686`, `26687`, `1320`, `9093` for `node3`.

```bash
# Initialize localnet configuration
# Build the node image and initialize four node directories under build/localnet.
# Takes several minutes the first time, since it compiles the chain in Docker.
make localnet-init

# Start all validators
# Start all four nodes
make localnet-start

# View logs
# Follow the logs. This does not exit on its own; press Ctrl+C to stop following
make localnet-logs

# Stop
make localnet-stop

# Clean all localnet data
# Delete build/localnet immediately, without confirming
make localnet-clean
```

### Confirm the network is healthy

Each node exposes its own RPC port. Check that every node has found the other three and that they are advancing together:

```bash
for port in 26657 26667 26677 26687; do
curl -s http://localhost:$port/status | grep -o '"latest_block_height":"[0-9]*"'
curl -s http://localhost:$port/net_info | grep -o '"n_peers":"[0-9]*"'
done
```

Each node should report `"n_peers":"3"` and a block height that climbs on repeated calls.

### Send a transaction

The localnet uses a different chain ID and key name than `make start`, so the commands in the CLI reference below need adjusting. Run them inside a container:

```bash
docker exec node0 exampled tx counter add 7 \
--from validator --chain-id example-localnet \
--keyring-backend test --home /data/node0 --yes
```

Then confirm the state replicated by querying a different node:

```bash
docker exec node2 exampled query counter count --home /data/node2
```

## CLI reference

Once the chain is running, these are the core [CLI](/sdk/next/learn/concepts/cli-grpc-rest#cli) commands you'll use to inspect state and submit transactions.
Expand Down Expand Up @@ -122,9 +152,9 @@ The most common settings to change during development:

| Setting | Default | Description |
|---|---|---|
| `minimum-gas-prices` | `"0stake"` | Minimum fee the node accepts before processing a transaction |
| `minimum-gas-prices` | `"0stake"` | Minimum fee the node accepts before processing a transaction. Set by this chain in `exampled/cmd/commands.go`, not by the SDK, whose own default is empty |
| `pruning` | `"default"` | How much historical state to keep (`default`, `nothing`, `everything`, `custom`) |
| `api.enable` | `true` | Enables the REST API on port 1317 |
| `api.enable` | `true` after `make start` | Enables the REST API on port 1317. The SDK default is `false`; `scripts/local_node.sh` turns it on for local development |
| `grpc.enable` | `true` | Enables the gRPC server on port 9090 |

### config.toml
Expand All @@ -135,7 +165,7 @@ The settings most likely to change during development:
|---|---|---|
| `moniker` | `"test"` | Human-readable name for the node |
| `log_level` | `"info"` | Log verbosity (`debug`, `info`, `error`) |
| `consensus.timeout_commit` | `"5s"` | How long to wait after a block is committed before starting the next one |
| `consensus.timeout_commit` | `"5s"` | How long to wait after a block is committed before starting the next one. The SDK raises CometBFT's own 1s default to 5s |
| `p2p.seeds` | `""` | Seed nodes to connect to on a live network |
| `p2p.persistent_peers` | `""` | Peers to maintain permanent connections to |

Expand Down Expand Up @@ -210,6 +240,12 @@ make test-sim

Simulation requires the `sims` build tag, which the Makefile targets handle automatically.

Each of these runs the simulation across 38 built-in seeds, so expect roughly ten minutes per target. The Makefile deliberately uses smaller values than the SDK defaults of 500 blocks and 200 operations per block, which across 38 seeds take hours. To simulate more deeply, override them:

```bash
make test-sim-full SIM_NUM_BLOCKS=500 SIM_BLOCK_SIZE=200 SIM_TIMEOUT=4h
```

## Lint

Linting is the quickest way to catch style problems and common code-quality issues before CI or code review does.
Expand All @@ -230,9 +266,9 @@ make lint-fix

Use this table as a quick reference for choosing the right validation command for the kind of change you made.

| Command | What it validates |
|---|---|
| `go test ./x/counter/...` | Keeper, MsgServer, QueryServer in isolation |
| `go test -run TestE2ETestSuite ./tests/...` | Full transaction and query flow on a live node |
| `make test-sim-full` | Non-determinism and invariant violations |
| `make lint` | Code style and static analysis |
| Command | What it validates | Typical runtime |
|---|---|---|
| `go test ./x/counter/...` | Keeper, MsgServer, QueryServer in isolation | seconds |
| `go test -run TestE2ETestSuite ./tests/...` | Full transaction and query flow on a live node | under a minute |
| `make test-sim-full` | Non-determinism and invariant violations | around ten minutes |
| `make lint` | Code style and static analysis | a few minutes |