End block header is never checked if end block is too far
Description
The prove_data_commitment
function receives as arguments start and end block heights start_block
and end_block
as well as end_header_hash
, the hash of the header at block height end_block
(it also receives a block-header hash start_header_hash
, which is however unused).
The block-header hash start_header_hash
acts as a root of trust: in the mapping function, prove_subchain
is called, which will, for the batch in which the block with block height end_block - 1
lies, check that the next block-header hash of the block in the chain with claimed height end_block - 1
is equal to the end_header_hash
that was passed to prove_data_commitment
(in the following snippet, global_end_header_hash
is what was passed as end_header_hash
to prove_data_commitment
):
let root_matches_end_header =
self.is_equal(last_block_id_proof_root, *global_end_header_hash);
let end_header_check = self.or(is_not_last_block, root_matches_end_header);
self.assert_is_equal(end_header_check, true_bool);
However, if end_block > start_block + NB_MAP_JOBS * BATCH_SIZE
, then no batch will contain the block with block height end_block - 1
.
The missing check of the last enabled block against a trusted header hash as a root of trust thus means that it is possible to use an arbitrary chain of blocks instead, with data hashes of an attacker's choosing.
Impact
An attacker could generate a proof for the CombinedSkipCircuit in which target_block > trusted_block + NB_MAP_JOBS * BATCH_SIZE
and return a data commitment with arbitrary data hashes of the attacker's choosing.
Recommendations
Consider adding a constraint in prove_data_commitment
that verifies end_block <= start_block + NB_MAP_JOBS * BATCH_SIZE
.
Remediation
This issue has been acknowledged by Succinct, and a fix was implemented in the following commit: .