Assessment reports>Brevis>Discussion>Minor recommendations

Minor recommendations

In this section we collect some minor recommendations.

String function for lists

The String function for lists just places a comma and space consecutively (", ") between each component. Adding, for example, the left square bracket "[" at the start and the right square bracket "]" at the end, similarly to how it is done for tuples with the left and right parentheses "(" and ")", would make it easier to understand the output.

Typo in one of the constants for Poseidon implementation in zk-hash

In the zk-hash repository's poseidon/circuit/constants.go, the very first string, in the function strPOSEIDON_C and for case t == 2, which should consist of a flat list, begins correctly with a single left square bracket "[" introducing the list, but it ends with two right square brackets, "]". Both of the bracket characters are replaced by an empty string in the function parseOneDimensionArray that is used to parse this string, which is why this does not cause an error on runtime.

	s := ""
	if t == 2 {
		s =
			`[0x9c46e9ec68e9bd4fe1faaba294cba38a71aa177534cdd1b6c7dc0dbd0abd7a7,
// ...
0xe3eca007699dd0f852eb22da642e495f67c988dd5bf0137676b16a31eab4667]
]`

We recommend to remove the last right square bracket, ].

Ambiguous genericity in ToUint521

The ToUint521 in the brevis-sdk file sdk/circuit_api.go has the cases for Bytes32 and Uint248 implemented as follows:

case Bytes32:
	// Recompose the Bytes32 into BigField.NbLimbs limbs
	bits := v.toBinaryVars(api.g)
	f := Uint521Field{}
	limbs := make([]variable, f.NbLimbs())
	b := f.BitsPerLimb()
	limbs[0] = api.g.FromBinary(bits[:b]...)
	limbs[1] = api.g.FromBinary(bits[b : 2*b]...)
	limbs[2] = api.g.FromBinary(bits[2*b:]...)
	limbs[3], limbs[4], limbs[5] = 0, 0, 0
	el := api.Uint521.f.NewElement(limbs)
	return newU521(el)
case Uint248:
	el := api.Uint521.f.NewElement([]variable{v.Val, 0, 0, 0, 0, 0})
	return newU521(el)

Note that in the Bytes32 case, the implementation starts in a way that does not hardcode the number of limbs of a Uint521, by instantiating limbs with f.NbLimbs() components instead of a hardcoded 6. However, the code following this assumes that this is what the value was; if f.NbLimbs() were smaller, one of the assignments would panic due to an out-of-bounds write attempt. In the second case shown, Uint248 constructs the result, assuming six limbs as well.

Similarly, the function appears to be independent of the precise value of f.BitsPerLimbs() but makes the implicit assumption that 3*b >= 256 so that the 256 bits from Bytes32 fits into the first three limbs.

We recommend to make the code more consistent by either hardcoding actual values throughout in this function or making the implementation independent of the values.

In the former case, it would be good to assert that the values used are equal to f.NbLimbs() and f.BitsPerLimb() to ensure that, should one or both change later, it will not be forgotten to update the ToUint521 function.

String representation of LogField is empty

In the sdk repository's sdk/circuit_input.go, the type LogField and its method function String is defined. They are given by the following:

// LogField represents a single field of an event.
type LogField struct {
	// The contract from which the event is emitted
	Contract Uint248
	// The event ID of the event to which the field belong (aka topics[0])
	EventID Uint248
	// Whether the field is a topic (aka "indexed" as in solidity events)
	IsTopic Uint248
	// The index of the field. For example, if a field is the second topic of a log, then Index is 1; if a field is the
	// third field in the RLP decoded data, then Index is 2.
	Index Uint248
	// The value of the field in event, aka the actual thing we care about, only 32-byte fixed length values are supported.
	Value Bytes32
}

func (f LogField) String() string { return "" }

The string representation of a LogField is thus just the empty string. In contrast, similar types have nonempty string representations:

func (s StorageSlot) String() string { return "StorageSlot" }

func (r Receipt) String() string { return "Receipt" }

// ...

