Malicious authority can permanently kill an exodus signing session via an undecodable nonce commitment

Component: pallets/exodus @ 0162da46, src/lib.rs:3061-3115 (validate_exodus_nonce_commitment_size_and_get_metadata), src/impls/frost.rs (generate_group_commitment), src/lib.rs:3405-3412, 3436-3444 (exile).

Description: The on-chain validation of register_nonce_commitment checks only byte LENGTH (package.hiding_commitment.len() != element_bytes_len(), lib.rs:3072-3078), not that the bytes are a valid curve point. A malicious active authority can submit 33 bytes of garbage (e.g. [0xFF; 33], invalid prefix) that passes validate_unsigned and lands on chain. Then EVERY honest authority running generate_group_commitment offchain fails: the filter_map in impls/frost.rs drops the undecodable entry, then nonce_commitments_bytes.len() != signing_commitments.len() → Err(InvalidParticipantId). The handler (exodus_run_process, lib.rs:3405-3412) calls exodus_records.exclude_exodus_session(session): the node bans that session permanently in local storage (release_block: Default, types/local_storage.rs:309-316). Since all honest nodes fail deterministically on the same on-chain data, the exodus session dies for everyone. There is no on-chain mechanism to identify or eject the bad-nonce sender (unlike DKG which has complaint rounds 3-4). The attacker repeats one bad nonce per new session for persistent DoS of all exodus signing, defeating the ROAST resilience guarantee against unresponsive/malicious signers.

Impact: Liveness: all exodus requests (bridge-out, rotation, governance) stall permanently while the attacker stays an active authority. No direct fund loss.

Reproduction: Local PoC: faithfully replicates the generate_group_commitment filter (impls/frost.rs): 3 nonce commitments (2 valid generator points + 1 [0xFF; 33] that passes the on-chain length check) → only 2 decode → nonce_commitments_bytes.len() != signing_commitments.len() → Err(InvalidParticipantId). Executed 2026-10-02: submitted nonces: 3, successfully decoded: 2. The downstream on-chain chain (failure → exclude_exodus_session at lib.rs:3405-3412) verified by code reading. Integration test poc_garbage_nonce_breaks_group_commitment_for_everyone added in pallets/exodus/src/tests.rs.

Severity: Medium. Reason: requires an insider (active authority / compromised validator key), so likelihood is low, but the impact is total and persistent DoS of the signing pipeline with no on-chain recovery path. Not High because validator privilege is required.

Recommendation: Add curve-point deserialization checks in validate_exodus_nonce_commitment_size_and_get_metadata (and in validate_unsigned): reject nonce commitments that are not valid points before they enter the mempool/chain. Alternative: identify the culprit offchain (each nonce is recorded per authority_index in NonceCommitments) and exclude them from the next session instead of banning the whole session.

Verification: VERIFIED (PoC executed; on-chain continuation verified by code reading).

Thank you for your report!

You are absolutely right here. Which of the two options you’ve given would you consider preferable? In my view, this thread deserves to be elevated. If you’re interested in continuing the research on abuses from the validator side of the protocol, you’re welcome to do so!

Thanks for confirming. Of the two options, I’d pick rejecting undecodable points at validation time (option 1) as the primary fix:

  • It closes the root cause: bytes that can never be used should never be admitted on chain. Decompressing a 33-byte compressed point is cheap, so the extra check inside validate_exodus_nonce_commitment_size_and_get_metadata costs essentially nothing on the validate_unsigned path.
  • It is deterministic across all nodes because it lives on the consensus-critical validation path. Per-node offchain exclusion heuristics (option 2) must be reimplemented identically by every operator to avoid divergence, and they leave the poisoned bytes on chain permanently.
  • Option 2 is still useful as a complement for recovery: NonceCommitments already maps each nonce to its authority_index on chain, so every node can identify a poisoned session’s culprit from the same data. For sessions poisoned before the fix lands, excluding the identified culprit from the next session beats banning the whole session in local storage.

So: option 1 to prevent, option 2 (ideally with the exclusion decision made deterministic rather than per-node local storage) to recover.

Hey @Rusdiwireng — it looks like the dev team has fixed the issue you reported. Could you confirm whether it’s resolved on your end now, or continue the thread if it still persists?

It looks like deserialization happens on the stack only, that’s why we’ve decided to put this check right in the validate_unsigned.

I read the diff for c884ec7. The new is_nonce_valid_point() check runs in the validate_unsigned path and rejects undecodable 33-byte points with BadProof, for both the hiding and the binding commitment. That is exactly option 1 from my earlier reply: poisoned bytes can no longer enter the pool or the chain, so generate_group_commitment on honest nodes will never see them. Confirmed resolved on my end.