Skip to content

perf: give branches stable nonce identities - #34

Open
dangerousfood wants to merge 8 commits into
codex/integrator-taker-feefrom
codex/nonce-addressed-branches
Open

dangerousfood wants to merge 8 commits into
codex/integrator-taker-feefrom
codex/nonce-addressed-branches

Conversation

@dangerousfood

@dangerousfood dangerousfood commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • give every internal branch a unique, stable 32-bit identity; no branch-identity bits are reserved for depth
  • keep price, quantity, correction, and the full uint32 branch identity in the packed branch summary stored in its parent
  • store children exclusively at tree[bytes32(branchIdentity)], so quantity/correction rewrites never relocate child slots
  • allocate order nonces downward and branch identities upward in the same 32-bit namespace, rotating the book before the two fronts meet
  • preserve two-slot Branch storage while retaining cheap routing:
    • mixed-price split depth is derived from the two child ticks
    • same-price split depth is cached in five redundant high tick bits of the stored left-child representation; all internal/public readers restore the exact child node
  • reuse a single mapping hash when insertion/cancellation loads both children
  • avoid storage operations that stable branch identity makes unnecessary:
    • identify leaves/uniform branches from the packed correction code before loading a child slot
    • rewrite only the child pointer that actually changed during insert, cancel, materialization, and partial match
    • carry the already-loaded nonceAndFlags word through match/rest/cancel routing instead of reloading it

This removes the cascading child-slot moves caused by quantity/correction changes without reducing the identity field to 26 bits. Both order nonces and branch identities use the complete 32-bit node-identity field. A boundary test allocates a branch identity with bit 31 set and verifies it is preserved exactly.

Existing branches retain their identity when their packed summary changes; only a new topology node consumes a branch identity. The ascending branch and descending order fronts are shared across both sides of one book and are checked before every rest, so a branch identity cannot alias an order nonce.

Why not a separate flat bookId + nonce storage scheme?

Solidity's nested mapping already derives the effective branch location from those two identities: the book base comes from bookId, and tree[branchIdentity] hashes the identity with that book's tree slot. Passing an explicit bookId into unstructured storage would replace that hash with a longer preimage; it would not remove the cold SLOAD/SSTORE. The material saving comes from keeping the branch identity stable so child slots never move.

Incremental gas result for the follow-up optimizations

Canonical isolated snapshots compare the prior full-width implementation (1ce25f8) with this commit (247e715). The values below are sums of separate benchmark transactions used only as an aggregate regression signal; 4.44M is not the gas cost of one fill.

Snapshot group Independent transactions Sum before Sum after Aggregate delta Lower / same / higher
Base 28 4,474,650 4,440,779 -0.757% 22 / 1 / 5
Hooks 28 4,925,467 4,891,689 -0.686% 22 / 1 / 5
Fees 28 4,535,925 4,502,054 -0.747% 22 / 1 / 5
Integrator fees 5 609,433 608,614 -0.134% 5 / 0 / 0
All runtime cases 89 14,545,475 14,443,136 -0.704% 71 / 3 / 15

The median case improves 0.171%. The largest incremental improvement is the pathological partial-fill comb at -3.053%. The largest incremental regression is only +0.079% and is a hook-enabled unfilled cancellation; opcode tracing confirms that path has the same four SLOADs and two SSTOREs, so the small change is control-flow/stack overhead rather than an added storage operation.

Representative base paths

Runtime path Before After Delta
64-level pathological off-spine partial fill 666,725 646,370 -20,355 (-3.053%)
Ask partially fills off-spine bid branch 119,360 116,427 -2,933 (-2.457%)
Bid partially fills off-spine ask branch 119,208 116,281 -2,927 (-2.455%)
Dirty same-price bid-subtree consumption 88,150 87,479 -671 (-0.761%)
Dirty same-price ask-subtree consumption 88,306 87,641 -665 (-0.753%)
Full-depth bid comb consumption 335,775 334,093 -1,682 (-0.501%)
Max-depth ask comb consumption 331,994 330,318 -1,676 (-0.505%)
Rest ask into a 5,000-order side 163,956 163,676 -280 (-0.171%)
Rest bid into a 5,000-order side 173,330 173,052 -278 (-0.160%)
Full single-bid match 77,713 77,552 -161 (-0.207%)
Rest ask into empty book 150,847 150,847 0 (0.000%)
Cancel unfilled bid 39,831 39,861 +30 (+0.075%)

Measured storage-op changes

