Skip to content

perf: fold constant byte operations in std/math/uints - #1796

Open
colll78 wants to merge 1 commit into
Consensys:masterfrom
colll78:feat/uints-fold-constant-byte-ops
Open

perf: fold constant byte operations in std/math/uints#1796
colll78 wants to merge 1 commit into
Consensys:masterfrom
colll78:feat/uints-fold-constant-byte-ops

Conversation

@colll78

@colll78 colll78 commented Jul 15, 2026

Copy link
Copy Markdown

Description

When both operands of a Bytes.And/Or/Xor byte operation are compile-time constants, evaluate the operation natively at compile time instead of emitting a lookup query. The fold is applied per query in the variadic chain, so constant prefixes and constant–constant pairs fold while any witness operand keeps the exact existing lookup path; BinaryField (U32/U64) operations benefit through the shared twoArgFn.

Soundness: both operands of a folded query are already width-checked — circuit inputs by enforceWidth (which panics on constants wider than 8 bits), intermediates because they come either from a lookup response or from a previous fold that produced a uint8. The fold additionally guards on BitLen() > 8 and falls back to the lookup for anything wider, so a constant can never be silently truncated. An unrecognized table also falls back to the lookup path.

Motivation: byte-oriented circuits routinely mix constants (padding bytes, IVs, domain separators, masks) with witness bytes, and each such constant pair currently costs a lookup query plus its share of the log-derivative table commitment. In a production circuit of ours dominated by SHA-512/HMAC this fold removed ~38,000 R1CS constraints; on gnark's own std/hash/sha2 gadget see the numbers below.

Type of change

  • New feature (non-breaking change which adds functionality)

How has this been tested?

  • New TestByteOpConstantFoldMixed: for each of And/Or/Xor, a chain mixing a folded constant pair, a witness operand, and a trailing constant queried against a non-constant intermediate; valid and invalid assignments via assert.CheckCircuit (Groth16 + PLONK, bn254 + bls12-381).
  • New TestByteOpConstantFoldAddsNoConstraints: compiles a constant-only And/Or/Xor circuit and asserts its R1CS constraint count equals a baseline circuit that only instantiates the tables.
  • Full existing suites pass: go test ./std/math/uints/ ./std/hash/sha2/... ./std/hash/sha3/...

How has this been benchmarked?

  • Compiling the std/hash/sha2 gadget over a 64-byte message on bn254 (Linux x86-64): R1CS 184,199 → 183,992 constraints; SCS 560,641 → 559,628 constraints.

Checklist:

  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • I did not modify files generated from templates
  • golangci-lint does not output errors locally (not installed locally; go vet and gofmt are clean)
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

Note

Medium Risk
Touches core bitwise byte logic in circuits; behavior is guarded by width checks and lookup fallback, with new tests, but incorrect folding would affect proof soundness for hash/crypto gadgets.

Overview
Bytes.And / Or / Xor now route each pairwise step through queryOrFold: when both operands are compile-time constants within 8 bits, the op is evaluated natively instead of emitting a log-derivative lookup. Variadic chains still use lookups wherever a witness or wide constant appears; unknown tables fall back to tbl.Query.

BinaryField (U32/U64) picks this up automatically via the shared twoArgFn.

New tests cover mixed constant/witness chains (valid and invalid circuits) and assert constant-only byte ops add no R1CS constraints beyond instantiating the lookup tables.

Reviewed by Cursor Bugbot for commit 926e18b. Bugbot is set up for automated code reviews on this repo. Configure here.

When both operands of an And/Or/Xor byte operation are compile-time
constants, evaluate the operation natively at compile time instead of
emitting a lookup query. The fold applies per query in the variadic
chain, so constant prefixes and constant-constant pairs fold while any
witness operand keeps the lookup path. Both operands are already
width-checked, and the fold additionally guards on BitLen so an
out-of-range constant falls back to the lookup instead of being
truncated.

Circuits over byte-oriented gadgets commonly mix constants (padding,
IVs, domain separators) with witness bytes; those queries and their
log-derivative table costs disappear. Compiling the sha2 gadget over a
64-byte message: r1cs 184,199 -> 183,992 constraints, scs 560,641 ->
559,628 constraints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant