test(gnovm): bench the gas inputs a caller controls - #5994
Draft
davd-gzl wants to merge 4 commits into
Draft
Conversation
The IBC gas table is generated by gen_native_table.py from these benchmarks, so a constant can only describe a shape the benchmark actually sampled. Two entries never sampled theirs. innerHash was benched once, with two fixed 32-byte digests. One point fits only as SizeFlat, so the table charges 7513 regardless of size while both arguments accept []byte of any length. Measured through the same dispatch harness, 4096 bytes per side costs 69476ns against that flat 7513, and it keeps growing. Now benched at 32/256/1024/4096 so the fitter can see both arguments. modExp ties base, exponent and modulus to a single n, sampling only the diagonal len(exp) == len(mod). Its own comment says cost is len(exp) * len(mod), but nothing makes a caller respect the diagonal: with the modulus held at the 256-byte ceiling, raising the exponent from 32 to 1024 bytes takes 301974ns to 9105730ns, none of which the fit can see. Adds off-diagonal rows that vary the exponent alone. No table values change here. The table must be regenerated through the fitter on reference hardware, which its own header already requires before any consensus-relevant deployment.
Collaborator
🛠 PR Checks SummaryAll Automated Checks passed. ✅ Manual Checks (for Reviewers):
Read More🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers. ✅ Automated Checks (for Contributors):🟢 Maintainers must be able to edit this pull request (more info) ☑️ Contributor Actions:
☑️ Reviewer Actions:
📚 Resources:Debug
|
makeBigDec is deterministic, so calling it twice with the same argument produced two equal values and the benchmark measured x - x rather than a general subtraction. This is hygiene, not the explanation for the current constants. Measured either way the difference is small (872ns equal, 756ns distinct at 1000 digits), so the degenerate input is not what produced OpCPUSlopeBigDecSub. What the measurement does show is that the two constants disagree with the work. Across 100 to 10000 digits, subtraction costs 0.675 ns/digit and addition 0.694, essentially identical, while the table charges 0.20 and 3.75 respectively. The ratio is hardware-independent even though the absolute numbers are not, and an 18.75x gap between two operations that measure the same is not supported by any input shape I could construct. Both constants need refitting on reference hardware over a range where the slope is visible: at 10 and 100 digits both operations sit at their base cost, so a fit weighted to small inputs describes noise.
davd-gzl
force-pushed
the
fix/ibc-crypto-gas-calibration
branch
from
July 21, 2026 18:45
ca00395 to
a8faf20
Compare
The gas table in gnovm/stdlibs/native_gas.go is generated by gen_native_table.py, whose spec list could only describe one variable argument per native: a single (slope_idx, kind) pair, or nothing at all. Two IBC crypto natives need two, and the spec list silently priced them wrong. crypto/merkle.innerHash(left, right []byte) hashes both arguments, but was declared "Flat". A flat charge prices megabytes at the cost of the 64-byte digest case it was calibrated on. crypto/modexp.modExp(base, exp, modulus []byte) was declared with a slope on argument 2, the modulus. The exponent drives the work and was charged nothing, so a small modulus with a huge exponent is cheap to buy and expensive to run. Neither could be corrected by editing the spec list, because the schema had nowhere to put the second argument. The Go side already summed Slope and Slope2 over independent indices and kinds; only the Python side could not express it. Add NATIVE_SPECS_2ARG, carrying two (index, kind) pairs and a regex that captures both sizes, fit as cost = base + s1*N1 + s2*N2 and emitted as Slope/SlopeIdx/SlopeKind plus Slope2/Slope2Idx/Slope2Kind. This is not NATIVE_SPECS_2D, which measures one argument two ways and gives both its slopes the same index. fit_2d is generalized into fit_plane, which both forms now share; 2-D keeps the count*perElem product its second column always was. Move innerHash and modExp onto the new form. Their old regexes matched benchmark names that no longer exist, so the fitter was not even reading their data: innerHash dropped out of the table entirely and modExp demoted to a flat 24327ns against calls measured at 19ms. Re-grid the benchmarks so the two arguments vary independently. On the diagonal N1 == N2 the design columns are identical and the split between the slopes is arbitrary, so innerHash gains off-diagonal L/R pairs and modExp sweeps the exponent with the modulus pinned and vice versa. The fitter now reports when a grid is collinear rather than emitting an unidentified split. Modular exponentiation is superlinear in both lengths and an additive model cannot express that, so the fitter also replays the emitted integers against the measurements and warns when the entry undercharges its own worst sampled point. modExp trips it at 14x; innerHash fits at R²=0.998, as it should, being a single sha256 over the concatenation. Constants in native_gas.go are unchanged: they must be measured on the reference hardware named in that file's header.
…g ones The undercharge replay only ran on the two-slope path, so the shape that actually shipped the bad prices was unchecked. Worse, a poor linear fit was silently demoted to a flat charge: R^2 is low precisely when cost is superlinear, so the weakest possible pricing got chosen exactly when the input was least safe to leave unpriced. That is how innerHash came to bill 7513 for a slice of any length. Replay the emitted integers for one-parameter and flat entries too, and report the measured spread when demoting, so a correct demotion (params setters move 0.9x over N=1..1000) reads differently from a dangerous one (pairingCheck moves 2.3x over N=1..4). Also corrects the modExp spec comment. It claimed the fit was trustworthy over the benched range; the generator measures a 14-18x undercharge at 512-byte exponent against 512-byte modulus, which is a benched point, and X_modExp enforces no ceiling at all.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gas table for native functions is generated from these benchmarks, so a constant can only describe an input shape the benchmark actually measured. Two entries never measured theirs.
innerHashwas benched once, at 32 bytes per side. One data point fits only as a flat charge, so the table bills 7513 whatever the size, while both arguments accept a slice of any length.modExpties base, exponent and modulus to one size, so it only ever sampleslen(exp) == len(mod). Nothing makes a caller respect that.Now benched across sizes, and off the diagonal, so the fit can see what it is pricing.
Also gives the BigDec subtraction benchmark distinct operands; it was measuring
x - x. That one is hygiene rather than a live problem: measured either way the difference is small, so it is not what produced the current constant.No table values change here. Regenerating the table needs the reference hardware its own header already calls for.