Skip to content

Hardening: network-duty fee-bypass admits zero-fee txs from non-participants (no signer authorization at the ante layer) #1539

Open @kAIPraxisBot opened 2026-08-03 19:33 UTC 1 comment Updated 2026-09-18 19:30 UTC

Summary

NetworkDutyFeeBypassDecorator waives fees + clears min-gas-price for ~12 "network duty" message types, and the exemption is decided purely by Go message type (isExemptMessageType in inference-chain/app/ante_fee.go) — there is no check at the ante/CheckTx layer that the signer is an active participant / authorized host. So any funded account can submit structurally-valid, zero-fee duty-typed transactions. They pass CheckTx, enter the mempool, occupy block space, incur secp256k1 signature verification on every validator, and are rejected only later in DeliverTx (e.g. participant is not active).

The real authorization for these types (allowlist / participant / dedup / deadline) lives in the message handlers, which run in DeliverTx — i.e. after mempool admission and block inclusion. CheckTx runs the ante only, so unauthorized duty-typed txs are admitted regardless.

Where

  • inference-chain/app/ante_fee.goNetworkDutyFeeBypassDecorator / isExemptMessageType / GonkaFeeChecker. On main, the exempt set is 12 types: MsgSubmitPocBatch, MsgSubmitPocValidationsV2, MsgMLNodeWeightDistribution, MsgSubmitSeed, MsgSubmitHardwareDiff, MsgClaimRewards, MsgSettleDevshardEscrow, and the 5 BLS DKG types.
  • inference-chain/app/ante_poc_period.goPocPeriodValidationDecorator gates only timing (checkPocMessageTooLate) and only for the 4 PoC types; it does not check the signer. The other 8 exempt types have no ante-layer gate at all.

Impact (bounded — hardening, not a critical exploit)

An attacker imposes free, unauthenticated load: zero-fee inclusion consuming validator sig-verify + gossip + mempool + block bytes, with no economic cost (balance never drains; one-time dust to fund accounts).

Honest severity limits, verified against live mainnet consensus params (max_gas = -1, max_bytes = 22 MB): - Not a practical throughput DoS: with unlimited block gas, saturating a block is bounded only by the ~22 MB byte limit, which (given CheckTx enforces the account sequence → one pending tx per account) would require on the order of tens of thousands of concurrently-funded accounts. - The Priority: 10_000_000 boost is inert for block ordering: the app uses the default NoOpMempool, so PrepareProposal builds blocks in CometBFT FIFO order and never consults the priority. No priority-based censorship of paid traffic.

So this is a resource-abuse / free-spam hardening gap, not a network-halting exploit.

Reproduction

A zero-fee MsgClaimRewards signed by a non-participant account: CheckTx returns code 0 (admitted), the tx is included in a block, and only then fails in DeliverTx with participant is not active — having consumed block gas at zero fee.

Suggested fix

Add an ante-layer signer-authorization check for the exempt types (reject duty-typed txs from non-participants / non-allowlisted signers at CheckTx), and/or withhold the fee-bypass + priority until the signer is authorized. This mirrors the participant check that already protects MsgValidation, and closes the gap where the only authorization runs in DeliverTx after admission.


💬 Comments (1)

@Ryanchen911 commented 2026-08-11 01:51 UTC

I verified this end-to-end against upgrade-v0.2.16 (6e92e9089) and can confirm the report. I'd like to take it.

Confirmed

Ante ordering. In NewAnteHandler (inference-chain/app/ante.go:195) NetworkDutyFeeBypassDecorator sits at index 11 and ante.NewSigVerificationDecorator at index 19 — the fee-waiver decision is made before the signature is verified, so the exemption is granted on unauthenticated input.

No signer gate. isExemptMessageType (ante_fee.go:116-160) switches on the Go type only. isNetworkDuty unwraps one authz.MsgExec level but likewise never looks at who signed.

Authorization really does live only in DeliverTx. Per MessagePermissions (x/inference/keeper/permissions.go:84) and the handlers:

exempt type handler permission
MsgClaimRewards ActiveParticipant OR PreviousActiveParticipant (msg_server_claim_rewards.go:23)
MsgSubmitPocBatch, MsgSubmitSeed, MsgSubmitHardwareDiff ParticipantPermission
MsgSettleDevshardEscrow EscrowAllowListPermission
MsgSubmitPocValidationsV2, MsgMLNodeWeightDistribution NoPermission — only a blocklist check on msg.Creator
5 BLS DKG types in-handler scan of epochBLSData.Participants

Inert priority confirmed. There is no SetMempool call anywhere in app/, so the default NoOpMempool is in effect and Priority: 10_000_000 never influences block ordering — matching your severity analysis.

One constraint the report doesn't mention, and it decides the fix

A naive "check the tx signer is a participant" would break production. In warm-key mode the DAPI wraps duty messages in authztypes.NewMsgExec(granteeAddress, msgs) (tx_manager.go:833) while setting transaction.Creator = icc.Address, the cold account (cosmosclient.go:409). The tx signer is the grantee; the protocol actor is the Creator/Settler field. The ante check must read the message field, not the signer.

Two more invariants I checked, which decide which predicate is safe:

  • ActiveParticipantsSet only retains ~2 epochs — module.go:862-868 clears epoch-2. So gating on active would reject legitimate MsgClaimRewards from a previous-epoch participant and BLS DKG traffic for the epoch being rotated.
  • Participants is effectively append-only: RemoveParticipant has no production caller, and every ActiveParticipant is constructed from a registered one (chainvalidation.go:220). Registration is therefore a strict superset of every handler's requirement — it cannot reject traffic DeliverTx would accept.

Proposed fix

Take the report's second option — withhold the fee bypass and the priority boost when the actor is not authorized, rather than rejecting the tx outright:

  • authorized actor → unchanged behaviour, still zero-fee;
  • unauthorized actor → no waiver, so GonkaFeeChecker applies MinGasPriceNgonka and an unfunded zero-fee spam tx fails CheckTx on ErrInsufficientFee and never reaches the mempool.

This closes the free-load gap while keeping a false positive on my side from ever halting consensus-critical PoC/BLS traffic — an outright reject would make a mistaken predicate a liveness bug. Predicate: Participants.Has(Creator) for the participant-gated and BLS types, IsAllowedEscrowCreator(Settler) for MsgSettleDevshardEscrow, fail-closed on a nil keeper or unparseable address, applied inside the MsgExec unwrap as well.

Tests: end-to-end CheckTx via BaseApp (registered vs. unregistered actor, direct and MsgExec-wrapped, following the ante_bridge_checktx_test.go harness) plus unit coverage for the actor extraction and the escrow allowlist branch.

Will open a PR against upgrade-v0.2.16.


🔄 Auto-synced from Issue #1539 every hour.