Debugger opcode dumps isolate the final engine call in each benchmark:

Engine call SLOAD before → after SSTORE before → after
Ask partially fills off-spine bid branch 13 → 9 3 → 2
Consume dirty same-price bid subtree 20 → 15 1 → 1
Fully match one bid 6 → 4 1 → 1
Rest ask into empty book 5 → 5 3 → 3
Cancel unfilled bid 4 → 4 2 → 2

Combined comparison with the PR parent

Runtime path Parent This PR Delta
Ask partially fills off-spine bid branch 159,849 116,427 -27.16%
Bid partially fills off-spine ask branch 159,696 116,281 -27.19%
64-level pathological off-spine partial fill 3,255,481 646,370 -80.15%
Cancel 64-deep bid comb 423,078 399,374 -5.60%
Cancel max-depth ask comb 424,751 399,065 -6.05%
Rest ask into a 5,000-order side 163,742 163,676 -0.04%
Rest bid into a 5,000-order side 173,067 173,052 -0.01%
Rest ask into empty book 150,668 150,847 +0.12%
Rest bid into empty book 152,432 152,603 +0.11%

Across the 80 runtime-snapshot cases shared with the parent, aggregate gas falls 0.83%. The median change is +0.119%; the largest remaining regression is +2.11% on deep full-subtree consumption. The stable-identity architecture therefore removes the severe partial-update and cancellation cliffs, while the follow-up changes recover gas broadly across the common match/rest paths.

Verification

Local verification on 247e715:

  • forge fmt --check
  • forge lint (warnings only)
  • forge build --sizes src/DeepstateV1.sol (23,354-byte runtime; 1,222-byte margin)
  • canonical isolated runtime snapshot: 89/89 passed
  • behavioral/unit suite: 404/404 passed
  • invariant suite: 38/38 at 256 runs × 64 calls, 0 handler reverts
  • coverage suite: 244/244 passed; adjusted lines/statements/branches/functions all 100%

CI reruns the full sharded invariant, coverage, Halmos, SMT, static-analysis, build-size, and gas gates for the pushed commit.

@dangerousfood

Copy link
Copy Markdown
Collaborator Author

Comprehensive A/B gas comparison

Baseline is parent 40db8a0; candidate is 9ceba6b. Both use the pinned Foundry toolchain. Canonical numbers below use forge test --isolate. New benchmark methods were copied unchanged onto the baseline so the byte-for-byte fixture is the same; temporary scaling tests were removed after measurement.

Meaning of the 5,000-order result

This measures one additional rest into a side that already contains 5,000 orders. Setup is outside the gas meter. It does not mean that constructing the entire 5,000-order book is 29.7% more expensive.

The old packed branch word contained a descendant order path, so Patricia split decisions could read a routing key directly from the branch. The new low 32 bits are an independent branch identity. The current implementation calls _leftmostLeaf to recover an actual descendant order key when routing through a branch. Those extra storage reads grow with tree depth.

Existing 5,000-order-side rest Baseline Candidate Delta
Ask 163,742 212,228 +48,486 (+29.61%)
Bid 173,067 224,480 +51,413 (+29.71%)

A controlled one-side insertion sweep shows the scaling directly:

Existing orders Baseline Candidate Delta
1 140,701 140,946 +245 (+0.17%)
4 142,952 150,271 +7,319 (+5.12%)
16 143,088 155,255 +12,167 (+8.50%)
64 143,188 169,051 +25,863 (+18.06%)
256 142,730 175,441 +32,711 (+22.92%)
1,024 143,252 183,023 +39,771 (+27.76%)
5,000 143,819 195,074 +51,255 (+35.64%)

The absolute 5,000-order overhead is consistently about 51k; the percentage differs from the checked-in benchmark because each harness has different fixed costs.

Intended optimization: off-spine quantity reduction

Surviving branch depth Baseline Candidate Delta
2 156,972 117,797 -39,175 (-24.96%)
4 255,708 138,121 -117,587 (-45.98%)
8 455,341 180,929 -274,412 (-60.27%)
16 853,514 265,451 -588,063 (-68.90%)
32 1,650,742 435,376 -1,215,366 (-73.63%)
64 3,256,047 714,168 -2,541,879 (-78.07%)

This is the intended win: every surviving ancestor keeps its nonce-addressed child slot, so the unwind rewrites the parent pointer rather than relocating both child pointers.

Exact-key cancellation tradeoff

Path depth Baseline Candidate Delta
2 66,625 71,834 +5,209 (+7.82%)
4 77,571 88,196 +10,625 (+13.70%)
8 100,387 121,844 +21,457 (+21.37%)
16 145,838 188,959 +43,121 (+29.57%)
32 234,798 321,247 +86,449 (+36.82%)
64 414,465 575,570 +161,105 (+38.87%)

Cancellation receives no child-slot-move benefit when collapsing/promoting nodes, but it still pays for descendant-key routing. This is the largest regression and should not be hidden.

Complete checked-in snapshot coverage

The snapshot contains 80 shared cases plus 9 new cases. The categories below account for every entry:

Category Base Fee Hook
19 ordinary fills/cancels/rests, aggregate +0.181% +0.180% +0.127%
Deep full-subtree fills +0.31–0.32% +0.29–0.32% +0.33–0.34%
5,000-order-side rests +29.61–29.71% +29.57–29.67% +25.04–25.88%
64-level exact cancellations +37.67–38.10% +37.67–38.10% +35.85–36.18%
New 4-leaf off-spine partial fills -25.91–25.93% -25.81–25.83% -23.77–23.95%
New 64-level off-spine partial fill -78.08% -77.47% -77.50%

For the 19 ordinary base-profile paths, every individual delta is between +0.07% and +0.28%. Large-book best-leaf partial fills are only +0.17% because the existing dirty-right-spine optimization already avoided cascading rewrites there.

Integrator profile: three match paths are +6 gas, empty-book rest is +204 gas (+0.13%), and repeated-fee-token routing improves by 1,988 gas (-1.62%).

The earlier -40.5% / -94.4% figures were non-isolated forge test A/B results. The repository's canonical isolated snapshot gives -25.9% / -78.1%; absolute savings remain approximately 41.4k gas for the shallow off-spine cases and 2.54M gas for the 64-level case.

Conclusion

This PR is a strong, depth-amplifying optimization for off-spine quantity mutation, but it is not a universal gas reduction. The current descendant-key recovery creates meaningful insertion and exact-cancellation regressions. A follow-up can likely remove most of those reads by using the stored representative price for price-divergent branches and descending only when the Patricia split is in the nonce half (same-price routing).

@dangerousfood

Copy link
Copy Markdown
Collaborator Author

Implemented the follow-up routing redesign in 03eec7b.

The branch nonce now carries both stable identity and the cached Patricia split depth: (serial << 6) | depth. Cancellation routes directly from that depth; mixed-price insertion uses the cached depth and representative tick; branch rebuilds preserve the identity and no longer descend merely to recover a tick. Child loads also share one mapping hash.

This eliminates the previous gas cliffs:

Base-profile path Original parent Previous PR version 03eec7b
5,000-order rest ask 163,742 212,228 (+29.61%) 163,817 (+0.05%)
5,000-order rest bid 173,067 224,480 (+29.71%) 173,191 (+0.07%)
Deep bid cancellation 423,078 584,264 (+38.10%) 376,773 (-10.95%)
Deep ask cancellation 424,751 584,741 (+37.67%) 376,482 (-11.36%)
Pathological partial fill 3,255,481 713,602 (-78.08%) 644,069 (-80.22%)

These are canonical --isolate measurements. The updated snapshot has 89/89 passing entries. Remaining shared-path overhead is bounded to roughly 0.04-0.34%; there is no longer a large insertion or cancellation regression.

@dangerousfood

Copy link
Copy Markdown
Collaborator Author

Final comprehensive gas diff (40db8a0a8d3afd)

These are actual forge snapshot --isolate --force results. The 80 rows below are every canonical runtime case present in the parent snapshot and this PR snapshot; no rows are sampled or estimated. Negative is cheaper.

Suite Cases Cheaper Higher Aggregate Median Worst regression
Base paths (no hook / no fee) 25 4 21 -2.507% +0.163% +0.319%
Hook-enabled paths 25 4 21 -2.241% +0.117% +0.335%
Protocol-fee paths 25 4 21 -2.483% +0.162% +0.318%
Integrator fee paths 5 1 4 -0.319% +0.006% +0.013%
All shared cases 80 13 67 -2.297% +0.129% +0.335%

The result is not literally cheaper in every row. Most common-path movements are 25–240 gas (≤0.335%), while deep mutation paths fall by tens of thousands of gas and aggregate gas falls 2.30%.

