Assessment reports>Brevis>Medium findings>Padding incorrect for output-commitment computation
Category: Coding Mistakes

Padding incorrect for output-commitment computation

Medium Severity
Informational Impact
N/A Likelihood

Description

In sdk/host_circuit.go, the function commitOutput handles padding before calculating the Keccak hash of the output bits. See Discussion section ref for a description of the padding used for Keccak. In the code, the implementation is as follows:

func (c *HostCircuit) commitOutput(bits []frontend.Variable) OutputCommitment {
	if len(bits)%8 != 0 {
		panic(fmt.Errorf("len bits (%d) must be multiple of 8", len(bits)))
	}

	rounds := len(bits)/1088 + 1
	paddedLen := rounds * 1088
	padded := make([]frontend.Variable, paddedLen)
	copy(padded, bits)

	// pad 101, start from one bit after the
	padded[len(bits)] = 1
	for i := len(bits) + 1; i < paddedLen-1; i++ {
		padded[i] = 0
	}
	padded[len(padded)-1] = 1

    // ...
}

The padding is incorrect whenever len(bits) % 1088 == 1087. In this case, only a single bit is left available in the last unfilled block, which is insufficient for the padding, as the shortest possible padding consist of two bits, 11. Thus, the correct padding would consist of the bit 1, followed by 1,087 bits 0, and finally another bit 1. The implementation appends only the single bit 1, however.

Let us go through the code in the concrete example of len(bits) = 1087 to illustrate this. Then len(bits) / 1088 = 1087 / 1088 will be zero, so we will have rounds = 1, and thus paddedLen = 1088. The lines padded[len(bits)] = 1 and padded[len(padded)-1] = 1 will then set the same bit to 1, and the padding will only consist of this single bit 1.

Impact

For bits satisfying len(bits) % 1088 == 1087, the computed padding will be incorrect, making the hash calculated by commitOutput different than the one calculated by the user's contract on chain.

However, in practice, the length of bits should always be divisible by 8. As long as this is satisfied, the edge case of len(bits) % 1088 == 1087 cannot occur. The main output type of concern would be Uint248, which has functions OutputUint taking a user-provided bit length, and OutputBool, which could plausibly be outputted as a single bit. However, the former checks that the bit size chosen is divisible by 8:

func (api *CircuitAPI) OutputUint(bitSize int, v Uint248) {
	if bitSize%8 != 0 {
		panic("bitSize must be multiple of 8")
	}

    // ...
}

and the latter uses an eight-bit representation:

api.addOutput(api.g.ToBinary(v.Val, 8))

Should these implementations be changed or another output type be added, so that output bitstrings that are of length not divisible by 8 become possible, then the discussed padding error could occur.

Recommendations

The computation of rounds should be replaced by the following formula (or an equivalent one):

rounds := ((len(bits) + 1) / 1088) + 1

Remediation

This issue has been acknowledged by Brevis, and a fix was implemented in commit be28c8e2.

Zellic © 2025Back to top ↑