Assessment reports>Blobstream X>Critical findings>Tendermint X TendermintSkipCircuit ,skip, function's return value need not be related to parameters
Category: Coding Mistakes

Tendermint X TendermintSkipCircuit skip function's return value need not be related to parameters

Critical Severity
Critical Impact
High Likelihood

Description

In the CombinedSkipCircuit's define function, a trusted block height and header hash are provided as public inputs (trusted_block and trusted_header_hash) as well as a target block height (target_block). The target block's header hash is then obtained by using the Tendermint X circuit, TendermintSkipCircuit:

let target_header_hash = builder.skip::<MAX_VALIDATOR_SET_SIZE, CHAIN_ID_SIZE_BYTES>(
    C::CHAIN_ID_BYTES,
    C::SKIP_MAX,
    trusted_block,
    trusted_header_hash,
    target_block,
);

For soundness of the CombinedSkipCircuit, the skip function should ensure that, assuming that trusted_block and trusted_header_hash are legitimate, the target_header_hash is the hash of a legitimate Tendermint block with block height target_block according to the Tendermint light client consensus rules.

However, the skip function is implemented as follows.

fn skip<const MAX_VALIDATOR_SET_SIZE: usize, const CHAIN_ID_SIZE_BYTES: usize>(
    &mut self,
    chain_id_bytes: &[u8],
    skip_max: usize,
    trusted_block: U64Variable,
    trusted_header_hash: Bytes32Variable,
    target_block: U64Variable,
) -> Bytes32Variable {
    let mut input_stream = VariableStream::new();
    input_stream.write(&trusted_block);
    input_stream.write(&trusted_header_hash);
    input_stream.write(&target_block);
    let output_stream = self.async_hint(
        input_stream,
        SkipOffchainInputs::<MAX_VALIDATOR_SET_SIZE> {},
    );
    let skip_variable = output_stream.read::<VerifySkipVariable<MAX_VALIDATOR_SET_SIZE>>(self);

    let target_header = skip_variable.target_header;

    self.verify_skip::<MAX_VALIDATOR_SET_SIZE, CHAIN_ID_SIZE_BYTES>(
        chain_id_bytes,
        skip_max,
        &skip_variable,
    );
    target_header
}

No constraints are introduced by this function itself, only by the following call.

self.verify_skip::<MAX_VALIDATOR_SET_SIZE, CHAIN_ID_SIZE_BYTES>(
    chain_id_bytes,
    skip_max,
    &skip_variable,
);

Note that before this call, while the arguments trusted_block, trusted_header_hash, and target_block are used to calculate skip_variable in this implementation of the prover, there are no constraints introduced in the circuit enforcing a relation between skip_variable and these arguments. An attacker can thus use an arbitrary chain of Tendermint blocks that pass the consensus checks — but using a fake initial block skip_variable.trusted_header of their choosing — and thereby have wide latitude in what to choose as skip_variable.target_header while fuilfilling the verify_skip constraints. For example, the attacker could use arbitrary validators they made up to create a chain of Tendermint blocks, with arbitrary data hashes of their choosing included in these blocks.

Impact

The target_header_hash used by CombinedSkipCircuit can be set to the hash of a Tendermint block for which the attacker could set the data hash and data hash of preceeding blocks in a valid chain to a value of their choosing. As target_header_hash is used as the root of trust for the prove_data_commitment function, this ultimately means that the attacker can produce a CombinedSkipCircuit proof for arbitrary data commitments.

Recommendations

In the skip function, add constraints that check that skip_variable.trusted_block, skip_variable.trusted_header, and skip_variable.target_block are equal to the parameters trusted_block, trusted_header_hash, and target_block that were passed to the function, respectively.

Remediation

This issue has been acknowledged by Succinct, and a fix was implemented in the following commit: .

Zellic © 2024Back to top ↑