It may be considered to return a descriptive string for LogField as well (such as "LogField").

Inconsistent serialization of Transaction struct

In the sdk repository, a Transaction struct is defined in sdk/circuit_input.go as follows:

type Transaction struct {
	ChainId  Uint248
	BlockNum Uint32
	Nonce    Uint248
	// GasTipCapOrGasPrice is GasPrice for legacy tx (type 0) and GasTipCapOap for
	// dynamic-fee tx (type 2)
	GasTipCapOrGasPrice Uint248
	// GasFeeCap is always 0 for legacy tx
	GasFeeCap Uint248
	GasLimit  Uint248
	From      Uint248
	To        Uint248
	Value     Bytes32
}

This struct is serialized with a pack function:

func (t Transaction) pack(api frontend.API) []variable {
	var bits []variable
	bits = append(bits, api.ToBinary(t.BlockNum.Val, 8*4)...)
	bits = append(bits, api.ToBinary(t.ChainId.Val, 8*4)...)
	bits = append(bits, api.ToBinary(t.Nonce.Val, 8*4)...)
	bits = append(bits, api.ToBinary(t.GasTipCapOrGasPrice.Val, 8*8)...)
	bits = append(bits, api.ToBinary(t.GasFeeCap.Val, 8*8)...)
	bits = append(bits, api.ToBinary(t.GasLimit.Val, 8*4)...)
	bits = append(bits, api.ToBinary(t.From.Val, 8*20)...)
	bits = append(bits, api.ToBinary(t.To.Val, 8*20)...)
	bits = append(bits, t.Value.toBinaryVars(api)...)
	return packBitsToFr(api, bits)
}

Note that in pack, the field BlockNum is serialized before ChainId, while in the struct these fields are listed in the other order. This inconsistency is not a problem in itself, but it could cause confusion later in development, so we recommend to keep the order consistent.

Incorrect error message in assignToggleCommitment

In the sdk repository's sdk/app.go, in assignToggleCommitment, there is the following snippet:

result, err := hasher.Sum()
if err != nil {
	panic(fmt.Sprintf("invalid toggles length %d", len(toggles)))
}

The error message was likely copied from another error earlier in the function and does not fit this error. We recommend to update the error message to something like the following:

panic(fmt.Sprintf("failed to hash toggle bit leaf: %s", err.Error()))

Incomplete or misleading error messages in validateInput

The validateInput function in the sdk/host_circuit.go file of the sdk repository carries out some sanity checks regarding the inputs. It is implemented as follows:

func (c *HostCircuit) validateInput() error {
	d := c.Input
	inputLen := len(d.Receipts.Raw) + len(d.StorageSlots.Raw) + len(d.Transactions.Raw)
	if inputLen > NumMaxDataPoints {
		return fmt.Errorf("input len must be less than %d", NumMaxDataPoints)
	}
	maxReceipts, maxSlots, maxTransactions := c.Guest.Allocate()
	if len(d.Receipts.Raw) != len(d.Receipts.Toggles) || len(d.Receipts.Raw) != maxReceipts {
		return fmt.Errorf("receipt input/toggle len mismatch: %d vs %d",
			len(d.Receipts.Raw), len(d.Receipts.Toggles))
	}
	if len(d.StorageSlots.Raw) != len(d.StorageSlots.Toggles) || len(d.StorageSlots.Raw) != maxSlots {
		return fmt.Errorf("storageSlots input/toggle len mismatch: %d vs %d",
			len(d.StorageSlots.Raw), len(d.StorageSlots.Toggles))
	}
	if len(d.Transactions.Raw) != len(d.Transactions.Toggles) || len(d.Transactions.Raw) != maxTransactions {
		return fmt.Errorf("transaction input/toggle len mismatch: %d vs %d",
			len(d.Transactions.Raw), len(d.Transactions.Toggles))
	}
	return nil
}

