diff --git a/sdk/next/tutorials/example/00-overview.mdx b/sdk/next/tutorials/example/00-overview.mdx index 1c3e3c7c..15151eae 100644 --- a/sdk/next/tutorials/example/00-overview.mdx +++ b/sdk/next/tutorials/example/00-overview.mdx @@ -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. --- diff --git a/sdk/next/tutorials/example/01-prerequisites.mdx b/sdk/next/tutorials/example/01-prerequisites.mdx index 979524ce..fc822793 100644 --- a/sdk/next/tutorials/example/01-prerequisites.mdx +++ b/sdk/next/tutorials/example/01-prerequisites.mdx @@ -1,11 +1,11 @@ --- +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. This tutorial is intended for macOS and Linux systems. Other systems may have additional requirements. @@ -13,12 +13,12 @@ This tutorial is intended for macOS and Linux systems. Other systems may have ad ## 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). diff --git a/sdk/next/tutorials/example/02-quickstart.mdx b/sdk/next/tutorials/example/02-quickstart.mdx index 9b143dbb..050e31e9 100644 --- a/sdk/next/tutorials/example/02-quickstart.mdx +++ b/sdk/next/tutorials/example/02-quickstart.mdx @@ -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 --- @@ -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" ``` diff --git a/sdk/next/tutorials/example/03-build-a-module.mdx b/sdk/next/tutorials/example/03-build-a-module.mdx index 9f5932b2..79ee56dd 100644 --- a/sdk/next/tutorials/example/03-build-a-module.mdx +++ b/sdk/next/tutorials/example/03-build-a-module.mdx @@ -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). Before continuing, you must follow the [Prerequisites guide](/sdk/next/tutorials/example/01-prerequisites) to make sure everything is installed. @@ -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 diff --git a/sdk/next/tutorials/example/04-counter-walkthrough.mdx b/sdk/next/tutorials/example/04-counter-walkthrough.mdx index f3e324c3..0d7efd2e 100644 --- a/sdk/next/tutorials/example/04-counter-walkthrough.mdx +++ b/sdk/next/tutorials/example/04-counter-walkthrough.mdx @@ -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: @@ -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 } ``` @@ -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 @@ -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: @@ -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 @@ -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 { @@ -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 @@ -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 } ``` @@ -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. @@ -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"}}, + }, }, }, } @@ -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`: diff --git a/sdk/next/tutorials/example/05-run-and-test.mdx b/sdk/next/tutorials/example/05-run-and-test.mdx index f4ba0915..f7db85f4 100644 --- a/sdk/next/tutorials/example/05-run-and-test.mdx +++ b/sdk/next/tutorials/example/05-run-and-test.mdx @@ -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 --- @@ -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`. -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. @@ -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 @@ -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 | @@ -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. @@ -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 |