区块链【免费下载链接】cosmos-sdkFramework for building performant, customizable blockchains with native interoperability项目地址https://gitcode.com/gh_mirrors/co/cosmos-sdk点击查看免费下载构建一条基于 Cosmos SDK 的链是一项复杂的工程需要大量的真实环境验证。应用测试网Application Testnet是 Cosmos SDK 提供的一种“就地in-place”测试方案直接把主网mainnet的状态快照作为起点运行一条由单验证者控制的本地测试网用于验证升级迁移upgrade migrations、在贴近真实的数据和环境下调试应用逻辑。读完本文你将掌握InitSimAppForTestnet的状态改造方法论Staking / Distribution / Slashing / Bank / Upgrade 五大模块、newTestnetApp的接线方式以及in-place-testnet命令的完整使用流程。本文以 docs/docs/build/building-apps/05-app-testnet.md 为核心结合仓库内 server/util.go、server/start.go、simapp/simd/cmd/commands.go 等源码展开确保每个改造步骤都有实现依据。为什么要用主网状态来创建测试网文档开宗明义地指出构建应用复杂且需要大量测试而 Cosmos SDK 允许开发者获取主网的状态并针对该状态运行测试。这种做法的价值在于升级迁移测试链上升级尤其是引入新 store、改变存储布局的硬分叉往往只能在与真实数据分布一致的条件下才能暴露问题真实环境验证主网状态包含大量真实账户、余额、委托与治理记录测试网直接继承这些数据可以复现“真实世界”中的边界情况无需重放历史测试网从主网当前的区块高度继续出块而不是从头同步节省大量时间。从仓库实现看这一能力的底层载体是in-place-testnet子命令与testnetify状态改造流程。InPlaceTestnetCreator的命令注释明确写道该命令利用提供的 chainID、operator 地址以及本地 priv_validator 私钥控制数据目录中所代表的网络从而创建与主网环境几乎一致的测试网见 server/start.go。测试网设置Testnet Setup的整体思路文档将创建测试网的过程归纳为一个核心函数// InitSimAppForTestnet is broken down into two sections: // Required Changes: Changes that, if not made, will cause the testnet to halt or panic // Optional Changes: Changes to customize the testnet to ones liking (lower vote times, fund accounts, etc) func InitSimAppForTestnet(app *SimApp, newValAddr bytes.HexBytes, newValPubKey crypto.PubKey, newOperatorAddress, upgradeToTrigger string) *SimApp { ... }该函数接收 5 个关键参数参数类型含义app*SimApp从主网状态加载的原始应用实例newValAddrbytes.HexBytes新验证者的共识地址consensus addressnewValPubKeycrypto.PubKey新验证者的共识公钥用于构建共识层密钥newOperatorAddressstring新验证者的运营商地址operator addressbech32 格式upgradeToTriggerstring可选要触发的升级名称为空则不触发改造被明确划分为两类Required Changes必需修改不做就会导致测试网停机halt或 panic。核心是替换验证者集validator set并同步更新分布、惩罚、银行相关状态Optional Changes可选修改按需定制测试网体验例如降低投票时间、给账户注资、适配自定义模块的状态。Required Changes必需修改Staking将验证者集迁移为单验证者创建测试网最重要的步骤是把验证者集从“多个验证者”迁移为“一个或少数几个”。这样开发者可以直接用本地私钥启动链无需替换验证者密钥。文档中的实现分四步第一步构造新验证者对象ctx : app.BaseApp.NewUncachedContext(true, tmproto.Header{}) pubkey : ed25519.PubKey{Key: newValPubKey.Bytes()} pubkeyAny, err : types.NewAnyWithValue(pubkey) if err ! nil { tmos.Exit(err.Error()) } // STAKING // // Create Validator struct for our new validator. _, bz, err : bech32.DecodeAndConvert(newOperatorAddress) if err ! nil { tmos.Exit(err.Error()) } bech32Addr, err : bech32.ConvertAndEncode(simvaloper, bz) if err ! nil { tmos.Exit(err.Error()) } newVal : stakingtypes.Validator{ OperatorAddress: bech32Addr, ConsensusPubkey: pubkeyAny, Jailed: false, Status: stakingtypes.Bonded, Tokens: sdk.NewInt(900000000000000), DelegatorShares: sdk.MustNewDecFromStr(10000000), Description: stakingtypes.Description{ Moniker: Testnet Validator, }, Commission: stakingtypes.Commission{ CommissionRates: stakingtypes.CommissionRates{ Rate: sdk.MustNewDecFromStr(0.05), MaxRate: sdk.MustNewDecFromStr(0.1), MaxChangeRate: sdk.MustNewDecFromStr(0.05), }, }, MinSelfDelegation: sdk.OneInt(), }关键点解读OperatorAddress前缀写死为simvaloper对应 simapp 的验证者前缀若你的链使用不同前缀需相应替换Status直接设为stakingtypes.Bonded已绑定Tokens 9000000000000009 × 10¹⁴作为新验证者的投票权voting power来源这一数值与后续testnetify中构造 CometBFT Validator 的VotingPower: 900000000000000完全对应见 server/start.go保证 SDK 层与 CometBFT 层的权力一致佣金率设置为Rate 5% / MaxRate 10% / MaxChangeRate 5%MinSelfDelegation为 1。第二步清空旧的权力存储power store与最后一个验证者存储// Remove all validators from power store stakingKey : app.GetKey(stakingtypes.ModuleName) stakingStore : ctx.KVStore(stakingKey) iterator : app.StakingKeeper.ValidatorsPowerStoreIterator(ctx) for ; iterator.Valid(); iterator.Next() { stakingStore.Delete(iterator.Key()) } iterator.Close() // Remove all validators from last validators store iterator app.StakingKeeper.LastValidatorsIterator(ctx) for ; iterator.Valid(); iterator.Next() { app.StakingKeeper.LastValidatorPower.Delete(iterator.Key()) } iterator.Close()通过ValidatorsPowerStoreIterator与LastValidatorsIterator遍历并删除主网原有验证者的权力索引确保新验证者是唯一被记录的对象。第三步写入新验证者// Add our validator to power and last validators store app.StakingKeeper.SetValidator(ctx, newVal) err app.StakingKeeper.SetValidatorByConsAddr(ctx, newVal) if err ! nil { panic(err) } app.StakingKeeper.SetValidatorByPowerIndex(ctx, newVal) app.StakingKeeper.SetLastValidatorPower(ctx, newVal.GetOperator(), 0) if err : app.StakingKeeper.Hooks().AfterValidatorCreated(ctx, newVal.GetOperator()); err ! nil { panic(err) }依次调用SetValidator、SetValidatorByConsAddr、SetValidatorByPowerIndex、SetLastValidatorPower将新验证者注册进 staking 的各索引存储并触发AfterValidatorCreated钩子hook通知依赖 staking 钩子的其他模块如 distribution 自动记录的验证者历史奖励。Distribution为新验证者初始化分配记录由于验证者集发生了变化必须为新验证者更新 distribution 各存储中的记录否则在分配奖励时会出现状态缺失// Initialize records for this validator across all distribution stores app.DistrKeeper.ValidatorHistoricalRewards.Set(ctx, newVal.GetOperator(), 0, distrtypes.NewValidatorHistoricalRewards(sdk.DecCoins{}, 1)) app.DistrKeeper.ValidatorCurrentRewards.Set(ctx, newVal.GetOperator(), distrtypes.NewValidatorCurrentRewards(sdk.DecCoins{}, 1)) app.DistrKeeper.ValidatorAccumulatedCommission.Set(ctx, newVal.GetOperator(), distrtypes.InitialValidatorAccumulatedCommission()) app.DistrKeeper.ValidatorOutstandingRewards.Set(ctx, newVal.GetOperator(), distrtypes.ValidatorOutstandingRewards{Rewards: sdk.DecCoins{}})这四行分别初始化ValidatorHistoricalRewards历史奖励记录周期 01 个参考计数ValidatorCurrentRewards当前周期奖励ValidatorAccumulatedCommission累计佣金使用InitialValidatorAccumulatedCommission()初始值ValidatorOutstandingRewards未结算的待分配奖励。Slashing设置新验证者的签名信息Slashing 模块同样需要为新验证者建立签名信息否则节点启动后无法正常执行签名计数与惩罚逻辑// SLASHING // // Set validator signing info for our new validator. newConsAddr : sdk.ConsAddress(newValAddr.Bytes()) newValidatorSigningInfo : slashingtypes.ValidatorSigningInfo{ Address: newConsAddr.String(), StartHeight: app.LastBlockHeight() - 1, Tombstoned: false, } app.SlashingKeeper.ValidatorSigningInfo.Set(ctx, newConsAddr, newValidatorSigningInfo)要点StartHeight设为app.LastBlockHeight() - 1即测试网开始前的一个高度这是为了让 slashing 的缺席计数absent validator从测试网启动的边界正确开始Tombstoned保持false避免新验证者被标记为已“墓碑化”而无法参与共识。Bank为主网之外的新账户注资在测试网中为自定义账户直接铸造并转移代币可以避免沿用主网密钥从而避免密钥泄露风险方便测试治理提案、转账等操作// BANK // defaultCoins : sdk.NewCoins(sdk.NewInt64Coin(ustake, 1000000000000)) localSimAppAccounts : []sdk.AccAddress{ sdk.MustAccAddressFromBech32(cosmos12smx2wdlyttvyzvzg54y2vnqwq2qjateuf7thj), sdk.MustAccAddressFromBech32(cosmos1cyyzpxplxdzkeea7kwsydadg87357qnahakaks), sdk.MustAccAddressFromBech32(cosmos18s5lynnmx37hq4wlrw9gdn68sg2uxp5rgk26vv), sdk.MustAccAddressFromBech32(cosmos1qwexv7c6sm95lwhzn9027vyu2ccneaqad4w8ka), sdk.MustAccAddressFromBech32(cosmos14hcxlnwlqtq75ttaxf674vk6mafspg8xwgnn53), sdk.MustAccAddressFromBech32(cosmos12rr534cer5c0vj53eq4y32lcwguyy7nndt0u2t), sdk.MustAccAddressFromBech32(cosmos1nt33cjd5auzh36syym6azgc8tve0jlvklnq7jq), sdk.MustAccAddressFromBech32(cosmos10qfrpash5g2vk3hppvu45x0g860czur8ff5yx0), sdk.MustAccAddressFromBech32(cosmos1f4tvsdukfwh6s9swrc24gkuz23tp8pd3e9r5fa), sdk.MustAccAddressFromBech32(cosmos1myv43sqgnj5sm4zl98ftl45af9cfzk7nhjxjqh), sdk.MustAccAddressFromBech32(cosmos14gs9zqh8m49yy9kscjqu9h72exyf295afg6kgk), sdk.MustAccAddressFromBech32(cosmos1jllfytsz4dryxhz5tl7u73v29exsf80vz52ucc)} // Fund localSimApp accounts for _, account : range localSimAppAccounts { err : app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, defaultCoins) if err ! nil { tmos.Exit(err.Error()) } err app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, account, defaultCoins) if err ! nil { tmos.Exit(err.Error()) } }实现细节每个账户先通过MintCoins在minttypes.ModuleName模块账户中铸造1000000000000ustake1 × 10¹²再经SendCoinsFromModuleToAccount转入目标账户。地址前缀为cosmos的示例地址可按你的链配置替换。Upgrade可选地在测试网上调度升级文档提供了一段“如果希望调度升级”的模板// UPGRADE // if upgradeToTrigger ! { upgradePlan : upgradetypes.Plan{ Name: upgradeToTrigger, Height: app.LastBlockHeight(), } err app.UpgradeKeeper.ScheduleUpgrade(ctx, upgradePlan) if err ! nil { panic(err) } }ScheduleUpgrade在 x/upgrade/keeper/keeper.go 中有完整实现它会先执行plan.ValidateBasic()校验若当前已有其他 Plan 则取消并覆盖之且会拒绝调度“发生在过去”的升级plan.Height sdkCtx.HeaderInfo().Height时返回错误。这里将升级高度设为app.LastBlockHeight()即让升级处理在测试网启动后的第一个区块被触发。需要特别说明的是不设置该标志新引入的 store 也会被自动注册以防 panic——--trigger-testnet-upgrade标志仅在你确实想测试升级 handler 本身时才需要设置见 server/start.go 的命令 Long 说明。Optional Changes可选修改对于依赖上述模块特定状态的自定义模块或者你想直接测试自定义模块时需要在InitSimAppForTestnet中补充对自定义模块状态的改写例如修改治理模块的投票参数缩短投票周期为自定义模块的存储写入测试数据调整共识参数如区块时间、最大 gas以加快测试节奏。文档强调这些改动“按需进行”不影响测试网能否启动但决定了测试体验与测试覆盖面。接线将测试网能力挂到 CLI 上改造函数写好之后还需要把它接入 CLI让in-place-testnet命令能够调用。文档给出了两处 diff。第一处在root.go的initRootCmd中注册命令server.AddCommands(rootCmd, simapp.DefaultNodeHome, newApp, createSimAppAndExport, addModuleInitFlags) server.AddTestnetCreatorCommand(rootCmd, simapp.DefaultNodeHome, newTestnetApp, addModuleInitFlags)AddTestnetCreatorCommand的实现在 server/util.go它调用InPlaceTestnetCreator(appCreator)构造in-place-testnet子命令并通过addStartFlags附加模块初始化参数后挂到根命令下。在 simapp 的实际代码中根命令注册位于 simapp/simd/cmd/commands.go。第二处新增newTestnetApp辅助函数// newTestnetApp starts by running the normal newApp method. From there, the app interface returned is modified in order // for a testnet to be created from the provided app. func newTestnetApp(logger log.Logger, db cometbftdb.DB, appOpts servertypes.AppOptions) servertypes.Application { // Create an app and type cast to an SimApp app : newApp(logger, db, appOpts) simApp, ok : app.(*simapp.SimApp) if !ok { panic(app created from newApp is not of type simApp) } newValAddr, ok : appOpts.Get(server.KeyNewValAddr).(bytes.HexBytes) if !ok { panic(newValAddr is not of type bytes.HexBytes) } newValPubKey, ok : appOpts.Get(server.KeyUserPubKey).(crypto.PubKey) if !ok { panic(newValPubKey is not of type crypto.PubKey) } newOperatorAddress, ok : appOpts.Get(server.KeyNewOpAddr).(string) if !ok { panic(newOperatorAddress is not of type string) } upgradeToTrigger, ok : appOpts.Get(server.KeyTriggerTestnetUpgrade).(string) if !ok { panic(upgradeToTrigger is not of type string) } // Make modifications to the normal SimApp required to run the network locally return simapp.InitSimAppForTestnet(simApp, newValAddr, newValPubKey, newOperatorAddress, upgradeToTrigger) }该函数的工作流程非常清晰先通过newApp正常构造应用并断言类型为*simapp.SimApp从appOpts底层是 Viper中取出四个由框架注入的键值——这正是InitSimAppForTestnet所需的参数调用InitSimAppForTestnet完成就地状态改造。这里的四个键定义于 server/start.go键常量值含义KeyIsTestnetis-testnet标记当前运行模式为测试网KeyNewChainIDnew-chain-ID测试网的新链 IDKeyNewOpAddrnew-operator-addr新验证者运营商地址KeyNewValAddrnew-validator-addr新验证者共识地址KeyUserPubKeyuser-pub-key本地 priv_validator 的公钥KeyTriggerTestnetUpgradetrigger-testnet-upgrade要触发的升级名称运行测试网in-place-testnet 命令接线完成后通过in-place-testnet子命令即可一键把当前数据目录中的主网状态“就地”改造成测试网并启动simd in-place-testnet [newChainID] [newOperatorAddress]例如 server/start.go 中给出的示例in-place-testnet localosmosis osmo12smx2wdlyttvyzvzg54y2vnqwq2qjateuf7thj命令行为要点依据InPlaceTestnetCreator的实现与命令 Long 描述命令接收恰好两个位置参数cobra.ExactArgs(2)新链 ID 与新验证者运营商地址命令执行前默认有确认提示“This operation will modify state in your data folder and cannot be undone. Do you want to continue? (y/n)”可用--skip-confirmation跳过——这是为了防止误操作破坏数据目录见 server/start.go可通过--trigger-testnet-upgrade v21指定在测试网第一个区块运行某个升级 handler不指定时新引入的 store 也会被自动注册以避免 panic命令会修改数据目录内的状态与 blockStore不可逆将 chainID 改写为新链 ID、清理旧网络的addrbook.json、重写 blockStore 中的 commit / seenCommit / ValidatorSet 条目使本地 priv_validator 成为唯一验证者。完整的底层改造流程由testnetify完成见 server/start.go核心步骤包括修改 genesis 文件的 chainID 并重新保存删除并重建addrbook.json防止旧网络 peer 造成错误日志从 priv_validator 文件加载本地共识密钥计算validatorAddress通过临时 proxyApp 查询应用当前高度与 AppHash对齐应用层与 blockStore 高度差异例如 halt-height 停止与 SIGTERM 停止两种场景的处理路径不同将最新区块的LastCommit与SeenCommit重签为仅由本地验证者签名构造只含本地验证者的ValidatorSetVotingPower: 900000000000000并同步改写 stateDB 中validatorsKey的当前 / 上一 / 下一高度条目将新 genesisDoc 写入 stateDB 的genesisDoc键。此外命令的 Long 描述特别提醒两个工程细节见 server/start.go测试网启动后的第一个区块可能耗时最长可达一分钟如果快照距今较久大量待结算状态如过期的锁仓需要一次性提交建议等第一个区块出完后再停止 daemon命令只能对未改造过的状态使用一次网络停止后应改用常规的start命令对已改造过的状态再次使用该命令可能导致不可预期的行为。与经典多验证者测试网的对比仓库中还存在另一条构建测试网的路径通过simd testnet子命令见 simapp/simd/cmd/testnet.go初始化一套全新的多验证者测试网相关使用步骤记录在 simapp/README.mdmake build→simd init→keys add→genesis add-genesis-account→gentx→collect-gentxs→ 配置persistent_peers→simd start。两者定位不同testnet经典方式从零初始化适合功能开发阶段的快速联调状态与主网无关in-place-testnet就地方式直接继承主网数据目录的状态适合升级迁移验证与贴近真实环境的测试这正是本文所讲解的方案。常见问题与排错建议基于文档与源码实现汇总以下实操要点验证者集不匹配导致 panic若 Staking 改造不完整如遗漏SetValidatorByConsAddr应用层与 CometBFT 层验证者集不一致启动时可能 panic——所有 “Required Changes” 都必须执行slashing 状态缺失ValidatorSigningInfo的StartHeight使用app.LastBlockHeight() - 1而不是当前高度是为了让签名计数的窗口正确衔接测试网首个区块升级无法调度ScheduleUpgrade拒绝“过去”的高度且不允许重复名称的升级若upgradeToTrigger对应的 handler 未注册ApplyUpgrade会直接报错“ApplyUpgrade should never be called without first checking HasHandler”见 x/upgrade/keeper/keeper.go因此请确保测试网使用的二进制中已注册对应升级 handler数据不可逆命令会就地改写数据目录建议先对数据目录做快照或备份再执行in-place-testnet。小结应用测试网是 Cosmos SDK 提供的“从主网到测试网”的一键能力通过InitSimAppForTestnet完成 Staking / Distribution / Slashing / Bank / Upgrade 五个模块的状态改造将验证者集收敛为单验证者再由newTestnetApp与AddTestnetCreatorCommand将改造逻辑接入 CLI最终以simd in-place-testnet [newChainID] [newOperatorAddress]一键运行。该方案让开发者能在与主网几乎一致的数据环境下安全地验证升级迁移与真实业务逻辑是链上应用上线前测试的重要一环。赞分享区块链【免费下载链接】cosmos-sdkFramework for building performant, customizable blockchains with native interoperability项目地址https://gitcode.com/gh_mirrors/co/cosmos-sdk点击查看免费下载相关推荐从0到1开发自定义协议ESP32 Bus Pirate贡献指南与命令扩展实例从0到1开发自定义协议ESP32 Bus Pirate贡献指南与命令扩展实例 ESP32 Bus Pirate是一款功能强大的硬件黑客工具它支持多种协议并提嵌入式硬件开发Portmaster SPN 测试网实战指南用 Docker 与 Shadow 搭建 Port17 本地测试环境Portmaster SPN 测试网实战指南用 Docker 与 Shadow 搭建 Port17 本地测试环境 Portmaster 的 SPNSafe网络安全如何快速搭建Cosmos SDK多节点测试网完整入门指南如何快速搭建Cosmos SDK多节点测试网完整入门指南 Cosmos SDK是一个功能强大的区块链开发框架允许开发者构建高性能的公共区块链网络。本文将详细区块链创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考