Base paths (no hook / no fee) — all shared cases
Path Parent Final Gas Δ % Δ
CancelAskSkipsPathologicalBidTree 39,664 39,778 +114 +0.287%
CancelFilledBidClaim 36,750 36,852 +102 +0.278%
CancelFullDepthBidCombRightmost 423,078 376,773 -46,305 -10.945%
CancelMaxValidDepthAskCombRightmost 424,751 376,482 -48,269 -11.364%
CancelPartialBid 48,804 48,918 +114 +0.234%
CancelUnfilledAsk 39,881 39,995 +114 +0.286%
CancelUnfilledBid 39,717 39,831 +114 +0.287%
FillAskConsumesDirtySamePriceBidSubtree 87,242 87,411 +169 +0.194%
FillAskConsumesFullDepthBidComb 327,303 328,313 +1,010 +0.309%
FillAskFullyMatchesSingleBid 77,593 77,713 +120 +0.155%
FillAskPartiallyMatchesSingleBid 97,288 97,408 +120 +0.123%
FillBidConsumesAskAndRestsRemainder 130,241 130,314 +73 +0.056%
FillBidConsumesDirtySamePriceAskSubtree 87,376 87,567 +191 +0.219%
FillBidConsumesMaxValidDepthAskComb 323,500 324,532 +1,032 +0.319%
FillBidConsumesSamePriceAskSubtree 77,368 77,422 +54 +0.070%
FillBidFullyMatchesSingleAsk 77,268 77,388 +120 +0.155%
FillBidPartiallyMatchesSingleAsk 97,182 97,302 +120 +0.123%
FillRestAskEmptyBook 150,668 150,806 +138 +0.092%
FillRestBidEmptyBook 152,432 152,565 +133 +0.087%
LargeRandomBookAskMatchesOneBid 121,497 121,695 +198 +0.163%
LargeRandomBookBidMatchesOneAsk 136,708 136,942 +234 +0.171%
LargeRandomBookPartialFillsAsk 137,846 138,086 +240 +0.174%
LargeRandomBookPartialFillsBid 122,621 122,825 +204 +0.166%
LargeRandomBookRestsAsk 163,742 163,609 -133 -0.081%
LargeRandomBookRestsBid 173,067 172,983 -84 -0.049%
Hook-enabled paths — all shared cases
Path Parent Final Gas Δ % Δ
CancelAskSkipsPathologicalBidTree 43,705 43,780 +75 +0.172%
CancelFilledBidClaim 37,497 37,563 +66 +0.176%
CancelFullDepthBidCombRightmost 446,433 400,481 -45,952 -10.293%
CancelMaxValidDepthAskCombRightmost 447,267 399,351 -47,916 -10.713%
CancelPartialBid 53,592 53,669 +77 +0.144%
CancelUnfilledAsk 43,922 43,997 +75 +0.171%
CancelUnfilledBid 44,505 44,582 +77 +0.173%
FillAskConsumesDirtySamePriceBidSubtree 95,252 95,373 +121 +0.127%
FillAskConsumesFullDepthBidComb 414,778 416,130 +1,352 +0.326%
FillAskFullyMatchesSingleBid 85,769 85,841 +72 +0.084%
FillAskPartiallyMatchesSingleBid 105,472 105,544 +72 +0.068%
FillBidConsumesAskAndRestsRemainder 139,218 139,243 +25 +0.018%
FillBidConsumesDirtySamePriceAskSubtree 94,243 94,386 +143 +0.152%
FillBidConsumesMaxValidDepthAskComb 410,116 411,490 +1,374 +0.335%
FillBidConsumesSamePriceAskSubtree 93,348 93,384 +36 +0.039%
FillBidFullyMatchesSingleAsk 84,195 84,267 +72 +0.086%
FillBidPartiallyMatchesSingleAsk 104,117 104,189 +72 +0.069%
FillRestAskEmptyBook 140,357 140,447 +90 +0.064%
FillRestBidEmptyBook 142,118 142,203 +85 +0.060%
LargeRandomBookAskMatchesOneBid 138,973 139,140 +167 +0.120%
LargeRandomBookBidMatchesOneAsk 154,786 154,995 +209 +0.135%
LargeRandomBookPartialFillsAsk 146,871 147,063 +192 +0.131%
LargeRandomBookPartialFillsBid 133,256 133,412 +156 +0.117%
LargeRandomBookRestsAsk 193,723 193,607 -116 -0.060%
LargeRandomBookRestsBid 198,650 198,565 -85 -0.043%
Protocol-fee paths — all shared cases
Path Parent Final Gas Δ % Δ
CancelAskSkipsPathologicalBidTree 39,664 39,778 +114 +0.287%
CancelFilledBidClaim 36,750 36,852 +102 +0.278%
CancelFullDepthBidCombRightmost 423,078 376,773 -46,305 -10.945%
CancelMaxValidDepthAskCombRightmost 424,771 376,502 -48,269 -11.364%
CancelPartialBid 48,804 48,918 +114 +0.234%
CancelUnfilledAsk 39,881 39,995 +114 +0.286%
CancelUnfilledBid 39,717 39,831 +114 +0.287%
FillAskConsumesDirtySamePriceBidSubtree 87,846 88,015 +169 +0.192%
FillAskConsumesFullDepthBidComb 352,999 354,009 +1,010 +0.286%
FillAskFullyMatchesSingleBid 78,197 78,317 +120 +0.153%
FillAskPartiallyMatchesSingleBid 97,892 98,012 +120 +0.123%
FillBidConsumesAskAndRestsRemainder 130,838 130,911 +73 +0.056%
FillBidConsumesDirtySamePriceAskSubtree 87,970 88,161 +191 +0.217%
FillBidConsumesMaxValidDepthAskComb 324,094 325,126 +1,032 +0.318%
FillBidConsumesSamePriceAskSubtree 77,962 78,016 +54 +0.069%
FillBidFullyMatchesSingleAsk 77,862 77,982 +120 +0.154%
FillBidPartiallyMatchesSingleAsk 97,776 97,896 +120 +0.123%
FillRestAskEmptyBook 150,894 151,032 +138 +0.091%
FillRestBidEmptyBook 152,651 152,784 +133 +0.087%
LargeRandomBookAskMatchesOneBid 122,101 122,299 +198 +0.162%
LargeRandomBookBidMatchesOneAsk 137,302 137,536 +234 +0.170%
LargeRandomBookPartialFillsAsk 138,440 138,680 +240 +0.173%
LargeRandomBookPartialFillsBid 123,225 123,429 +204 +0.166%
LargeRandomBookRestsAsk 163,968 163,835 -133 -0.081%
LargeRandomBookRestsBid 173,286 173,202 -84 -0.048%
Integrator fee paths — all shared cases
Path Parent Final Gas Δ % Δ
IntegratorAndProtocolFeeFill 129,374 129,380 +6 +0.005%
IntegratorFillAskFullyMatchesSingleBid 103,768 103,774 +6 +0.006%
IntegratorFillBidFullyMatchesSingleAsk 103,888 103,894 +6 +0.006%
IntegratorFillRestBidEmptyBook 151,809 151,828 +19 +0.013%
IntegratorRouteNetsRepeatedFeeToken 122,504 120,516 -1,988 -1.623%