The first check is to ensure that the total number of inputs is not higher than the maximum that is allowed, NumMaxDataPoints. The error message fmt.Errorf("input len must be less than %d", NumMaxDataPoints) is inaccurate as it suggests that an input length that is equal to the maximum is not allowed, while it is. Thus, fmt.Errorf("input len must be less than or equal to %d", NumMaxDataPoints) would be more accurate.

The other three checks are all analogous and check that for each of the three input types, the amount of raw data entries matches the configured allocation and that this is also equal to the number of toggles. The error messages, however, only mention the input and toggle lengths and suggest that they mismatch, when the error could also have arisen due to a mismatch with the configured maximum amount. Updating the first error message to

fmt.Errorf("mismatch between receipt input length, toggle length, or allocated receipt length: %d vs %d vs %d",
			len(d.Receipts.Raw), len(d.Receipts.Toggles), maxReceipts)

would make it more accurate and useful — analogously for the other two types.

Typo in buildSendRequestCalldata error message

The buildSendRequestCalldata function in sdk/app.go of the sdk repository has a typo in an error message, where fonud is used instead of found.

Typo in calMerkelRoot function name

The function calMerkelRoot in the sdk repository's sdk/host_circuit.go is used to calculate the root hash of a Merkle tree. The function name has a typo however, using "Merkel" instead of "Merkle".

Ethereum header Nonce not zero

In the zk-bridge repository's circuits/fabric/headers/util.go, the function genHeader generates an Ethereum header with dummy values. The field Nonce is filled with the byte 1:

Nonce:           [8]byte{1, 1, 1, 1, 1, 1, 1, 1},

The Ethereum yellow paper describes the Nonce field as follows: "A 64-bit value that is now deprecated due to the replacement of proof of work consensus. It is set to 0x0000000000000000; formally Hn."

For this reason, usage of a dummy value of all zero might be better.

Environment variables ignored in NewService

In the sdk repository, the following function in sdk/prover/server.go

func NewService(app sdk.AppCircuit, config ServiceConfig) (*Service, error) {
	pk, vk, ccs, err := readOrSetup(app, config.SetupDir, config.GetSrsDir())
	if err != nil {
		return nil, err
	}
	return &Service{
		svr: newServer(app, pk, vk, ccs),
	}, nil
}

should likely use config.GetSetupDir() instead of config.SetupDir to handle usage of environmental variables in the path correctly, similarly to how it is done for the SRS dir with config.GetSrsDir().

Dead code in Poseidon implementation

The function MixS in the zk-hash repository's poseidon/circuit/circuit.go is never used anywhere and could thus be removed.

Likely unnecessary check in Bytes2Bits

In the zk-hash repository, the Bytes2Bits function in keccak/periphery.go is implemented as follows:

func Bytes2Bits(bytes []byte) (bits []uint8) {
	if len(bytes)%8 != 0 {
		panic("invalid length")
	}
	for i := 0; i < len(bytes); i++ {
		bits = append(bits, byte2Bits(bytes[i])...)
	}
	return
}

It is unclear why the check that 8 divides len(bytes) is performed in this function. If this check is not needed, it should be removed so that the function also becomes usable in other cases. Should this check have meaning in the context of a particular use (for example, when serializing data into a sequence of 64-bit words), consider creating a wrapper with a name reflecting the use case and moving the check into the wrapper.

Keccak functions require round indexes while padding requires input length

The zk-hash repository offers multiple functions to calculate the Keccak hash (Keccak256BitsOptimized, Keccak256Bits, and Keccak256). These require a round index as an argument. This argument roundIndex can be a circuit variable and not just a native constant, so the Keccak functions support variable-length inputs in circuit. The in-circuit padding function Pad101Bits instead gets the length of the input as an argument. This inLen argument can also be a circuit variable, so Pad101Bits supports variable length inputs as well. However, there is no in-circuit implementation of the calculation of the round index from the input length that a user could use. There is a GetRoundIndex function in keccak/periphery.go, but it only carries out this calculation for the native int type. It might make sense to add a function that constrains the calculation of the round index from the input length in circuit.

Zellic © 2025Back to top ↑