Hardening: network-duty fee-bypass admits zero-fee txs from non-participants (no signer authorization at the ante layer) #1539
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.go—NetworkDutyFeeBypassDecorator/isExemptMessageType/GonkaFeeChecker. Onmain, 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.go—PocPeriodValidationDecoratorgates 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)
🔄 Auto-synced from Issue #1539 every hour.
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)NetworkDutyFeeBypassDecoratorsits at index 11 andante.NewSigVerificationDecoratorat 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.isNetworkDutyunwraps oneauthz.MsgExeclevel but likewise never looks at who signed.Authorization really does live only in
DeliverTx. PerMessagePermissions(x/inference/keeper/permissions.go:84) and the handlers:MsgClaimRewardsActiveParticipantORPreviousActiveParticipant(msg_server_claim_rewards.go:23)MsgSubmitPocBatch,MsgSubmitSeed,MsgSubmitHardwareDiffParticipantPermissionMsgSettleDevshardEscrowEscrowAllowListPermissionMsgSubmitPocValidationsV2,MsgMLNodeWeightDistributionNoPermission— only a blocklist check onmsg.CreatorepochBLSData.ParticipantsInert priority confirmed. There is no
SetMempoolcall anywhere inapp/, so the defaultNoOpMempoolis in effect andPriority: 10_000_000never 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 settingtransaction.Creator = icc.Address, the cold account (cosmosclient.go:409). The tx signer is the grantee; the protocol actor is theCreator/Settlerfield. The ante check must read the message field, not the signer.Two more invariants I checked, which decide which predicate is safe:
ActiveParticipantsSetonly retains ~2 epochs —module.go:862-868clearsepoch-2. So gating on active would reject legitimateMsgClaimRewardsfrom a previous-epoch participant and BLS DKG traffic for the epoch being rotated.Participantsis effectively append-only:RemoveParticipanthas no production caller, and everyActiveParticipantis constructed from a registered one (chainvalidation.go:220). Registration is therefore a strict superset of every handler's requirement — it cannot reject trafficDeliverTxwould 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:
GonkaFeeCheckerappliesMinGasPriceNgonkaand an unfunded zero-fee spam tx failsCheckTxonErrInsufficientFeeand 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
rejectwould make a mistaken predicate a liveness bug. Predicate:Participants.Has(Creator)for the participant-gated and BLS types,IsAllowedEscrowCreator(Settler)forMsgSettleDevshardEscrow, fail-closed on a nil keeper or unparseable address, applied inside theMsgExecunwrap as well.Tests: end-to-end
CheckTxviaBaseApp(registered vs. unregistered actor, direct andMsgExec-wrapped, following theante_bridge_checktx_test.goharness) plus unit coverage for the actor extraction and the escrow allowlist branch.Will open a PR against
upgrade-v0.2.16.