fix(at): waive AT step fees when the chain has no native asset - #187
Conversation
deployAtWorkingAssetHeight let non-native-working ATs deploy on a chain without a native asset, but the runtime half was missing: willExecute() still demanded a native fee balance, getMaxStepsPerRound() budgeted zero steps, and calcStepFees() still billed steps x feePerStep in NATIVE - so a deployed AT could never take its first round, while reporting non-fatal/not-finished. Add the atNoNativeAssetFeeWaiverHeight feature trigger (80,000 on Previewnet): from that height, while the native asset does not exist, the effective feePerStep is zero. All step-fee sites price through one effectiveFeePerStep() helper keyed off the consensus-anchored gate height, so updated nodes cannot disagree with each other on per-AT or per-block fee totals. Chains whose native asset exists are unchanged even beyond the trigger (guarded suite-wide by activating the trigger in test-chain-v2.json, whose genesis has NATIVE). ATs deployed while the bug was live start executing on their own at the activation height and process their already-confirmed queued messages; nothing needs resending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codex consensus review of the waiver flagged that the native asset is not permanently absent: IssueAssetTransaction allows a development- group-approved runtime bootstrap of assetId 0. Cover the transition: the AT runs fee-free up to the bootstrap, is starved once fees resume (zero native balance, even when transactions arrive), and charges real step fees again once native-funded. Mutation-verified: ignoring asset existence in the waiver fails this test and the genesis-native guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Dual-model review record: an adversarial Codex consensus review of It flagged one low-severity gap, addressed in |
Problem
deployAtWorkingAssetHeight(60,000) let non-native-working ATs deploy on a chain without a native asset, but the runtime half was missing. Three sites still priced execution in NATIVE:ChainATAPI.hasNativeFeeBalance()— gateswillExecute(), so the AT never takes a roundgetMaxStepsPerRound()— budgets steps against the native balance (0 balance → 0 steps)calcStepFees()/calcFinalFees()— billssteps × feePerStepin NATIVE, debited byBlock.processAtFeesAndStatesOn Previewnet nothing can ever fund an AT's native balance, so the deployed SMPL faucet AT (
AG9QWs1tEBTmXoH2rrQXwV4LdMAM99o5WD) sat idle through both confirmed claim MESSAGEs (blocks 73,473 / 73,477) while reporting non-fatal/not-finished. TheDeployAtTransactioncomment claiming the runtime already waived these fees described only the VM-facinggetFeePerStep()hook and has been corrected.Fix
New feature trigger
atNoNativeAssetFeeWaiverHeight= 80,000 on Previewnet (~2026-08-05 at the observed ~1,040 blocks/day). From that height, whileassetExists(Asset.NATIVE)is false — the same detection the deploy-side fix uses — the effectivefeePerStepis zero.All step-fee sites price through one
effectiveFeePerStep()helper keyed off the consensus-anchored gate height, so no site can be waived while another still charges. Zero fees no-op cleanly in block fee accounting (modifyAssetBalancereturns early on 0). Chains whose native asset exists are unchanged even beyond the trigger; blocks below the trigger replay byte-for-byte. The pinned AT jar needs no change (getFeePerStep()already returns 0 for non-native ATs).blockchain.jsondeliberately omits the trigger (missing key → disabled).Rollout
featureTriggers are excluded from
getChainConfigHash, so peering is unaffected — but every minting node must run this build before height 80,000. After activation the deployed SMPL AT self-recovers: its first round runs at the trigger block and the two queued MESSAGEs are processed in successive rounds, nothing to resend.Tests
ATNoNativeAssetFeeWaiverTests(new no-native chain config, trigger at 10): AT skipped every round below the trigger, executes fee-free from the trigger block (fees 0, working-asset funding untouched, native asset still absent); with no trigger configured it stays skipped forever (pins today's live behavior); on the default chain, fees still charged with the trigger active.test-chain-v2.jsonnow activates the trigger from 0 with NATIVE in genesis, so the entire existing AT suite continuously guards the native-chain-unchanged property.🤖 Generated with Claude Code