Finding 1 of 4: The Nothing Halt
A Missing EncryptedRandom Field Lets One Byzantine Proposer Halt Secret Network
Severity: Critical (chain halt)
Affected component:
scrtlabs/SecretNetwork at commit edcbccbbbe65cae1bd93d4d9089ff5d98228f704 (= tag v1.25.0);
consensus fork scrtlabs/tendermint@v0.38.23-secret.1;
SGX enclave block-verifier
Fix: shipped in scrtlabs/tendermint@v0.38.23-secret.2, packaged in a SecretNetwork release on 2026-06-10
Disclosure: coordinated; reported 2026-06-01, fix released 2026-06-10
Every accepted Secret Network block must advance the SGX enclave's per-height validator-set evidence for height + 1, because that is the read every honest validator performs before it can build the next block. The consensus layer treats the field that gates the corresponding write as optional.
The block that halts the chain contains nothing. A malicious proposer does not attack the randomness, forge the proof, corrupt the seed, or race the enclave. They remove the field. That is enough to permanently halt, until governance intervention, a chain designed to tolerate up to a third of its validators being Byzantine. And the recovery path a competent operator would reach for first is closed by two lines in x/compute/internal/keeper/querier.go:
// x/compute/internal/keeper/querier.go:316-317
// SECURITY: Enforce height restriction - only allow querying heights < current height
// This prevents non-SGX nodes from getting data for the current block and participating in consensusThe comment defends a real attack. That defense converts this halt into a governance event.
This writeup describes a vulnerability disclosed to Secret Labs by Bluethroat Labs on 2026-06-01 and fixed on 2026-06-10 under coordinated disclosure. All observations reflect the audited revision. No exploitation was observed in the wild.
Separate incident: This consensus-layer chain-halt vulnerability is unrelated to the June 2026 CW20-ICS20/Axelar bridge infinite-mint exploit. The two incidents affected different components and had different root causes.
Executive Summary
- A single Byzantine proposer, at any one height where CometBFT's round-robin selects them, submits an otherwise-valid block with
Header.EncryptedRandom = nil.Header.Hash()does not commit to the field, so an honest supermajority signs the block with real keys and it commits. - Once committed,
x/computeBeginBlock sees nil, logsNon-encrypted block, and skipsSubmitBlockSignatures. That call is the only path that writesnext_validator_set_evidenceinto the enclave's block-verifier state. - At
H + 1, honest validators calltmenclave.SubmitValidatorSet. The enclave recomputes the expected height-bound evidence, finds no match, and returnsSGX_ERROR_UNEXPECTED. Go surfaces this asCONSENSUS FAILURE!!! err="Failed to submit validator set to enclave"and every honest validator's proposal path panics. SECRET_NODE_MODE=replaycannot recover the halt. The ecall-record querier explicitly rejectsreq.Height >= currentHeight; the code comment declares that restriction intentional, defending a different attack. The halt is halt-until-coordinated-recovery, not per-node self-healing.- Reproduced end-to-end on real Intel SGX silicon on Azure
Standard_DC8s_v3with DCAP-v3 attestation. Honest and malicious images share byte-identical enclave runtime artifacts; only/usr/bin/secretddiffers. Three honest validators stayed pinned atH = 10, same hash, same block time, across 195 status samples over 15 minutes.
Background
Three SCRT-specific mechanics make the bug legible.
Validator-set evidence. Each accepted block feeds the enclave a signed header. Inside submit_block_signatures, the enclave computes KEY_MANAGER.encrypt_hash(next_validators_hash, height + 1) and stores it as next_validator_set_evidence. On the next height, ecall_submit_validator_set recomputes the expected evidence with Keychain::encrypt_hash_ex and refuses to advance if the stored value does not match. Secret's own forum post Enhancing Robustness of Current Validator Set in Secret Node Enclave describes this loop by name.
x/compute BeginBlock is the sole writer. The SubmitBlockSignatures C-ABI call from the Go module into the enclave is the only production writer of next_validator_set_evidence. git grep -n 'next_validator[s]_evidence' at the audited revision confirms this: the only assignments live inside submit_block_signatures.rs in the block shown in the "Vulnerability Details" table below. Nothing else in the codebase reaches that field.
Header.EncryptedRandom is optional at every host layer. Secret's fork of CometBFT extends the block header with an EncryptedRandom *EnclaveRandom field carrying the proposer's enclave-produced random beacon plus its proof. The field is a pointer, defaults to nil on the wire, does not participate in Header.Hash(), and is absent from the ABCI RequestProcessProposal schema.
Each layer, in isolation, is defensible. The Tendermint fork treats the field as optional because Go's protobuf and pointer semantics make that the path of least resistance. x/compute treats a nil random as a signal to skip a call it cannot meaningfully make. The enclave treats missing evidence as a hard fault because accepting stale or absent evidence would break the anti-equivocation guarantee that motivates the mechanism. Composition breaks the system: an "if you feel like it" write feeds a "we cannot proceed without it" read, with no host-side veto in between.
Vulnerability Details
Seven layers touch the field between the malicious proposer's state.MakeBlock and the enclave's SubmitValidatorSet at H + 1. Six say "sure, fine." The seventh is the enclave, and by the time it says no, the malformed block has already committed.
| # | Layer | File | Behavior on EncryptedRandom == nil |
|---|---|---|---|
| 1 | Protobuf conversion | types/random.go EnclaveRandomFromProto | Returns nil, nil |
| 2 | Stateless field validation | types/random.go EnclaveRandom.ValidateBasic | // todo tm-enclave: this -- no checks |
| 3 | Header hash coverage | types/block.go:472-487 Header.Hash() | Field not in the input set; hash unchanged whether nil or populated |
| 4 | Stateful block validation | state/validation.go:124-134 | if block.EncryptedRandom != nil { ... } -- early-return on nil |
| 5 | ABCI ProcessProposal | state/execution.go | Request struct does not carry EncryptedRandom; app cannot see it |
| 6 | x/compute BeginBlock | x/compute/module.go | if block_header.EncryptedRandom != nil -- silent skip, Debug log |
| 7 | SGX enclave SubmitValidatorSet at H+1 | offchain.rs:1798-1816 | Mismatch returns SGX_ERROR_UNEXPECTED, Go panics |
Row 4: stateful validation fails open
// scrtlabs/tendermint@v0.38.23-secret.1/state/validation.go:124-134
if !(os.Getenv("SECRET_NODE_MODE") == "replay") {
if block.EncryptedRandom != nil { // fail-open on absence
proofValid := tmenclave.ValidateRandom(
block.EncryptedRandom.Random,
block.EncryptedRandom.Proof,
block.AppHash,
uint64(block.Height),
)
if !proofValid {
return fmt.Errorf("invalid proof for encrypted random. ...")
}
}
// no else branch: nil skips the ONLY line of defense in stateful validation.
}The guard reads as "verify the proof if it is there." The unspoken assumption is that the proof will always be there. Remove the field, remove the check.
Row 5: ProcessProposal cannot see the field
// scrtlabs/tendermint@v0.38.23-secret.1/state/execution.go, ProcessProposal call
resp, err := blockExec.proxyApp.ProcessProposal(context.TODO(), &abci.RequestProcessProposal{
Hash: block.Header.Hash(), // hash does not cover EncryptedRandom
Height: block.Height,
Time: block.Time,
Txs: block.Txs.ToSliceOfBytes(),
ProposedLastCommit: buildLastCommitInfoFromStore(block, blockExec.store, state.InitialHeight),
Misbehavior: block.Evidence.Evidence.ToABCI(),
ProposerAddress: block.ProposerAddress,
NextValidatorsHash: block.NextValidatorsHash,
// no EncryptedRandom. an app-layer veto would require an ABCI schema change,
// not a check. the defense-in-depth layer is absent.
})Row 6: x/compute skips the sole writer
// scrtlabs/SecretNetwork:x/compute/module.go BeginBlock
if block_header.EncryptedRandom != nil {
randomAndProof := append(block_header.EncryptedRandom.Random, block_header.EncryptedRandom.Proof...)
random, validator_set_evidence, err = api.SubmitBlockSignatures(header, b_commit, data, randomAndProof)
if err != nil {
ctx.Logger().Error("Failed to submit block signatures")
return err
}
am.keeper.SetRandomSeed(ctx, random, validator_set_evidence)
} else {
ctx.Logger().Debug("Non-encrypted block", "Block_hash", block_header.LastBlockId.Hash,
"Height", height, "Txs", len(x2_data))
// this Debug line is the entire honest-side handling for a nil field.
// no error, no re-request, no abort. the block commits. the enclave is
// not advanced. the write H+1 depends on never happens.
}SubmitBlockSignatures is the unique producer of the H+1 evidence:
// scrtlabs/SecretNetwork:cosmwasm/enclaves/shared/block-verifier/src/submit_block_signatures.rs:142-148
if let tendermint::Hash::Sha256(val) = header.header.next_validators_hash {
let validator_set_evidence = KEY_MANAGER.encrypt_hash(val, height + 1);
next_validator_set_evidence.copy_from_slice(validator_set_evidence.as_slice());
message_verifier
.next_validators_evidence
.copy_from_slice(validator_set_evidence.as_slice());
// sole writer of next_validators_evidence anywhere in the codebase.
// git grep -n 'next_validator[s]_evidence' at the audited revision returns
// exactly these two assignments. no second writer, no fallback branch.
}Row 7: the enclave hard-faults at H + 1
// scrtlabs/SecretNetwork:cosmwasm/enclaves/execute/src/registration/offchain.rs:1798-1816
let expected_evidence = Keychain::encrypt_hash_ex(
&seeds.arr[i_seed as usize],
validator_set_hash,
height,
);
is_match = verified_msgs.next_validators_evidence == expected_evidence;
// ...
if !is_match {
if extra.height != 0 { // every H+1 lands here
error!("validator set evidence mismatch");
return sgx_status_t::SGX_ERROR_UNEXPECTED;
}
// ...
#[cfg(feature = "production")]
{ ... } // strictly later; only the
// initial-bootstrap branch
}The production feature gate lives strictly later in the same function, inside the extra.height == 0 initial-validator-set bootstrap. The H+1 attack returns at the earlier guard regardless of whether production is compiled in. Evidence is height-bound, so no honest validator can substitute the stored H - 1 evidence even when the validator set is unchanged.
Attack Scenario
H-1 honest proposer block commits, enclave advanced normally
H Byzantine proposer sets Header.EncryptedRandom = nil
honest supermajority signs the block hash (which excludes EncryptedRandom)
block commits x/compute logs "Non-encrypted block"; enclave NOT advanced
H+1 every honest validator calls tmenclave.SubmitValidatorSet
enclave compares stored vs expected next_validators_evidence
mismatch returns SGX_ERROR_UNEXPECTED (offchain.rs:1816, extra.height != 0)
Go side panic CONSENSUS FAILURE!!! from state.MakeBlock
chain halt no honest validator can produce H+1
replay-mode recovery rejected by querier.go SECURITY: commentThe malicious diff is one file, +7/-2 lines, and lives entirely in state.MakeBlock:
// scrtlabs/tendermint@v0.38.23-secret.1/state/execution.go, malicious proposer only
- return state.MakeBlock(height, txl, commit, evidence, proposerAddr)
+ b, err := state.MakeBlock(height, txl, commit, evidence, proposerAddr)
+ if err == nil {
+ b.Header.EncryptedRandom = nil // the entire attack, one line
+ }
+ return b, errPatch SHA-256 d1923468d3e800bf9ddc93c0cc7a612161b453e29c6e369147a5d3566aaf82ee. Applied cleanly to a fresh clone at the audited revision via git apply --check in the reproducibility gate. The full patch bundle is available on request under responsible disclosure.
On the Azure Standard_DC8s_v3 HW witness (Xeon Platinum 8370C-class, /dev/sgx_enclave and /dev/sgx_provision, DCAP-v3), the proposer schedule at heights 1 through 9 carried valid {random, proof} pairs. At height 10, produced by the malicious val0 (4114F98023D24486B3581D763E69E8541D431663, 25% voting power), the field was null. The three honest validators (75%) signed the malformed block with their legitimate keys. All four RPC endpoints agreed:
val0..val3 hash=D689D5C6DB825B5A0836B8EE562B5F6482F87CE792924ECCEC736D107F1D8361
encrypted_random=nullAfter val0 was killed at 2026-05-31T15:20:04Z, a 10-second-interval status capture produced 195 samples across the three honest validators over 15 minutes. Every sample reported:
latest_block_height = "10"
latest_block_hash = "D689D5C6DB825B5A0836B8EE562B5F6482F87CE792924ECCEC736D107F1D8361"
latest_block_time = "2026-05-31T15:18:57.700659402Z"Two of three honest validators, val1 and val3, logged the explicit enclave error plus the Go panic. The third, val2, is a silent liveness witness: it was not selected as proposer during the 15-minute observation window (CometBFT's stake-weighted round-robin did not place it at the top of the proposer queue), so it never entered state.MakeBlock for H + 1 and therefore never invoked the enclave call that would have raised the mismatch. Its /status and /consensus_state samples remained pinned at H = 10 throughout, which is the same liveness-loss signal.
ERROR [secret_enclave::registration::offchain] validator set evidence mismatch
ERROR [secret_enclave::registration::offchain] validator set evidence mismatch
ERROR [secret_enclave::registration::offchain] validator set evidence mismatch
3:19PM ERR CONSENSUS FAILURE!!! err="Failed to submit validator set to enclave" module=consensus stack="goroutine 29 [running]:
runtime/debug.Stack()
github.com/cometbft/cometbft/consensus.(*State).receiveRoutine.func2()
/go/pkg/mod/github.com/scrtlabs/tendermint@v0.38.23-secret.1/consensus/state.go:801 +0x46
panic({0x45418e0?, 0x55893a0?})
/usr/local/go/src/runtime/panic.go:787 +0x132
github.com/cometbft/cometbft/state.State.MakeBlock(...)
/go/pkg/mod/github.com/scrtlabs/tendermint@v0.38.23-secret.1/state/state.go:301 +0xcc5
github.com/cometbft/cometbft/state.(*BlockExecutor).CreateProposalBlock(...)
/go/pkg/mod/github.com/scrtlabs/tendermint@v0.38.23-secret.1/state/execution.go:140 +0x3cd
github.com/cometbft/cometbft/consensus.(*State).defaultDecideProposal(...)
/go/pkg/mod/github.com/scrtlabs/tendermint@v0.38.23-secret.1/consensus/state.go:1215 +0x65
github.com/cometbft/cometbft/consensus.(*State).enterPropose(...)
/go/pkg/mod/github.com/scrtlabs/tendermint@v0.38.23-secret.1/consensus/state.go:1194 +0x82e
github.com/cometbft/cometbft/consensus.(*State).enterNewRound(...)
/go/pkg/mod/github.com/scrtlabs/tendermint@v0.38.23-secret.1/consensus/state.go:1113 +0xb4f
...The enclave rejecting the input is the genuine enclave
Between the honest and malicious images, only the host binary differs:
path match
/usr/bin/secretd false
/usr/lib/libgo_cosmwasm.so true
/usr/lib/librandom_api.so true
/usr/lib/librust_cosmwasm_enclave.signed.so truesecretd check-enclave on the same host reported SGX enclave health status: Success before the run. The DCAP attestation path was healthy end-to-end. The H + 1 failure is the genuine Secret-Labs-signed enclave doing exactly what it is supposed to do: refusing to accept a validator-set submission for which it has no prior evidence. Attestation is not a barrier to this attack, because the attack does not touch the enclave.
Recovery Is Blocked by Design
The natural triage instinct is that a halt caused by missing enclave state should be recoverable by asking a healthy peer or by falling back to a non-SGX signing path. Secret's ecall-record querier explicitly forecloses both:
// scrtlabs/SecretNetwork:x/compute/internal/keeper/querier.go:316-322
// SECURITY: Enforce height restriction - only allow querying heights < current height
// This prevents non-SGX nodes from getting data for the current block and participating in consensus
currentHeight := ctx.BlockHeight()
if req.Height >= currentHeight {
return nil, status.Errorf(codes.FailedPrecondition,
"cannot query ecall record for height %d: must be less than current height %d",
req.Height, currentHeight)
}
// the SECURITY comment is doing real work. it defends against a well-defined
// attack: a non-SGX node piggybacking on live SGX output. it also converts
// THIS halt into a coordinated-recovery event, because a replay-mode node
// that tries to fetch H+1 to unblock finalization is denied by the same rule.Replay-mode validators can enter consensus and sign prevotes, precommits, and even proposals. What they cannot do is finalize H + 1, because x/compute in replay handling tries to fetch the missing SGX evidence via gRPC, and the querier rejects the current-height request:
signed proposal height=11
signed and pushed vote height=11 SIGNED_MSG_TYPE_PREVOTE
signed and pushed vote height=11 SIGNED_MSG_TYPE_PRECOMMIT
Waiting for SGX node ecall record, retrying...
error="gRPC EcallRecord failed for height 11: rpc error: code = FailedPrecondition desc = cannot query ecall record for height 11: must be less than current height 10"In the primary rerun, three replay nodes logged 91 unbounded retries each; in a supplemental long probe, 237.
The enclave's extra.height == 0 initial-validator-set bootstrap branch is not a per-node recovery path either. In mainnet builds it is gated by #[cfg(feature = "production")], which unconditionally rejects with Initial validator set can't be set in production. Even absent that gate, sealed KEY_MANAGER state persisting across restarts means an already-registered node cannot legally re-enter the bootstrap branch to re-derive seeds; the branch is reachable only from a fresh, unregistered node performing initial network entry, which is not the state a halted validator is in. Coordinated social recovery through a patch or a documented emergency-upgrade procedure remains possible. Independent operator recovery does not.
Why This Matters
Secret Network is a semi-permissioned validator set with SGX-attested nodes. That gates entry, not proposer behavior. Once inside, CometBFT's stake-weighted round-robin selects every active-set validator as proposer with frequency proportional to voting power. A validator with 1% voting power proposes roughly once every 100 heights; at Secret's observed block cadence of approximately 6 seconds [REASONABLE INFERENCE from mainnet block-time observation], that is about once every 10 minutes. A 0.1% validator proposes roughly once every 100 minutes. There is no minimum-stake threshold below which the schedule skips a validator.
The cost asymmetry favors the attacker by orders of magnitude. Attacker cost is the operational cost of running one SGX host plus whatever the attacker's active-set bond is worth. There is no on-chain slashing rule that fires on EncryptedRandom == nil, because honest validators sign the block; no double-sign evidence, no light-client evidence, no application-layer misbehavior report is produced. The only stake-side exposure is jail-for-downtime if the attacker's validator later stops signing. Defender cost is a network-wide halt affecting every dApp, every user, every IBC channel, and every governance action until operators coordinate a patched binary. An actor with no profit motive at all, willing to burn one bond to stop the network for hours, gets exactly that trade.
Secret's own operational posture reinforces the framing. Public documentation states that Secret's consensus and computation layer are combined and validators use Intel SGX while processing transactions. The 2026 roadmap treats non-SGX validator participation as future work requiring explicit community approval. The v1.18 upgrade proposal treats enclave-bricking as a network-wide production risk severe enough to warrant a documented emergency-upgrade procedure.
The x/emergencybutton circuit breaker cannot help. It operates at the application layer, above consensus. A consensus-layer halt blocks block production itself, so the circuit breaker cannot fire because it needs a block to commit.
What This Did Not Allow
- No safety violation. Honest validators do not double-sign, do not fork, do not commit conflicting blocks. The malformed block is unique; every honest validator that precommits
Hprecommits the same hash. - No forged signatures. Honest validators sign
Hwith legitimate keys against a block that legitimately passes their validation logic. The bug is that the validation logic permits nil. - No enclave compromise. The three enclave runtime artifacts are byte-identical between honest and malicious images. No enclave code is modified, no attestation is forged, no sealed data is accessed.
- No exfiltration of enclave secrets, seeds, or sealed data. The enclave rejects the poisoned state without leaking anything.
- No permissionless validator entry. Secret's admission is semi-permissioned. The finding stays inside the canonical BFT threat model that some active-set validator may be Byzantine; it does not assume the attacker can register a fresh validator on demand.
- No permanent chain destruction. Coordinated recovery via patch or documented emergency-upgrade procedure remains possible. The window between exploitation and recovery is the window in which the network is down.
Severity Classification
Bluethroat classification: Critical (chain halt).
Immunefi's blockchain/DLT rubric places "network not being able to confirm new transactions" and total network shutdown in the Critical tier (Immunefi Vulnerability Severity Classification System v2.3, accessed 2026-07-14, paraphrased from the blockchain/DLT category). The demonstrated effect satisfies that bar: after the H = 10 commit, no honest validator can build H + 1, and no independent recovery path exists on the audited codebase. The trigger is a single Byzantine proposer at one height, the textbook BFT-tolerated fault.
The severity rests on a mechanically irrefutable construction: SubmitBlockSignatures is the unique writer of next_validators_evidence, and SubmitValidatorSet is the only gate at H + 1. Skip the write, the read fails. There is no second writer and no fallback branch in the audited code. Secret Labs' public triage classification is not published for this finding at the time of writing. The vendor patched at Critical-appropriate speed (nine calendar days from report to release).
Fix
Shipped in Secret's Tendermint fork v0.38.23-secret.2, released as part of a SecretNetwork patch on 2026-06-10. The patched fork rejects nil EncryptedRandom in stateful validation, closing the fail-open at the earliest host-side gate that can see the field. Honest validators now reject the malformed block at consensus validation and it never commits.
Defense-in-depth items not covered by the shipped patch, offered as follow-up:
- Enforce real length checks in
EnclaveRandom.ValidateBasic(). The current implementation is atodostub. - Reject nil
EncryptedRandominx/computeBeginBlock as a belt-and-braces measure. - Add
EncryptedRandomtoRequestProcessProposalso the application layer can independently veto malformed proposals without relying on the consensus fork. - Add regression tests for nil, empty, and malformed
RandomandProof, and for theHtoH + 1validator-set evidence transition.
Timeline
- 2026-06-01 -- Finding reported to Secret Labs under coordinated disclosure with the full bundle (report, HW and SW witnesses, patch, reproducibility gate, PoC SHA-256).
- 2026-06-10 -- Fix released.
- 2026-07-22 -- Public writeup published following the coordinated-disclosure window.
Key Takeaway
When a system spans a host and an enclave, any field that is optional on the host and mandatory in the enclave is a chain-halt primitive waiting for a Byzantine proposer. Optionality across a trust boundary defines a bug class. Audit the writer and the reader as one object, not as two components in isolation. And when the only obvious recovery path around such a halt is closed by a SECURITY: comment defending a different attack, the halt becomes a governance event.
References
- Secret Labs, Enhancing Robustness of Current Validator Set in Secret Node Enclave -- describes the validator-set evidence design that this bug bypasses.
- Secret Labs, How Secret Network Uses SGX -- SGX and consensus coupling in production.
- Secret Labs, Secret Network 2026 Roadmap -- non-SGX validator participation as future work.
- Secret Labs, Secret Network v1.18 Upgrade Proposal -- enclave-bricking treated as network-wide production risk.
- Secret Labs, Mainnet Upgrade to 1.24.1 -- version context for the vulnerable release line.
scrtlabs/SecretNetworkrelease v1.25.0 -- vulnerable release anchor.- Consensus fork:
scrtlabs/tendermint@v0.38.23-secret.1(vulnerable),v0.38.23-secret.2(patched). - Immunefi Vulnerability Severity Classification System v2.3 -- severity-tier reference.
- KuCoin, coverage of Secret Network's proposed migration to Arbitrum, 2026-07-07.
Bluethroat Labs is a security research firm focused on smart contracts, distributed consensus, and TEE-integrated systems. This is finding 1 of 4 disclosed to Secret Labs in this engagement; companion writeups will follow as vendor timelines allow.

