From 495f222e25a51ac8bfa9181535c1bd8a89a1752d Mon Sep 17 00:00:00 2001 From: "docs-sync[bot]" Date: Tue, 31 Mar 2026 20:12:00 +0000 Subject: [PATCH 1/3] docs: sync example tutorials from cosmos/docs [docs-sync] --- docs/03-build-a-module.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/03-build-a-module.md b/docs/03-build-a-module.md index 026206b..8e9a593 100644 --- a/docs/03-build-a-module.md +++ b/docs/03-build-a-module.md @@ -2,7 +2,7 @@ In [quickstart](./02-quickstart.md), 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](https://docs.cosmos.network/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](https://docs.cosmos.network/sdk/next/learn/concepts/modules). Before continuing, you must follow the [Prerequisites guide](./01-prerequisites.md) to make sure everything is installed. From 10a9fb5f3e8894b0ca186bb8463ab3fccd05f025 Mon Sep 17 00:00:00 2001 From: "docs-sync[bot]" Date: Wed, 1 Apr 2026 18:05:33 +0000 Subject: [PATCH 2/3] docs: sync example tutorials from cosmos/docs [docs-sync] --- docs/01-prerequisites.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/01-prerequisites.md b/docs/01-prerequisites.md index 46dc6da..4107da6 100644 --- a/docs/01-prerequisites.md +++ b/docs/01-prerequisites.md @@ -40,7 +40,7 @@ Verify: `go env GOPATH` ## Make -Make is used to run build and development commands throughout the tutorial. +Make is used to run build and development commands throughout this tutorial. ```bash make --version @@ -74,7 +74,7 @@ git --version ## Clone the repository -Clone [cosmos/example](https://github.com/cosmos/example) and navigate into it: +Clone [cosmos/example](https://github.com/cosmos/example) and navigate into it. ```bash git clone https://github.com/cosmos/example From ba7f83f5abbb70e867951e49d24aba6013ea25b7 Mon Sep 17 00:00:00 2001 From: "docs-sync[bot]" Date: Tue, 25 Aug 2026 17:24:06 +0000 Subject: [PATCH 3/3] docs: sync example tutorials from cosmos/docs [docs-sync] --- docs/01-prerequisites.md | 12 +-- docs/02-quickstart.md | 4 +- docs/03-build-a-module.md | 29 ++++++- docs/04-counter-walkthrough.md | 82 +++++++++++++----- docs/05-run-and-test.md | 148 +++++++++++++++++++++++++++++---- 5 files changed, 225 insertions(+), 50 deletions(-) diff --git a/docs/01-prerequisites.md b/docs/01-prerequisites.md index 4107da6..0f22d10 100644 --- a/docs/01-prerequisites.md +++ b/docs/01-prerequisites.md @@ -1,6 +1,6 @@ # 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. This tutorial is intended for macOS and Linux systems. Other systems may have additional requirements. @@ -8,12 +8,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). @@ -40,7 +40,7 @@ Verify: `go env GOPATH` ## Make -Make is used to run build and development commands throughout this tutorial. +Make is used to run build and development commands throughout the tutorial. ```bash make --version @@ -74,7 +74,7 @@ git --version ## Clone the repository -Clone [cosmos/example](https://github.com/cosmos/example) and navigate into it. +Clone [cosmos/example](https://github.com/cosmos/example) and navigate into it: ```bash git clone https://github.com/cosmos/example diff --git a/docs/02-quickstart.md b/docs/02-quickstart.md index 2fd4c90..cc91160 100644 --- a/docs/02-quickstart.md +++ b/docs/02-quickstart.md @@ -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" ``` diff --git a/docs/03-build-a-module.md b/docs/03-build-a-module.md index 8e9a593..97e29a1 100644 --- a/docs/03-build-a-module.md +++ b/docs/03-build-a-module.md @@ -2,7 +2,7 @@ In [quickstart](./02-quickstart.md), 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](https://docs.cosmos.network/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](https://docs.cosmos.network/sdk/next/learn/concepts/modules). Before continuing, you must follow the [Prerequisites guide](./01-prerequisites.md) to make sure everything is installed. @@ -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 @@ -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 ``` ### Query the chain diff --git a/docs/04-counter-walkthrough.md b/docs/04-counter-walkthrough.md index c487fe9..535839b 100644 --- a/docs/04-counter-walkthrough.md +++ b/docs/04-counter-walkthrough.md @@ -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 } ``` @@ -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 @@ -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: @@ -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 @@ -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 { @@ -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 @@ -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 } ``` @@ -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. @@ -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"}}, + }, }, }, } @@ -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`: diff --git a/docs/05-run-and-test.md b/docs/05-run-and-test.md index e9136cd..740cb98 100644 --- a/docs/05-run-and-test.md +++ b/docs/05-run-and-test.md @@ -31,29 +31,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](https://docs.cosmos.network/sdk/next/learn/concepts/cli-grpc-rest#cli) commands you'll use to inspect state and submit transactions. @@ -84,10 +114,86 @@ exampled tx counter add 10 --from alice --chain-id demo --yes # Add with a gas limit exampled tx counter add 10 --from alice --chain-id demo --gas 200000 --yes -# Update module parameters (requires governance authority) -exampled tx counter update-params --from alice --chain-id demo --yes ``` +### Updating module parameters + +Counter params are governance-gated. `MsgUpdateParams` accepts only the gov module address as its +authority, so there is no direct CLI command for it: signing `update-params` with a user key such as +`alice` always fails with `ErrInvalidSigner`. Params change through a governance proposal instead. + +Look up the gov module address for your chain, which is the only valid authority: + +```bash +exampled query auth module-account gov +``` + +Write a `proposal.json` containing the message, using that address as `authority`. On the local `demo` +chain the value is `cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn`: + +```json +{ + "messages": [ + { + "@type": "/example.counter.MsgUpdateParams", + "authority": "cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn", + "params": { + "max_add_value": "50", + "add_cost": [{"denom": "stake", "amount": "200"}] + } + } + ], + "metadata": "ipfs://CID", + "deposit": "10000000stake", + "title": "Update counter params", + "summary": "Set max_add_value to 50 and add_cost to 200stake" +} +``` + +The `deposit` must meet the chain's `min_deposit`, which is `10000000stake` locally. Check it with +`exampled query gov params`. Then submit and vote: + +```bash +exampled tx gov submit-proposal proposal.json --from alice --chain-id demo --yes +exampled tx gov vote 1 yes --from alice --chain-id demo --yes +``` + +Check progress with `exampled query gov proposals`. + + +The local chain uses the default 48 hour `voting_period`, so a proposal submitted this way sits in +`PROPOSAL_STATUS_VOTING_PERIOD` for two days and the params do not change during a normal dev session. + + +To watch a param change actually take effect locally, shorten the voting period. Editing +`genesis.json` before `make start` does not work, because `scripts/local_node.sh` deletes the whole +home directory on every run. Let `make start` create the chain first, then stop it and edit in place: + +```bash +# 1. Let make start create ~/.exampleapp, then stop it with Ctrl+C +make start + +# 2. Lower both governance voting periods in the generated genesis. +# app_state.gov.params.voting_period, for example "20s" +# app_state.gov.params.expedited_voting_period must stay strictly shorter, for example "10s" +vi ~/.exampleapp/config/genesis.json + +# 3. Wipe block history so the edited genesis is re-read, keeping keys and config +exampled comet unsafe-reset-all + +# 4. Start the node directly. Do not use make start again, it would delete your edit +exampled start +``` + +Submit and vote as above, wait out the shortened period, and the proposal reaches +`PROPOSAL_STATUS_PASSED` and `exampled query counter params` reflects the new values. + +`exampled tx gov draft-proposal` can generate a skeleton, but it is an interactive terminal picker rather +than a scriptable command. Its top-level list offers only `text`, `community-pool-spend`, +`software-upgrade`, `cancel-software-upgrade`, and `other`, and choosing `other` opens a scroll-only list +of fully qualified message type URLs that typing does not filter. Writing the JSON by hand, as above, is +the more direct path. + ### Useful flags These flags are the ones you'll use most often while iterating locally. @@ -117,9 +223,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 @@ -130,7 +236,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 | @@ -205,6 +311,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. @@ -225,9 +337,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 |