0Abstract
We found a critical vulnerability in CometBFT that enables a single malicious validator to arbitrarily increase block timestamps, leading to either:
- Unbounded minting of native tokens on chains with staking (Babylon, Celestia), allowing 100% of the chains’ whole TVL to be extorted, and/or
- Permanent network halt (40+ chains)
This issue affects all versions of CometBFT and was patched on January 21, 2026.
We encountered this bug while running our bug-hunting and exploitation tool on Celestia, which produced proof-of-concept results. After a brief triage, we determined that the exploited logic was not in Celestia’s application layer, but in the CometBFT consensus logic, and quickly realized that the exploit could apply to most (40+) Cosmos-based chains. While investigating them, we further observed that this issue could be escalated into direct asset theft by abusing staking reward mechanics.
1Part 1: Technical background
1.1BFT time in CometBFT
CometBFT implements Byzantine Fault Tolerant (BFT) time to ensure that block timestamps are derived from a weighted median of validator votes, preventing any single faulty validator from arbitrarily manipulating time.
The key guarantee is that:
Block time is computed as a weighted median of precommit timestamps, where each validator’s timestamp is weighted by their voting power.
This should ensure that even with up to 1/3 Byzantine voting power, the median timestamp remains within bounds controlled by honest validators.
1.2Commit and commit signatures
type Commit struct { // NOTE: The signatures are in order of address to preserve the bonded // ValidatorSet order. // Any peer with a block can gossip signatures by index with a peer without // recalculating the active ValidatorSet. Height int64 `json:"height"` Round int32 `json:"round"` BlockID BlockID `json:"block_id"` Signatures []CommitSig `json:"signatures"`
// Memoized in first call to corresponding method. // NOTE: can't memoize in constructor because constructor isn't used for // unmarshaling. hash cmtbytes.HexBytes}
type CommitSig struct { BlockIDFlag BlockIDFlag `json:"block_id_flag"` ValidatorAddress Address `json:"validator_address"` Timestamp time.Time `json:"timestamp"` Signature []byte `json:"signature"`}In CometBFT, a Commit is the finalized proof that a block was accepted, bundling the block ID with a set of validator Commit Signatures. Each commit signature is a validator’s signed vote (with a timestamp and voting power) attesting to that block, and together they prove that >2/3 of the validator set agreed on the block.
2Part 2: The bug
In short, the bug manifests in two different places:
- Commit signature verification uses the validator index
- Median time computation uses the validator address and skips the validator if the address is not present in the current validator set
The following code snippet is an abridged version of the vulnerable functions, with the vulnerable branches being [1] and [2]:
func verifyCommitBatch(/* .. */){:go} for idx, commitSig := range commit.Signatures { if ignoreSig(commitSig) { continue }
if lookUpByIndex { val = vals.Validators[idx] /* [1] */ } voteSignBytes := commit.VoteSignBytes(chainID, int32(idx)) if err := bv.Add(val.PubKey, voteSignBytes, commitSig.Signature); err != nil { return err } }}func MedianTime(commit *types.Commit, validators *types.ValidatorSet) time.Time { weightedTimes := make([]*cmttime.WeightedTime, len(commit.Signatures)) totalVotingPower := int64(0)
for i, commitSig := range commit.Signatures { if commitSig.BlockIDFlag == types.BlockIDFlagAbsent { continue } _, validator := validators.GetByAddress(commitSig.ValidatorAddress) if validator != nil { /* [2] */ totalVotingPower += validator.VotingPower weightedTimes[i] = cmttime.NewWeightedTime(commitSig.Timestamp, validator.VotingPower) } }
return cmttime.WeightedMedian(weightedTimes, totalVotingPower)}2.1Commit verification: index-based lookup
When verifying a block’s commit signatures, CometBFT uses verifyCommitBatch (or verifyCommitSingle) with lookUpByIndex=true.
The validator is identified solely by index, meaning that the commitSig.ValidatorAddress field is not verified against val.Address. Since lookUpByIndex is always true in normal commit verification (VerifyCommit() → verifyCommitBatch(..., lookUpByIndex = true)), every block takes the vulnerable path.
2.2MedianTime: address-based lookup
In contrast, when computing block time, MedianTime uses address-based lookup.
3Part 3: Exploitation
Using these two oversights, an attacker can supply a malicious Commit struct with the following data:
Commit = { Signatures: [ { ValidatorAddress: 0xBaDBaDBaD..., Signature: <original signature>... }, { ValidatorAddress: 0xBaDBaDBaD..., Signature: <original signature>... }, /* repeat until we reach attacker's validator index */ { ValidatorAddress: "<attacker's valid address>", Signature: "<attacker's original signature>" } /* populate the rest with incorrect CommitSig items */ { ValidatorAddress: 0xBaDBaDBaD..., Signature: <original signature>... }, ] /* populate other fields appropriately */}Assuming the attacker’s validator index is , for all the validator lookup evaluates to nil, causing those entries to be skipped and contributing nothing to weightedTimes. For , weightedTimes[i] is set to cmttime.NewWeightedTime(commitSig.Timestamp, validator.VotingPower).
The function then computes the weighted median over weightedTimes, which collapses to commitSig.Timestamp since all other entries have zero weight. In effect, the attacker gains full control over the median timestamp by invalidating all other contributions.
for i, commitSig := range commit.Signatures { _, validator := validators.GetByAddress(commitSig.ValidatorAddress) if validator != nil { totalVotingPower += validator.VotingPower weightedTimes[i] = cmttime.NewWeightedTime(commitSig.Timestamp, validator.VotingPower) } // No error raised if validator is nil - silently skipped!}3.1Time warp and beyond
The immediate effect of this exploit grants the attacker the ability to advance the timestamp by arbitrary amounts. In an example local demo, we were capable of advancing the block time to 9999-12-31 23:59:59.
Height Proposer Block Time1 6658793E23AA3A1A3CB68FCD0CF4E14C299322B7 2026-01-07T21:51:01Z2 921265369C1BC15053183324643C284CCC2204F9 2026-01-07T21:51:02Z3 9F18146AD2BF25510208D13D4DDAED7DDFA029D1 9999-12-31T23:59:59Z ← MALICIOUS4 F847FCD43163C15C96C0EBD6B88CAF89F2CF1BD6 9999-12-31T23:59:59.001Z5 6658793E23AA3A1A3CB68FCD0CF4E14C299322B7 9999-12-31T23:59:59.002ZThis value is 1 second behind from the maximum timestamp allowed in CometBFT, causing the chain to halt due to an assertion after a single round. Furthermore, this is exacerbated by CometBFT’s monotonic time enforcement, making the damage irrecoverable.
timestamp: ×tamppb.Timestamp{Seconds: 253402300800, Nanos: 999999} after 10000-01-01
goroutine 242 [running]:runtime/debug.Stack() runtime/debug/stack.go:26 +0x5egithub.com/cometbft/cometbft/consensus.(*State).receiveRoutine.func2() github.com/cometbft/cometbft/consensus/state.go:799 +0x46panic({0x16b03c0?, 0xc0003c0850?}) runtime/panic.go:783 +0x132github.com/cometbft/cometbft/types.VoteSignBytes({0xc000012f40, 0xc}, 0xc001d9a820) github.com/cometbft/cometbft/types/vote.go:152 +0x18f...3.2Asset theft
However, we realized that this bug has far more severe implications than merely halting the network (serious as that already is). It can be exploited to mint an unbounded amount of native tokens, enabling direct asset theft. To our knowledge, at least two major chains compute staking rewards on a block timestamp basis; advancing the timestamp by hundreds or thousands of years would therefore cause immediate and extreme inflation of the native token supply.
Example
For example, by manipulating the timestamp 200 years into the future, a chain with an initial (genesis) supply of 4,002,000,000,000 tokens could see its supply balloon to 47,994,771,968,060 tokens, resulting in an inflation multiplier of approximately 1,199%.
An attacker could then exit the artificially inflated tokens (e.g., via a CEX), draining liquidity from the chain.
4Part 4: Proof of concept
We were able to construct the same PoC across the affected chains’ devnet configurations. In an N-validator setup with a single attacker-controlled node that emits blocks with forged Timestamp and ValidatorAddress values, we show that a single validator that is given at least one opportunity to propose a block can force a chain-wide timestamp jump without reaching a 1/3 or 2/3 quorum.
We will defer the disclosure of the PoC in consideration of chains that may not yet have been able to address the issue in a timely manner.
4.1PoC setup & observed results
./watch_time_jump.sh <http://localhost:26657> 600=== TIME JUMP DETECTED ===height: 23time: 2026-01-08T01:49:39.847205037Zprevtime: 2026-01-08T00:49:39.846205037Zdelta_s: 3600proposer: 4CA5AC0B75FA23B16F1D308247D480FEEC4BB936
=== TIME JUMP DETECTED ===height: 27time: 2026-01-08T02:49:39.851205037Zprevtime: 2026-01-08T01:49:39.850205037Zdelta_s: 3600proposer: 4CA5AC0B75FA23B16F1D308247D480FEEC4BB936
=== TIME JUMP DETECTED ===height: 31time: 2026-01-08T03:49:39.855205037Zprevtime: 2026-01-08T02:49:39.854205037Zdelta_s: 3600proposer: 4CA5AC0B75FA23B16F1D308247D480FEEC4BB936
=== TIME JUMP DETECTED ===height: 35time: 2026-01-08T04:49:39.859205037Zprevtime: 2026-01-08T03:49:39.858205037Zdelta_s: 3600proposer: 4CA5AC0B75FA23B16F1D308247D480FEEC4BB936Every time the malicious validator (4CA5…) proposes, the block timestamp is incremented by exactly 3600 seconds.
Time | Height | Total Supply | Minted This Block ------------------------------|--------|---------------------|------------------- 2041-01-25T08:57:02.721714914Z | 10 | 135,980,315,819,112 | +43992771925508 2041-01-25T08:57:02.721714914Z | 10 | 135,980,315,819,112 | +0 2041-01-25T08:57:02.722714914Z | 11 | 135,980,315,819,118 | +6 2041-01-25T08:57:02.723714914Z | 12 | 135,980,315,819,124 | +6 2041-01-25T08:57:02.724714914Z | 13 | 135,980,315,819,130 | +6 2240-12-08T08:57:02.725714914Z | 14 | 179,973,087,744,638 | +43992771925508 2240-12-08T08:57:02.726714914Z | 15 | 179,973,087,744,644 | +6 2240-12-08T08:57:02.727714914Z | 16 | 179,973,087,744,650 | +6 2240-12-08T08:57:02.728714914Z | 17 | 179,973,087,744,656 | +6Each time the timestamp is manipulated, the validator’s staking reward balloons to the extent that it becomes un-bondable.
5Part 5: Fixes
This bug can be fixed by adding an equality check in verifyCommitBatch when lookUpByIndex=true:
if lookUpByIndex { val = vals.Validators[idx]
if !bytes.Equal(val.Address, commitSig.ValidatorAddress) { return fmt.Errorf("validator address mismatch at index %d: expected %X, got %X", idx, val.Address, commitSig.ValidatorAddress) }}Additionally, MedianTime should return an error when encountering unknown validator addresses rather than silently skipping them:
_, validator := validators.GetByAddress(commitSig.ValidatorAddress)if validator == nil { return time.Time{}, fmt.Errorf("unknown validator address in commit: %X", commitSig.ValidatorAddress)}To summarize,
verifyCommitBatchignoresValidatorAddress: Add address equality check whenlookUpByIndex=trueMedianTimesilently skips unknown addresses: Return error for unknown addresses
6Part 6: Timeline
The first vulnerable commit was introduced in ad715fe9 in November 2019 (Release: Tendermint v0.33.0), which suggests that this bug underwent 6 years worth of bug bounty programs and audits.
The commit changed the Commit structure from Precommits (containing ValidatorIndex) to Signatures (containing ValidatorAddress) as part of ADR-25 to reduce block size.
func MedianTime(commit *types.Commit, validators *types.ValidatorSet) time.Time { for i, vote := range commit.Precommits { if vote != nil { _, validator := validators.GetByIndex(vote.ValidatorIndex) totalVotingPower += validator.VotingPower weightedTimes[i] = tmtime.NewWeightedTime(vote.Timestamp, validator.VotingPower) } } return tmtime.WeightedMedian(weightedTimes, totalVotingPower)}func MedianTime(commit *types.Commit, validators *types.ValidatorSet) time.Time { for i, commitSig := range commit.Signatures { if commitSig.Absent() { continue } _, validator := validators.GetByAddress(commitSig.ValidatorAddress) if validator != nil { // SILENTLY SKIPS UNKNOWN ADDRESSES! totalVotingPower += validator.VotingPower weightedTimes[i] = tmtime.NewWeightedTime(commitSig.Timestamp, validator.VotingPower) } } return tmtime.WeightedMedian(weightedTimes, totalVotingPower)}Consequently, MedianTime was changed to use an address-based validator lookup (GetByAddress), silently skipping validators whose addresses are not present in the validator set instead of propagating an error. Meanwhile, the commit verification logic continued to rely on index-based lookup (GetByIndex), resulting in the exploitable inconsistency.
6.1Timeline
| Date | Event |
|---|---|
| November 26, 2019 | Vulnerability introduced (Tendermint commit changed MedianTime to use address-based lookup) |
| January 15, 2020 | Tendermint v0.33.0 released (first vulnerable release) |
| February 2023 | CometBFT forked Tendermint and inherited the vulnerability |
| January 7, 2026 | Vulnerability discovered and reported to CometBFT via SEAL 911 |
| January 7, 2026 | War room created |
| January 9, 2026 | CometBFT acknowledged vulnerability |
| January 13, 2026 | CometBFT securely distributed patches to partners |
| January 21, 2026 | Public announcement |
7References
- Commit verification logic:
types/validation.go#L247-L265 - Median time calculation:
state/state.go#L269-L284 - Block time derivation:
state/state.go#L246-L260 - BFT Time specification:
spec/consensus/bft-time.md - Affected versions: v0.38.x (confirmed on v0.38.20), likely affects v0.37.x and v1.x branches
8Who are we?
QED Audit is a blockchain security firm delivering AI-powered audits.
In 2026, relying only on manual audits is no longer practical. Large codebases, such as blockchains built on the Cosmos SDK, are growing and changing at a pace manual security reviews cannot keep up with. This is reflected in the fact that this critical bug went unnoticed for six years.
At QED Audit, we provide continuous, automated analysis without sacrificing the depth of human review. We achieve this by combining AI agents, traditional analysis techniques, and deep blockchain security expertise. Contact us to see whether our automated researchers can uncover issues hiding in your codebase.