Targeted cases added on this branch

These three benchmarks did not exist in the parent snapshot file, so they were run separately against the parent implementation with the same isolated command and fixtures.

Base path Parent Final Delta
Ask partially fills off-spine bid branch 159,849 118,438 -25.91%
Bid partially fills off-spine ask branch 159,696 118,286 -25.93%
64-level pathological off-spine partial fill 3,255,481 644,069 -80.22%

First attempt → final smoothing

Cliff in the first stable-identity version First attempt Final Improvement
Rest ask into 5,000-order side 212,228 163,609 -22.91%
Rest bid into 5,000-order side 224,480 172,983 -22.94%
Cancel 64-deep bid comb 584,264 376,773 -35.51%
Cancel max-depth ask comb 584,741 376,482 -35.62%
Pathological off-spine partial fill 713,602 644,069 -9.74%

Final verification: 89/89 isolated gas cases, 401/401 behavioral tests, 38/38 invariants at 256×64 with zero handler reverts, and 241/241 coverage tests with adjusted 100% lines/statements/branches/functions.

@dangerousfood

Copy link
Copy Markdown
Collaborator Author

Updated the implementation to preserve the full 32-bit branch identity field—no serial << 6, no 26-bit cap, and no depth bits taken from the nonce.

Routing depth now comes from child ticks for mixed-price branches and from redundant stored-left tick bits for same-price branches. The externally visible child is decoded back to its exact node. The two-slot Branch layout and stable nonce-addressed child slots are unchanged.

Added a boundary test with branch identity 1 << 31, reran 403 behavioral tests, 38 invariants at 256×64 with zero handler reverts, 243 coverage tests with adjusted 100% coverage, the 89-case isolated gas snapshot, and the build-size gate. Full results and the corrected gas table are in the PR description.

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