diff --git a/.gas-snapshot.runtime b/.gas-snapshot.runtime index 0e6960b..d0febb9 100644 --- a/.gas-snapshot.runtime +++ b/.gas-snapshot.runtime @@ -1,80 +1,89 @@ -DeepstateV1IntegratorFeeGasTest:testGas_IntegratorAndProtocolFeeFill() (gas: 129374) -DeepstateV1IntegratorFeeGasTest:testGas_IntegratorFillAskFullyMatchesSingleBid() (gas: 103768) -DeepstateV1IntegratorFeeGasTest:testGas_IntegratorFillBidFullyMatchesSingleAsk() (gas: 103888) -DeepstateV1IntegratorFeeGasTest:testGas_IntegratorFillRestBidEmptyBook() (gas: 151809) -DeepstateV1IntegratorFeeGasTest:testGas_IntegratorRouteNetsRepeatedFeeToken() (gas: 122504) -RadixMatchingEngineFeeGasTest:testGas_CancelAskSkipsPathologicalBidTree() (gas: 39664) -RadixMatchingEngineFeeGasTest:testGas_CancelFilledBidClaim() (gas: 36750) -RadixMatchingEngineFeeGasTest:testGas_CancelFullDepthBidCombRightmost() (gas: 423078) -RadixMatchingEngineFeeGasTest:testGas_CancelMaxValidDepthAskCombRightmost() (gas: 424771) -RadixMatchingEngineFeeGasTest:testGas_CancelPartialBid() (gas: 48804) -RadixMatchingEngineFeeGasTest:testGas_CancelUnfilledAsk() (gas: 39881) -RadixMatchingEngineFeeGasTest:testGas_CancelUnfilledBid() (gas: 39717) -RadixMatchingEngineFeeGasTest:testGas_FillAskConsumesDirtySamePriceBidSubtree() (gas: 87846) -RadixMatchingEngineFeeGasTest:testGas_FillAskConsumesFullDepthBidComb() (gas: 352999) -RadixMatchingEngineFeeGasTest:testGas_FillAskFullyMatchesSingleBid() (gas: 78197) -RadixMatchingEngineFeeGasTest:testGas_FillAskPartiallyMatchesSingleBid() (gas: 97892) -RadixMatchingEngineFeeGasTest:testGas_FillBidConsumesAskAndRestsRemainder() (gas: 130838) -RadixMatchingEngineFeeGasTest:testGas_FillBidConsumesDirtySamePriceAskSubtree() (gas: 87970) -RadixMatchingEngineFeeGasTest:testGas_FillBidConsumesMaxValidDepthAskComb() (gas: 324094) -RadixMatchingEngineFeeGasTest:testGas_FillBidConsumesSamePriceAskSubtree() (gas: 77962) -RadixMatchingEngineFeeGasTest:testGas_FillBidFullyMatchesSingleAsk() (gas: 77862) -RadixMatchingEngineFeeGasTest:testGas_FillBidPartiallyMatchesSingleAsk() (gas: 97776) -RadixMatchingEngineFeeGasTest:testGas_FillRestAskEmptyBook() (gas: 150894) -RadixMatchingEngineFeeGasTest:testGas_FillRestBidEmptyBook() (gas: 152651) -RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookAskMatchesOneBid() (gas: 122101) -RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookBidMatchesOneAsk() (gas: 137302) -RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookPartialFillsAsk() (gas: 138440) -RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookPartialFillsBid() (gas: 123225) -RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookRestsAsk() (gas: 163968) -RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookRestsBid() (gas: 173286) -RadixMatchingEngineGasTest:testGas_CancelAskSkipsPathologicalBidTree() (gas: 39664) -RadixMatchingEngineGasTest:testGas_CancelFilledBidClaim() (gas: 36750) -RadixMatchingEngineGasTest:testGas_CancelFullDepthBidCombRightmost() (gas: 423078) -RadixMatchingEngineGasTest:testGas_CancelMaxValidDepthAskCombRightmost() (gas: 424751) -RadixMatchingEngineGasTest:testGas_CancelPartialBid() (gas: 48804) -RadixMatchingEngineGasTest:testGas_CancelUnfilledAsk() (gas: 39881) -RadixMatchingEngineGasTest:testGas_CancelUnfilledBid() (gas: 39717) -RadixMatchingEngineGasTest:testGas_FillAskConsumesDirtySamePriceBidSubtree() (gas: 87242) -RadixMatchingEngineGasTest:testGas_FillAskConsumesFullDepthBidComb() (gas: 327303) -RadixMatchingEngineGasTest:testGas_FillAskFullyMatchesSingleBid() (gas: 77593) -RadixMatchingEngineGasTest:testGas_FillAskPartiallyMatchesSingleBid() (gas: 97288) -RadixMatchingEngineGasTest:testGas_FillBidConsumesAskAndRestsRemainder() (gas: 130241) -RadixMatchingEngineGasTest:testGas_FillBidConsumesDirtySamePriceAskSubtree() (gas: 87376) -RadixMatchingEngineGasTest:testGas_FillBidConsumesMaxValidDepthAskComb() (gas: 323500) -RadixMatchingEngineGasTest:testGas_FillBidConsumesSamePriceAskSubtree() (gas: 77368) -RadixMatchingEngineGasTest:testGas_FillBidFullyMatchesSingleAsk() (gas: 77268) -RadixMatchingEngineGasTest:testGas_FillBidPartiallyMatchesSingleAsk() (gas: 97182) -RadixMatchingEngineGasTest:testGas_FillRestAskEmptyBook() (gas: 150668) -RadixMatchingEngineGasTest:testGas_FillRestBidEmptyBook() (gas: 152432) -RadixMatchingEngineGasTest:testGas_LargeRandomBookAskMatchesOneBid() (gas: 121497) -RadixMatchingEngineGasTest:testGas_LargeRandomBookBidMatchesOneAsk() (gas: 136708) -RadixMatchingEngineGasTest:testGas_LargeRandomBookPartialFillsAsk() (gas: 137846) -RadixMatchingEngineGasTest:testGas_LargeRandomBookPartialFillsBid() (gas: 122621) -RadixMatchingEngineGasTest:testGas_LargeRandomBookRestsAsk() (gas: 163742) -RadixMatchingEngineGasTest:testGas_LargeRandomBookRestsBid() (gas: 173067) -RadixMatchingEngineHookGasTest:testGas_CancelAskSkipsPathologicalBidTree() (gas: 43705) -RadixMatchingEngineHookGasTest:testGas_CancelFilledBidClaim() (gas: 37497) -RadixMatchingEngineHookGasTest:testGas_CancelFullDepthBidCombRightmost() (gas: 446433) -RadixMatchingEngineHookGasTest:testGas_CancelMaxValidDepthAskCombRightmost() (gas: 447267) -RadixMatchingEngineHookGasTest:testGas_CancelPartialBid() (gas: 53592) -RadixMatchingEngineHookGasTest:testGas_CancelUnfilledAsk() (gas: 43922) -RadixMatchingEngineHookGasTest:testGas_CancelUnfilledBid() (gas: 44505) -RadixMatchingEngineHookGasTest:testGas_FillAskConsumesDirtySamePriceBidSubtree() (gas: 95252) -RadixMatchingEngineHookGasTest:testGas_FillAskConsumesFullDepthBidComb() (gas: 414778) -RadixMatchingEngineHookGasTest:testGas_FillAskFullyMatchesSingleBid() (gas: 85769) -RadixMatchingEngineHookGasTest:testGas_FillAskPartiallyMatchesSingleBid() (gas: 105472) -RadixMatchingEngineHookGasTest:testGas_FillBidConsumesAskAndRestsRemainder() (gas: 139218) -RadixMatchingEngineHookGasTest:testGas_FillBidConsumesDirtySamePriceAskSubtree() (gas: 94243) -RadixMatchingEngineHookGasTest:testGas_FillBidConsumesMaxValidDepthAskComb() (gas: 410116) -RadixMatchingEngineHookGasTest:testGas_FillBidConsumesSamePriceAskSubtree() (gas: 93348) -RadixMatchingEngineHookGasTest:testGas_FillBidFullyMatchesSingleAsk() (gas: 84195) -RadixMatchingEngineHookGasTest:testGas_FillBidPartiallyMatchesSingleAsk() (gas: 104117) -RadixMatchingEngineHookGasTest:testGas_FillRestAskEmptyBook() (gas: 140357) -RadixMatchingEngineHookGasTest:testGas_FillRestBidEmptyBook() (gas: 142118) -RadixMatchingEngineHookGasTest:testGas_LargeRandomBookAskMatchesOneBid() (gas: 138973) -RadixMatchingEngineHookGasTest:testGas_LargeRandomBookBidMatchesOneAsk() (gas: 154786) -RadixMatchingEngineHookGasTest:testGas_LargeRandomBookPartialFillsAsk() (gas: 146871) -RadixMatchingEngineHookGasTest:testGas_LargeRandomBookPartialFillsBid() (gas: 133256) -RadixMatchingEngineHookGasTest:testGas_LargeRandomBookRestsAsk() (gas: 193723) -RadixMatchingEngineHookGasTest:testGas_LargeRandomBookRestsBid() (gas: 198650) +DeepstateV1IntegratorFeeGasTest:testGas_IntegratorAndProtocolFeeFill() (gas: 129218) +DeepstateV1IntegratorFeeGasTest:testGas_IntegratorFillAskFullyMatchesSingleBid() (gas: 103606) +DeepstateV1IntegratorFeeGasTest:testGas_IntegratorFillBidFullyMatchesSingleAsk() (gas: 103732) +DeepstateV1IntegratorFeeGasTest:testGas_IntegratorFillRestBidEmptyBook() (gas: 151866) +DeepstateV1IntegratorFeeGasTest:testGas_IntegratorRouteNetsRepeatedFeeToken() (gas: 120192) +RadixMatchingEngineFeeGasTest:testGas_CancelAskSkipsPathologicalBidTree() (gas: 39800) +RadixMatchingEngineFeeGasTest:testGas_CancelFilledBidClaim() (gas: 36860) +RadixMatchingEngineFeeGasTest:testGas_CancelFullDepthBidCombRightmost() (gas: 399374) +RadixMatchingEngineFeeGasTest:testGas_CancelMaxValidDepthAskCombRightmost() (gas: 399085) +RadixMatchingEngineFeeGasTest:testGas_CancelPartialBid() (gas: 48948) +RadixMatchingEngineFeeGasTest:testGas_CancelUnfilledAsk() (gas: 40017) +RadixMatchingEngineFeeGasTest:testGas_CancelUnfilledBid() (gas: 39861) +RadixMatchingEngineFeeGasTest:testGas_FillAskConsumesDirtySamePriceBidSubtree() (gas: 88083) +RadixMatchingEngineFeeGasTest:testGas_FillAskConsumesFullDepthBidComb() (gas: 359789) +RadixMatchingEngineFeeGasTest:testGas_FillAskFullyMatchesSingleBid() (gas: 78156) +RadixMatchingEngineFeeGasTest:testGas_FillAskPartiallyMatchesOffSpineBidBranch() (gas: 117031) +RadixMatchingEngineFeeGasTest:testGas_FillAskPartiallyMatchesSingleBid() (gas: 97851) +RadixMatchingEngineFeeGasTest:testGas_FillBidConsumesAskAndRestsRemainder() (gas: 130694) +RadixMatchingEngineFeeGasTest:testGas_FillBidConsumesDirtySamePriceAskSubtree() (gas: 88235) +RadixMatchingEngineFeeGasTest:testGas_FillBidConsumesMaxValidDepthAskComb() (gas: 330912) +RadixMatchingEngineFeeGasTest:testGas_FillBidConsumesSamePriceAskSubtree() (gas: 77861) +RadixMatchingEngineFeeGasTest:testGas_FillBidFullyMatchesSingleAsk() (gas: 77827) +RadixMatchingEngineFeeGasTest:testGas_FillBidPartiallyMatchesOffSpineAskBranch() (gas: 116875) +RadixMatchingEngineFeeGasTest:testGas_FillBidPartiallyMatchesSingleAsk() (gas: 97741) +RadixMatchingEngineFeeGasTest:testGas_FillRestAskEmptyBook() (gas: 151073) +RadixMatchingEngineFeeGasTest:testGas_FillRestBidEmptyBook() (gas: 152822) +RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookAskMatchesOneBid() (gas: 122655) +RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookBidMatchesOneAsk() (gas: 138168) +RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookPartialFillsAsk() (gas: 139312) +RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookPartialFillsBid() (gas: 123785) +RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookRestsAsk() (gas: 163902) +RadixMatchingEngineFeeGasTest:testGas_LargeRandomBookRestsBid() (gas: 173271) +RadixMatchingEngineFeeGasTest:testGas_PathologicalOffSpinePartialFillBidComb() (gas: 672066) +RadixMatchingEngineGasTest:testGas_CancelAskSkipsPathologicalBidTree() (gas: 39800) +RadixMatchingEngineGasTest:testGas_CancelFilledBidClaim() (gas: 36860) +RadixMatchingEngineGasTest:testGas_CancelFullDepthBidCombRightmost() (gas: 399374) +RadixMatchingEngineGasTest:testGas_CancelMaxValidDepthAskCombRightmost() (gas: 399065) +RadixMatchingEngineGasTest:testGas_CancelPartialBid() (gas: 48948) +RadixMatchingEngineGasTest:testGas_CancelUnfilledAsk() (gas: 40017) +RadixMatchingEngineGasTest:testGas_CancelUnfilledBid() (gas: 39861) +RadixMatchingEngineGasTest:testGas_FillAskConsumesDirtySamePriceBidSubtree() (gas: 87479) +RadixMatchingEngineGasTest:testGas_FillAskConsumesFullDepthBidComb() (gas: 334093) +RadixMatchingEngineGasTest:testGas_FillAskFullyMatchesSingleBid() (gas: 77552) +RadixMatchingEngineGasTest:testGas_FillAskPartiallyMatchesOffSpineBidBranch() (gas: 116427) +RadixMatchingEngineGasTest:testGas_FillAskPartiallyMatchesSingleBid() (gas: 97247) +RadixMatchingEngineGasTest:testGas_FillBidConsumesAskAndRestsRemainder() (gas: 130097) +RadixMatchingEngineGasTest:testGas_FillBidConsumesDirtySamePriceAskSubtree() (gas: 87641) +RadixMatchingEngineGasTest:testGas_FillBidConsumesMaxValidDepthAskComb() (gas: 330318) +RadixMatchingEngineGasTest:testGas_FillBidConsumesSamePriceAskSubtree() (gas: 77267) +RadixMatchingEngineGasTest:testGas_FillBidFullyMatchesSingleAsk() (gas: 77233) +RadixMatchingEngineGasTest:testGas_FillBidPartiallyMatchesOffSpineAskBranch() (gas: 116281) +RadixMatchingEngineGasTest:testGas_FillBidPartiallyMatchesSingleAsk() (gas: 97147) +RadixMatchingEngineGasTest:testGas_FillRestAskEmptyBook() (gas: 150847) +RadixMatchingEngineGasTest:testGas_FillRestBidEmptyBook() (gas: 152603) +RadixMatchingEngineGasTest:testGas_LargeRandomBookAskMatchesOneBid() (gas: 122051) +RadixMatchingEngineGasTest:testGas_LargeRandomBookBidMatchesOneAsk() (gas: 137574) +RadixMatchingEngineGasTest:testGas_LargeRandomBookPartialFillsAsk() (gas: 138718) +RadixMatchingEngineGasTest:testGas_LargeRandomBookPartialFillsBid() (gas: 123181) +RadixMatchingEngineGasTest:testGas_LargeRandomBookRestsAsk() (gas: 163676) +RadixMatchingEngineGasTest:testGas_LargeRandomBookRestsBid() (gas: 173052) +RadixMatchingEngineGasTest:testGas_PathologicalOffSpinePartialFillBidComb() (gas: 646370) +RadixMatchingEngineHookGasTest:testGas_CancelAskSkipsPathologicalBidTree() (gas: 43805) +RadixMatchingEngineHookGasTest:testGas_CancelFilledBidClaim() (gas: 37577) +RadixMatchingEngineHookGasTest:testGas_CancelFullDepthBidCombRightmost() (gas: 423089) +RadixMatchingEngineHookGasTest:testGas_CancelMaxValidDepthAskCombRightmost() (gas: 421941) +RadixMatchingEngineHookGasTest:testGas_CancelPartialBid() (gas: 53704) +RadixMatchingEngineHookGasTest:testGas_CancelUnfilledAsk() (gas: 44022) +RadixMatchingEngineHookGasTest:testGas_CancelUnfilledBid() (gas: 44617) +RadixMatchingEngineHookGasTest:testGas_FillAskConsumesDirtySamePriceBidSubtree() (gas: 95444) +RadixMatchingEngineHookGasTest:testGas_FillAskConsumesFullDepthBidComb() (gas: 421913) +RadixMatchingEngineHookGasTest:testGas_FillAskFullyMatchesSingleBid() (gas: 85683) +RadixMatchingEngineHookGasTest:testGas_FillAskPartiallyMatchesOffSpineBidBranch() (gas: 130654) +RadixMatchingEngineHookGasTest:testGas_FillAskPartiallyMatchesSingleBid() (gas: 105386) +RadixMatchingEngineHookGasTest:testGas_FillBidConsumesAskAndRestsRemainder() (gas: 139029) +RadixMatchingEngineHookGasTest:testGas_FillBidConsumesDirtySamePriceAskSubtree() (gas: 94463) +RadixMatchingEngineHookGasTest:testGas_FillBidConsumesMaxValidDepthAskComb() (gas: 417279) +RadixMatchingEngineHookGasTest:testGas_FillBidConsumesSamePriceAskSubtree() (gas: 93232) +RadixMatchingEngineHookGasTest:testGas_FillBidFullyMatchesSingleAsk() (gas: 84115) +RadixMatchingEngineHookGasTest:testGas_FillBidPartiallyMatchesOffSpineAskBranch() (gas: 129365) +RadixMatchingEngineHookGasTest:testGas_FillBidPartiallyMatchesSingleAsk() (gas: 104037) +RadixMatchingEngineHookGasTest:testGas_FillRestAskEmptyBook() (gas: 140488) +RadixMatchingEngineHookGasTest:testGas_FillRestBidEmptyBook() (gas: 142241) +RadixMatchingEngineHookGasTest:testGas_LargeRandomBookAskMatchesOneBid() (gas: 139499) +RadixMatchingEngineHookGasTest:testGas_LargeRandomBookBidMatchesOneAsk() (gas: 155630) +RadixMatchingEngineHookGasTest:testGas_LargeRandomBookPartialFillsAsk() (gas: 147698) +RadixMatchingEngineHookGasTest:testGas_LargeRandomBookPartialFillsBid() (gas: 133771) +RadixMatchingEngineHookGasTest:testGas_LargeRandomBookRestsAsk() (gas: 193677) +RadixMatchingEngineHookGasTest:testGas_LargeRandomBookRestsBid() (gas: 198637) +RadixMatchingEngineHookGasTest:testGas_PathologicalOffSpinePartialFillBidComb() (gas: 670693) \ No newline at end of file diff --git a/README.md b/README.md index 1b01259..dcef3a3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Radix Matching Foundry prototype for a two-sided native-ETH/ERC20 matching engine backed by a single -`mapping(bytes32 => Branch)` radix tree store. +`mapping(bytes32 => Branch)` radix tree store addressed by stable node nonce. ## Order Node Layout @@ -29,28 +29,37 @@ routers must choose ticks that incorporate the decimal relationship of each toke `FillParams` contains sorted `token0` and `token1` addresses, the book epoch, a packed incoming order, the bid/ask side, and `noRest` / `fillOrKill` controls. Incoming orders must leave nonce and correction -bits empty. Any permitted unmatched remainder receives the next decrementing nonce and rests in the +bits empty. Any permitted unmatched remainder receives the next decrementing order nonce and rests in the appropriate active book. ## Branch Encoding -The node layout remains one `bytes32`. Branch nodes do not use nonce tags, probes, or a separate namespace. +The packed node layout remains one `bytes32`, stored in its parent pointer. The low 32-bit nonce is +the node's unique, stable identity: a branch's child pointers are stored at +`tree[bytes32(branchNonce)]`, not at a key derived from the full mutable packed word. Price, +quantity, correction, and nonce all remain in the parent pointer. A partial fill can therefore +rewrite the branch summary in its parent while its own two children stay in the same mapping slot. -A branch node is addressed with an actual order boundary key from its children: +Order and branch identities share one collision-free 32-bit namespace with allocation fronts that +move toward one another: ```text -path = price || nonce -branch_path = max(child_a_path, child_b_path) +order_nonce = uint32.max, uint32.max - 1, ... +branch_nonce = 1, 2, ... ``` -The branch path is packed back into the same tick and nonce fields, while the quantity field stores -the exact sum of the child quantities. A uniform-tick branch stores `roundingCorrection + 1` in its +Each insertion into a nonempty side allocates exactly one new branch identity. The book rotates +before the two allocation fronts meet, so branch identities cannot alias order leaves. Radix +routing derives keys from descendant order leaves rather than treating a branch identity as part of +the path. The quantity field stores the exact sum of the child quantities. A uniform-tick branch +stores `roundingCorrection + 1` in its correction field, allowing aggregate matching to reproduce the exact sum of per-order rounded notionals. Mixed-tick branches use correction code zero and recurse when their quote value is needed. -Because the boundary key is an actual order path, uniqueness comes from the decrementing order nonce -rather than from a synthetic branch nonce namespace. +The nonce is a unique node identity and addresses that node's stored child-pointer pair. -Resting orders start at `type(uint32).max` and decrement from there. +Resting order nonces start at `type(uint32).max` and decrement by one. Branch nonces start at one and +increment by one. A book epoch remains restable only while the next order identity is above the next +branch identity. Higher order nonce still means earlier time priority at the same price. @@ -106,9 +115,9 @@ forge test --force --match-contract '.*RadixMatchingEngineInvariantTest.*' --mat FOUNDRY_INVARIANT_RUNS=2048 FOUNDRY_INVARIANT_DEPTH=64 forge test --force --match-contract '.*RadixMatchingEngineInvariantTest.*' --match-test 'invariant_.*' INVARIANT_RUNS=2048 INVARIANT_DEPTH=64 INVARIANT_SHARDS=8 INVARIANT_SHARD=1 make invariant-deep-shard INVARIANT_RUNS=2048 INVARIANT_DEPTH=64 INVARIANT_SHARDS=8 make invariant-deep-shards -forge test --isolate --force --match-contract 'RadixMatchingEngine(Gas|HookGas|FeeGas)Test' --gas-report -forge snapshot --isolate --force --match-contract 'RadixMatchingEngine(Gas|HookGas|FeeGas)Test' --snap .gas-snapshot.runtime -forge snapshot --isolate --force --match-contract 'RadixMatchingEngine(Gas|HookGas|FeeGas)Test' --check .gas-snapshot.runtime +forge test --isolate --force --match-contract '(RadixMatchingEngine(Gas|HookGas|FeeGas)Test|DeepstateV1IntegratorFeeGasTest)' --gas-report +forge snapshot --isolate --force --match-contract '(RadixMatchingEngine(Gas|HookGas|FeeGas)Test|DeepstateV1IntegratorFeeGasTest)' --snap .gas-snapshot.runtime +forge snapshot --isolate --force --match-contract '(RadixMatchingEngine(Gas|HookGas|FeeGas)Test|DeepstateV1IntegratorFeeGasTest)' --check .gas-snapshot.runtime forge build --sizes python3 script/check_tick_math.py --check test/TickMath32.t.sol uv run --locked --only-group static slither src/DeepstateV1.sol --config-file slither.config.json --exclude-informational diff --git a/coverage.exclusions.json b/coverage.exclusions.json index c7953ea..364ea5e 100644 --- a/coverage.exclusions.json +++ b/coverage.exclusions.json @@ -2,159 +2,157 @@ "src/DeepstateV1.sol": { "reason": "Foundry --ir-minimum cannot anchor executed inline assembly, assembly-only helper calls, optimized helper returns, storage-reference assignments, packed fee loads, identifier hashing, or transient settlement operations. Reentrancy, divergent-cancel, match-buffer growth, full-width quote, rounding, routing, fee, settlement, and invalid-token tests independently execute every excluded operation.", "lines": [ - 240, - 241, - 242, - 244, - 249, - 282, - 1180, - 1427, - 1449, - 1450, - 1505, - 1506, - 1507, - 1508, - 1516, - 1558, - 1565, - 1584, - 1585, - 1586, - 1587, - 1588, - 1599, - 1600, - 1601, - 1603, - 1604, - 1605, - 1606, - 1607, - 1609, - 1611, - 1612, - 1613, - 1616, - 1617, - 1631, - 1632, - 1634, - 1635, - 1636, - 1657, - 1658, - 1668, - 1669, - 1670, - 1671, - 1699, - 1733, - 1734, - 1745, - 1746, - 1757, - 1758, - 1785, + 287, + 288, + 289, + 291, + 296, + 343, + 1317, + 1637, + 1692, + 1693, + 1694, + 1695, + 1704, + 1721, + 1748, + 1794, + 1801, + 1820, + 1821, 1822, 1823, 1824, - 1825, + 1835, + 1836, + 1837, + 1839, + 1840, + 1841, + 1842, + 1843, + 1845, + 1847, + 1848, + 1849, + 1852, + 1853, + 1867, + 1868, + 1870, + 1871, + 1872, + 1893, 1894, - 1895, - 1896, - 1897, - 1898, - 1901, - 1902, - 1903, 1904, 1905, 1906, 1907, - 1908, - 1910, - 1911, - 1913, - 1914, - 1934, 1935, - 1938, - 1939, - 1940, - 1942, - 1943, - 1945, - 1946, - 1959, - 1960, - 1973, - 1974, - 1975, - 1976, - 1977, - 1993, - 2033, - 2057, + 1969, + 1970, + 1981, + 1982, + 2019, + 2064, + 2065, + 2066, 2067, - 2306, - 2316, - 2379, - 2383, - 2421, - 2424, - 2465, - 2466, - 2467, - 2468, - 2481, - 2482, - 2483, - 2484, - 2485, - 2486, - 2660, - 2759, - 2760, - 2940, - 2941, - 2942, - 2969, - 2973, - 2975, - 3000, - 3004, - 3006, - 3026, - 3030, - 3032, - 3070, - 3079, - 3084, - 3100, - 3105, - 3153, - 3154, - 3172, - 3173, - 3227, - 3235, - 3300 + 2136, + 2137, + 2138, + 2139, + 2140, + 2143, + 2144, + 2145, + 2146, + 2147, + 2148, + 2149, + 2150, + 2152, + 2153, + 2155, + 2156, + 2176, + 2177, + 2180, + 2181, + 2182, + 2184, + 2185, + 2187, + 2188, + 2201, + 2202, + 2215, + 2216, + 2217, + 2218, + 2219, + 2235, + 2275, + 2299, + 2309, + 2548, + 2558, + 2621, + 2625, + 2663, + 2666, + 2708, + 2709, + 2710, + 2711, + 2724, + 2725, + 2726, + 2727, + 2728, + 2729, + 2905, + 3015, + 3016, + 3197, + 3198, + 3199, + 3226, + 3230, + 3232, + 3257, + 3261, + 3263, + 3283, + 3287, + 3289, + 3327, + 3336, + 3341, + 3357, + 3362, + 3410, + 3411, + 3429, + 3430, + 3484, + 3492, + 3557 ], - "statements": 145, + "statements": 142, "functions": [], "branches": [ - "240:0:0", - "1603:129:0", - "1896:145:0", - "1907:146:0", - "1910:147:0", - "1913:148:0", - "1938:149:0", - "1945:150:0", - "1975:151:0", - "2940:198:0" + "287:2:0", + "1839:143:0", + "2138:160:0", + "2149:161:0", + "2152:162:0", + "2155:163:0", + "2180:164:0", + "2187:165:0", + "2217:166:0", + "3197:213:0" ] }, "src/libraries/TickMath32.sol": { diff --git a/docs/INDUCTIVE_PROOFS.md b/docs/INDUCTIVE_PROOFS.md index e4eb9e7..f7114f5 100644 --- a/docs/INDUCTIVE_PROOFS.md +++ b/docs/INDUCTIVE_PROOFS.md @@ -23,10 +23,15 @@ For a side `s` and leaf `l`, define: - `N_s(t, q)`, the exact integer notional represented by production tick `t`, rounded down for an ask and up for a bid. -For a subtree `T`, let `Leaves(T)` be its reachable leaves, `maxP(T)` its maximum raw path, and -`sumQ(T)` the sum of its live quantities. A clean branch address is the packed word +For a subtree `T`, let `Leaves(T)` be its reachable leaves, `rep(T)` a descendant leaf used only to +recover a radix routing key, `sumQ(T)` the sum of its live quantities, and `id(T)` the unique stable +branch nonce allocated when that internal node is created. A clean branch summary is the packed word -`A(T) = pack(maxP(T), sumQ(T), correction(T))`. +`A(T) = pack(price(rep(T)), sumQ(T), correction(T), id(T))`. + +The branch's identity is independent of the descendant key used for routing; +`tree[bytes32(id(T))]` stores its two children. Repacking price, quantity, or correction therefore +does not move those children. The correction field is zero for a mixed-tick branch. For a uniform branch it is one plus the difference between the aggregate-rounded notional and the sum of leaf-rounded notionals, with the @@ -40,7 +45,7 @@ sign convention stated in the contract. 2. every branch has exactly two nonzero children; 3. the children diverge at the branch's Patricia split, every left key is smaller than every right key, and every descendant shares the required preceding prefix; -4. the branch quantity is `sumQ(T)` and its raw boundary is `maxP(T)`; +4. the branch quantity is `sumQ(T)` and its identity is the stable `id(T)` allocated at creation; 5. a nonzero correction denotes a uniform-tick subtree and reconstructs exactly `sum(N_s(t, Q(l)))`; zero denotes a leaf or mixed branch; 6. all reachable node addresses are distinct and nonzero; and @@ -127,7 +132,8 @@ once. Therefore: Assuming child corrections are exact, adding this local zero/one term yields the exact parent correction. If child subtrees contain `m` and `n` leaves, the result is at most -`(m - 1) + (n - 1) + 1 = m + n - 1`. One book can assign at most `2^32 - 2` leaves, so the stored +`(m - 1) + (n - 1) + 1 = m + n - 1`. One book can contain at most `2^31` leaves before its opposing +identity-allocation fronts meet, so the stored `correction + 1` fits in `uint32`. This is structural induction on uniform-subtree height. ## 3. Radix Preservation @@ -135,8 +141,8 @@ correction. If child subtrees contain `m` and `n` leaves, the result is at most ### 3.1 Empty and leaf bases An empty root satisfies every tree clause vacuously. A newly packed leaf has positive validated -quantity, zero caller-supplied correction and nonce bits, and a contract-assigned nonce in -`[2, 2^32 - 1]`; it therefore satisfies `C`. +quantity, zero caller-supplied correction and nonce bits, and a contract-assigned nonce strictly +above the ascending branch-allocation front; it therefore satisfies `C`. ### 3.2 Insertion @@ -148,7 +154,11 @@ Assume `C(T)` after any required dirty-spine materialization. 3. **Divergence above the root split:** the new parent partitions the new key from every existing descendant because all old descendants share the old prefix. 4. **Divergence below the root split:** recursion enters exactly one child. By induction that child - remains clean; rebuilding the parent restores exact quantity, boundary and correction. + remains clean; rebuilding the parent restores exact quantity and correction while preserving the + existing parent's identity. + +Every nonempty insertion adds exactly one internal node and consumes exactly one fresh branch +identity. Existing internal nodes retain their identities on the recursive unwind. The depth increases before every recursive call and is at most 64. The SMT split obligations cover every possible depth. @@ -163,31 +173,26 @@ the target leaf, removal returns empty. On unwind: - two children are repacked from their exact summaries. For an off-spine path this proves `C` directly. On the global right path, the optimization may retain -the old branch address and update only its right pointer, producing `D` as proved in Section 4. +the old packed branch summary and update only its right pointer, producing `D` as proved in Section 4. ### 3.4 Live-node uniqueness -Leaf raw paths are unique because nonce assignment is global to the book. For two disjoint -subtrees, their maximum raw paths are therefore different, so their branch words differ in the path -fields. If two live subtrees share their maximum leaf, rooted-tree structure makes them nested. The -ancestor contains at least one additional positive sibling, so its checked aggregate quantity is -strictly greater than the descendant quantity. The same argument separates a branch from its -maximum leaf. Bid and ask subtrees are disjoint but use the same globally unique nonce sequence, so -the disjoint-subtree argument also applies across sides. Positive quantity separates every live node -from root zero. - -These cases exhaust leaf/branch and same-side/cross-side pairs. The exact packing-field injectivity -and strict-sum steps are SMT obligations. +Order identities descend from `2^32 - 1`; branch identities ascend from one. Allocation requires +the current order identity to be strictly above the current branch identity, and rotation disables +further rests before the fronts meet. Thus every allocated order and branch identity is nonzero, +globally unique within the book, and disjoint from the other class. Since the low 32 bits of every +live node contain that identity, distinct live nodes have distinct packed words and distinct branch +mapping keys. The fixed root key zero cannot alias either class. ## 4. Dirty Right-Spine Preservation Assume `D(T)` and modify only the rightmost path. 1. The left child is unchanged and remains clean. -2. A fill or removal only decreases descendant quantities. A retained anchor's historical quantity - was strictly greater than every old proper descendant and is therefore still strictly greater - than every new descendant. It cannot alias one. Production additionally falls back to a rebuilt - branch if a replacement child equals the anchor or its sibling. +2. A retained anchor keeps its unique branch nonce. Order and branch allocation fronts are + disjoint, and every other live branch has a different allocated nonce, so neither a descendant + rewrite nor a quantity change can alias the anchor. Production still rejects a replacement that + would create a self-cycle or duplicate child under corrupted state. 3. Updating the right pointer preserves reachability and key order because the replacement is the result of recursively modifying the old right subtree. 4. Dirty aggregate words are never trusted for mixed-price consumption. `_dirtyRightSpineData` @@ -236,9 +241,12 @@ guard is held. A failed transfer reverts both deletion and tree mutation atomica ## 7. Nonces And Epochs -An initialized book starts at nonce `2^32 - 1`. Rest assigns the current nonce and stores one less; -therefore assignments are exactly `2^32 - 1, ..., 2`, with no repetition. Assigning two leaves one, -which is the non-restable sentinel, and rotates the active pool epoch. +An initialized book starts with next order nonce `2^32 - 1` and next branch nonce one. Every rest +assigns the current order nonce and decrements that front by one. A rest into a nonempty side also +assigns the current branch nonce to the one new internal node and increments that front by one. +Before each allocation the order front must be strictly greater than the branch front. If the next +fronts would meet or cross, the stored order nonce becomes the non-restable sentinel one and the +active pool epoch rotates. Therefore neither sequence repeats and the two sequences never alias. For `epoch < 2^254 - 1`, increment is exact and cannot overlap either hook bit. At the terminal epoch, the explicit `EpochExhausted` guard reverts the entire attempted rest, so no wrapped epoch, owner, diff --git a/docs/PROOF_OBLIGATIONS.md b/docs/PROOF_OBLIGATIONS.md index f6887a3..fc56c72 100644 --- a/docs/PROOF_OBLIGATIONS.md +++ b/docs/PROOF_OBLIGATIONS.md @@ -70,7 +70,8 @@ Consequently, fill partitioning cannot create or destroy quote units. For `n` same-tick leaves, the encoded correction reconstructs the exact sum of independently rounded leaf notionals from the rounded aggregate notional. Its magnitude is at most `n - 1`; since a book -admits at most `2^32 - 2` leaves, `correction + 1` fits in `uint32`. +admits at most `2^31` leaves before its order- and branch-identity allocation fronts meet, +`correction + 1` fits in `uint32`. **Evidence:** `SMT` binary-merge and inductive-capacity lemmas, plus `BYTECODE` correction proofs. @@ -136,14 +137,13 @@ right key. Recursive insertion and removal advance the split depth, so a key wal ### R3. Live-node address uniqueness -Assume every live leaf has a unique nonce and every branch quantity sum is representable. Two -disjoint branches have different maximum descendant path keys. An ancestor and descendant that -share a maximum path have strictly different positive aggregate quantities. A branch and one of its -live leaves likewise differ in quantity. Thus no two simultaneously live tree nodes share a -`bytes32` mapping key. Original order ownership keys may equal historical branch words without -aliasing because ownership is stored in `orderOf`, not `tree`. +Order identities descend from `2^32 - 1`, while branch identities ascend from one. Every nonempty +insertion consumes one fresh branch identity, and the book becomes non-restable before the fronts +meet. Branches therefore cannot alias leaves, and two live branches cannot alias each other. +Quantity and correction rewrites preserve the branch identity and continue addressing the same +`tree[bytes32(branchNonce)]` slot; only topology creation consumes a new branch identity. -**Evidence:** `SMT` local strict-sum lemmas and `INDUCTION` over the tree. Keccak-scoped ownership is +**Evidence:** `SMT` allocation-front and radix-split lemmas and `INDUCTION` over the tree. Keccak-scoped ownership is covered separately by N1. ### R4. Aggregate quantity and quote correctness @@ -188,9 +188,11 @@ domains is mathematically impossible. ### N2. Nonce uniqueness and exhaustion -An initialized book assigns strictly decreasing nonces from `2^32 - 1` through `2`, never assigns -zero or one, and rotates immediately after assigning nonce two. A nonce-one book stays matchable and -cancelable but cannot rest an unmatched taker remainder. For every epoch below `2^254 - 1`, rotation +An initialized book assigns strictly decreasing order nonces from `2^32 - 1` and strictly increasing +branch nonces from one. A rest into a nonempty side allocates one identity from each front; a rest +into an empty side allocates only an order identity. The fronts never alias, and the book rotates as +soon as their next values would meet or cross. A nonce-one book stays matchable and cancelable but +cannot rest an unmatched taker remainder. For every epoch below `2^254 - 1`, rotation increments the epoch without altering either packed hook bit. At `2^254 - 1`, rotation reverts with `EpochExhausted`; EVM rollback leaves the book, owner, collateral, nonce, roots, and pool state unchanged. diff --git a/script/prove_protocol.py b/script/prove_protocol.py index 1da2c82..7cc45e8 100644 --- a/script/prove_protocol.py +++ b/script/prove_protocol.py @@ -76,6 +76,7 @@ def bind_model_to_source(): "uint256 private constant _QUANTITY_SHIFT = 64;", "uint256 private constant _CORRECTION_SHIFT = 32;", "uint256 private constant _NONCE_MASK = type(uint32).max;", + "uint256 private constant _BRANCH_NONCE_SHIFT = 64;", "uint256 private constant _POOL_EPOCH_MASK = (uint256(1) << 254) - 1;", "uint256 private constant _FEE_DELTA_DOMAIN = uint256(1) << 255;", "uint256 private constant _INTEGRATOR_FEE_DELTA_DOMAIN = uint256(1) << 254;", @@ -87,7 +88,7 @@ def bind_model_to_source(): "key := or(shl(32, sub(0xffffffff, tickKey)), and(order, 0xffffffff))", "prefixLength = uint8(LibBit.clz(differingBits << 192));", "one := and(shr(sub(63, depth), key), 1)", - "nextNonceAfter = nonce - 1;", + "nextOrderNonce = orderNonce - 1;", "if (oldEpoch == _POOL_EPOCH_MASK) revert EpochExhausted();", "epoch = oldEpoch + 1;", "return (poolState & _POOL_HOOK_ACTIVE_MASK) | epoch;", @@ -478,41 +479,38 @@ def prove_keys_and_radix(p): *assumptions, ) - child_quantity = Int("child_quantity") - sibling_quantity = Int("sibling_quantity") - p.prove( - "R3.ancestor-quantity-distinguishes-address", - child_quantity + sibling_quantity > child_quantity, - child_quantity >= 1, - sibling_quantity >= 1, - child_quantity + sibling_quantity <= UINT160_MAX, - ) - boundary_a = Int("branch_boundary_a") - boundary_b = Int("branch_boundary_b") + price_a = Int("branch_price_a") + price_b = Int("branch_price_b") + identity_a = Int("branch_identity_a") + identity_b = Int("branch_identity_b") quantity_a = Int("branch_quantity_a") quantity_b = Int("branch_quantity_b") correction_a = Int("branch_correction_a") correction_b = Int("branch_correction_b") packed_a = ( - (boundary_a / (1 << 32)) * (1 << 224) + price_a * (1 << 224) + quantity_a * (1 << 64) + correction_a * (1 << 32) - + boundary_a % (1 << 32) + + identity_a ) packed_b = ( - (boundary_b / (1 << 32)) * (1 << 224) + price_b * (1 << 224) + quantity_b * (1 << 64) + correction_b * (1 << 32) - + boundary_b % (1 << 32) + + identity_b ) p.prove( - "R3.disjoint-boundaries-distinguish-address", + "R3.distinct-identities-distinguish-packed-branches", packed_a != packed_b, - boundary_a >= 0, - boundary_a < (1 << 64), - boundary_b >= 0, - boundary_b < (1 << 64), - boundary_a != boundary_b, + price_a >= 0, + price_a <= UINT32_MAX, + price_b >= 0, + price_b <= UINT32_MAX, + identity_a >= 1, + identity_a <= UINT32_MAX, + identity_b >= 1, + identity_b <= UINT32_MAX, + identity_a != identity_b, quantity_a >= 1, quantity_a <= UINT160_MAX, quantity_b >= 1, @@ -523,6 +521,27 @@ def prove_keys_and_radix(p): correction_b <= UINT32_MAX, ) + branch_index_a = Int("branch_index_a") + branch_index_b = Int("branch_index_b") + p.prove( + "R3.ascending-branch-identities-are-unique", + 1 + branch_index_a != 1 + branch_index_b, + branch_index_a >= 0, + branch_index_b >= 0, + branch_index_a < UINT32_MAX, + branch_index_b < UINT32_MAX, + branch_index_a != branch_index_b, + ) + order_index = Int("order_index") + branch_index = Int("branch_index") + p.prove( + "R3.allocation-fronts-keep-branch-and-order-identities-disjoint", + 1 + branch_index != UINT32_MAX - order_index, + branch_index >= 0, + order_index >= 0, + 1 + branch_index < UINT32_MAX - order_index, + ) + old_aggregate = Int("dirty_old_aggregate") new_descendant = Int("dirty_new_descendant") removed_quantity = Int("dirty_removed_quantity") @@ -555,7 +574,26 @@ def prove_nonce_epoch_and_namespaces(p): j <= UINT32_MAX - 2, i != j, ) - p.prove("N2.exhaustion-after-two", 2 - 1 == 1) + p.prove( + "N2.assigned-branch-identity-unique", + 1 + i != 1 + j, + i >= 0, + j >= 0, + i <= UINT32_MAX - 2, + j <= UINT32_MAX - 2, + i != j, + ) + order_after = Int("next_order_after_rest") + branch_after = Int("next_branch_after_rest") + p.prove( + "N2.crossed-fronts-produce-exhausted-sentinel", + If(order_after <= branch_after, 1, order_after) == 1, + order_after >= 1, + order_after <= UINT32_MAX, + branch_after >= 1, + branch_after <= UINT32_MAX, + order_after <= branch_after, + ) address_a = BitVec("address_a", 160) address_b = BitVec("address_b", 160) diff --git a/src/DeepstateV1.sol b/src/DeepstateV1.sol index fed699c..134e171 100644 --- a/src/DeepstateV1.sol +++ b/src/DeepstateV1.sol @@ -15,7 +15,7 @@ import {TickMath32} from "./libraries/TickMath32.sol"; /// - bits 224-255: signed 32-bit logarithmic tick. /// - bits 64-223: 160-bit quantity. /// - bits 32-63: 32-bit same-tick branch correction code; zero for leaves/mixed branches. -/// - bits 0-31: 32-bit nonce/path suffix. +/// - bits 0-31: 32-bit node identity (order nonce for leaves; serial identity for branches). /// /// Tick `t` represents the dimensionless quote/base price `2 ** (96 * t / 2**31)`. /// Tick zero is therefore exactly 1:1, the full signed domain spans approximately @@ -33,12 +33,14 @@ import {TickMath32} from "./libraries/TickMath32.sol"; /// zero; the contract assigns a decrementing nonce so higher nonce values have earlier time /// priority at the same price. /// -/// Branch nodes are self-addressing aggregate nodes. A branch address is built with the maximum -/// raw price/nonce path key of its two children and the sum of their quantities. The child pointers -/// are stored in `tree[branch]`. A node is treated as a branch if it has a nonzero left child in -/// `tree`; otherwise it is treated as a leaf. This intentionally allows a branch key and an -/// original order key to be the same `bytes32`: branch structure lives in `tree`, while ownership -/// and side metadata live together in `orderOf`. +/// Branch nodes are aggregate nodes whose packed word lives in their parent pointer. Their child +/// pointers are stored in `tree[bytes32(nonce)]`, using the packed node's unique nonce as a stable +/// identity. Quantity/correction changes therefore rewrite only the parent pointer and never move +/// the node's children to a new mapping key. Same-price branches cache their nonce-side split depth +/// in redundant price-prefix bits of the stored left child; child readers restore the exact packed +/// node before returning it. This keeps both order and branch identities fully 32-bit. A node +/// is a branch when that nonce-addressed slot has a nonzero left child; otherwise it is a leaf. +/// Ownership and side metadata live in `orderOf`. /// /// The bid and ask books are conceptual trees that coexist in the same `tree` mapping: /// @@ -54,11 +56,11 @@ import {TickMath32} from "./libraries/TickMath32.sol"; /// decouples matching from per-maker execution while preserving price-time priority. /// /// Right-spine optimization: fills and cancels near the best price often mutate only the rightmost -/// path. In those cases the engine may keep a stable branch address and update only its right child -/// pointer. The branch word can then have a stale aggregate quantity/path, so the corresponding -/// book flag is set. Same-side insertion materializes the right spine back into exact aggregate -/// nodes before adding a new leaf. Matching can still safely consume same-price dirty subtrees by -/// recomputing their live quantity from child pointers. +/// path. Nonce-addressed child slots remain stable while the packed node is rewritten in its parent. +/// The branch word can then have a stale aggregate quantity/path, so the corresponding book flag is +/// set. Same-side insertion materializes the right spine back into exact aggregate nodes before +/// adding a new leaf. Matching can still safely consume same-price dirty subtrees by recomputing +/// their live quantity from child pointers. /// /// Pools use sorted token addresses, and each pool epoch derives an isolated book id. `fillRoute` /// mutates every selected book before settling one net transient delta per touched token. Optional @@ -71,9 +73,10 @@ import {TickMath32} from "./libraries/TickMath32.sol"; contract DeepstateV1 is Ownable { /// @notice Stored child pointers for a branch node. /// @dev - /// `leftNode` and `rightNode` are both `bytes32` nodes. They can be leaves or further branches. - /// Branches always have two children. A zero `leftNode` is the branch/leaf sentinel used by all - /// walkers, so live branches must never be stored with only one child. + /// `leftNode` and `rightNode` logically contain `bytes32` nodes. Same-price branches borrow the + /// stored left node's redundant top five tick bits for cached depth; `_leftNode` and the public + /// `tree` getter restore the exact child. Branches always have two children. A zero `leftNode` + /// is the branch/leaf sentinel, so live branches must never be stored with only one child. struct Branch { /// @notice Child whose sort key has a zero bit at the branch split depth. bytes32 leftNode; @@ -91,9 +94,10 @@ contract DeepstateV1 is Ownable { } /// @notice One isolated radix book for one token pair epoch. - /// @dev The low 32 bits of `nonceAndFlags` are the decrementing nonce. Bits above the nonce - /// hold per-book right-spine dirty flags. The zero node in `tree` is reserved as the root - /// anchor: `leftNode` is the ask root and `rightNode` is the bid root. + /// @dev The low 32 bits of `nonceAndFlags` are the decrementing order nonce, bits 32-33 are + /// per-book right-spine dirty flags, and bits 64-95 are the ascending branch serial. The zero + /// node in `tree` is reserved as the root anchor: `leftNode` is the ask root and `rightNode` is + /// the bid root. struct Book { uint256 nonceAndFlags; mapping(bytes32 => Branch) tree; @@ -118,10 +122,53 @@ contract DeepstateV1 is Ownable { uint256 private constant _QUANTITY_MASK = (uint256(1) << 160) - 1; /// @dev Mask for extracting the 32-bit same-tick correction code. uint256 private constant _CORRECTION_MASK = type(uint32).max; - /// @dev Mask for extracting or validating the 32-bit nonce/path suffix. + /// @dev Mask for extracting or validating a 32-bit node nonce. uint256 private constant _NONCE_MASK = type(uint32).max; + /// @dev Bit offset of the ascending branch-serial counter in `Book.nonceAndFlags`. + uint256 private constant _BRANCH_NONCE_SHIFT = 64; + /// @dev Mask for replacing the ascending branch-serial counter. + uint256 private constant _BRANCH_NONCE_MASK = uint256(type(uint32).max) << _BRANCH_NONCE_SHIFT; + /// @dev Same-price splits are at depths 32..63. Their zero-based nonce depth is cached in the + /// top five price bits of the left child's storage encoding. Those bits are redundant because + /// both children have the exact tick already stored in the parent branch summary. + uint256 private constant _CACHED_DEPTH_SHIFT = 251; + uint256 private constant _CACHED_DEPTH_MASK = uint256(0x1f) << _CACHED_DEPTH_SHIFT; /// @dev Root anchor in every book's tree. `leftNode` is ask root; `rightNode` is bid root. bytes32 private constant _ROOT_NODE = bytes32(0); + + /// @dev Return the storage key for a packed node's stable nonce identity. + function _branchKey(bytes32 node) private pure returns (bytes32) { + return bytes32(uint256(_nonce(node))); + } + + /// @dev Store a newly allocated branch. Same-price branches cache `depth - 32` in redundant + /// high price bits of the stored left child without changing the child node exposed to callers. + function _setNewBranchChildren(Book storage book, bytes32 node, bytes32 leftNode, bytes32 rightNode, uint8 depth) + private + { + Branch storage branch = book.tree[_branchKey(node)]; + branch.leftNode = _encodeStoredLeftNode(node, leftNode, depth); + branch.rightNode = rightNode; + } + + /// @dev Encode only same-price branch children. Mixed-price depths are recoverable from the + /// child ticks and therefore require no persistent metadata. + function _encodeStoredLeftNode(bytes32 branchNode, bytes32 leftNode, uint8 depth) + private + pure + returns (bytes32 storedLeftNode) + { + if (_correctionCode(branchNode) == 0) return leftNode; + uint256 depthCode = uint256(depth - 32) << _CACHED_DEPTH_SHIFT; + storedLeftNode = bytes32((uint256(leftNode) & ~_CACHED_DEPTH_MASK) | depthCode); + } + + /// @dev Restore price bits borrowed by a same-price branch's cached-depth encoding. + function _decodeStoredLeftNode(bytes32 branchNode, bytes32 storedLeftNode) private pure returns (bytes32 leftNode) { + if (_correctionCode(branchNode) == 0 || storedLeftNode == bytes32(0)) return storedLeftNode; + leftNode = bytes32((uint256(storedLeftNode) & ~_CACHED_DEPTH_MASK) | (uint256(branchNode) & _CACHED_DEPTH_MASK)); + } + /// @dev Dirty bit stored above the 32-bit `nextNonce` field when bid right-spine anchors are stale. uint256 private constant _BID_RIGHT_SPINE_DIRTY = uint256(1) << 32; /// @dev Dirty bit stored above the 32-bit `nextNonce` field when ask right-spine anchors are stale. @@ -273,19 +320,41 @@ contract DeepstateV1 is Ownable { /// @return remaining Incoming base quantity left unmatched. /// @return baseFilled Base quantity matched. /// @return quoteAmount Quote value matched. - function _matchBook(bytes32 id, Book storage book, bytes32 order, bool isBid, bool hookEnabled) + function _matchBook( + bytes32 id, + Book storage book, + bytes32 order, + bool isBid, + bool hookEnabled, + uint256 nonceAndFlags + ) internal - returns (int32 limitPrice, uint160 remaining, uint160 baseFilled, uint256 quoteAmount) + returns ( + int32 limitPrice, + uint160 remaining, + uint160 baseFilled, + uint256 quoteAmount, + uint256 updatedNonceAndFlags + ) { (limitPrice, remaining) = _validateIncomingOrder(order); + updatedNonceAndFlags = nonceAndFlags; _beginMatchBuffer(); + bool markDirty; if (isBid) { - (remaining, baseFilled, quoteAmount) = _matchIncomingBid(id, book, limitPrice, remaining, hookEnabled); + bool dirty = _rightSpineDirty(nonceAndFlags, false); + (remaining, baseFilled, quoteAmount, markDirty) = + _matchIncomingBid(id, book, limitPrice, remaining, hookEnabled, dirty); + if (markDirty && !dirty) updatedNonceAndFlags |= _ASK_RIGHT_SPINE_DIRTY; } else { - (remaining, baseFilled, quoteAmount) = _matchIncomingAsk(id, book, limitPrice, remaining, hookEnabled); + bool dirty = _rightSpineDirty(nonceAndFlags, true); + (remaining, baseFilled, quoteAmount, markDirty) = + _matchIncomingAsk(id, book, limitPrice, remaining, hookEnabled, dirty); + if (markDirty && !dirty) updatedNonceAndFlags |= _BID_RIGHT_SPINE_DIRTY; } + if (updatedNonceAndFlags != nonceAndFlags) book.nonceAndFlags = updatedNonceAndFlags; _emitBufferedMatches(id, !isBid); } @@ -300,14 +369,18 @@ contract DeepstateV1 is Ownable { /// @return baseFilled Base quantity bought. /// @return quoteAmount Quote paid at resting ask prices. /// @dev The ask root lives in `tree[0].leftNode`. - function _matchIncomingBid(bytes32 id, Book storage book, int32 limitPrice, uint160 remaining, bool hookEnabled) - private - returns (uint160 newRemaining, uint160 baseFilled, uint256 quoteAmount) - { + function _matchIncomingBid( + bytes32 id, + Book storage book, + int32 limitPrice, + uint160 remaining, + bool hookEnabled, + bool dirty + ) private returns (uint160 newRemaining, uint160 baseFilled, uint256 quoteAmount, bool markDirty) { bytes32 root = book.tree[_ROOT_NODE].leftNode; - if (root == bytes32(0)) return (remaining, 0, 0); + if (root == bytes32(0)) return (remaining, 0, 0, false); - if (!_rightSpineDirty(book, false) && limitPrice == type(int32).max && _quantity(root) <= remaining) { + if (!dirty && limitPrice == type(int32).max && _quantity(root) <= remaining) { baseFilled = _quantity(root); if (hookEnabled) _recordTopOrderChange(_rightmostLeaf(book, root), 0); quoteAmount = _consumeSubtree(id, book, root, false); @@ -315,12 +388,12 @@ contract DeepstateV1 is Ownable { unchecked { newRemaining = remaining - baseFilled; } - return (newRemaining, baseFilled, quoteAmount); + return (newRemaining, baseFilled, quoteAmount, false); } bytes32 newRoot; bytes32 matchChange; - uint256 matchFlags = _rightSpineDirty(book, false) ? _MATCH_DIRTY : 0; + uint256 matchFlags = dirty ? _MATCH_DIRTY : 0; if (hookEnabled) matchFlags |= _MATCH_HOOK; (newRoot, baseFilled, quoteAmount, matchChange) = _matchAskRightSpine(id, book, root, limitPrice, remaining, matchFlags); @@ -328,7 +401,7 @@ contract DeepstateV1 is Ownable { newRemaining = remaining - baseFilled; } if (newRoot != root) book.tree[_ROOT_NODE].leftNode = newRoot; - if (_matchChangeDirty(matchChange) && newRoot != bytes32(0)) _setRightSpineDirty(book, false); + markDirty = _matchChangeDirty(matchChange) && newRoot != bytes32(0); } /// @notice Match an incoming ask against the bid root. @@ -341,14 +414,18 @@ contract DeepstateV1 is Ownable { /// @return baseFilled Base quantity sold. /// @return quoteAmount Quote received at resting bid prices. /// @dev The bid root lives in `tree[0].rightNode`. - function _matchIncomingAsk(bytes32 id, Book storage book, int32 limitPrice, uint160 remaining, bool hookEnabled) - private - returns (uint160 newRemaining, uint160 baseFilled, uint256 quoteAmount) - { + function _matchIncomingAsk( + bytes32 id, + Book storage book, + int32 limitPrice, + uint160 remaining, + bool hookEnabled, + bool dirty + ) private returns (uint160 newRemaining, uint160 baseFilled, uint256 quoteAmount, bool markDirty) { bytes32 root = book.tree[_ROOT_NODE].rightNode; - if (root == bytes32(0)) return (remaining, 0, 0); + if (root == bytes32(0)) return (remaining, 0, 0, false); - if (!_rightSpineDirty(book, true) && limitPrice == type(int32).min && _quantity(root) <= remaining) { + if (!dirty && limitPrice == type(int32).min && _quantity(root) <= remaining) { baseFilled = _quantity(root); if (hookEnabled) _recordTopOrderChange(_rightmostLeaf(book, root), 0); quoteAmount = _consumeSubtree(id, book, root, true); @@ -356,12 +433,12 @@ contract DeepstateV1 is Ownable { unchecked { newRemaining = remaining - baseFilled; } - return (newRemaining, baseFilled, quoteAmount); + return (newRemaining, baseFilled, quoteAmount, false); } bytes32 newRoot; bytes32 matchChange; - uint256 matchFlags = _rightSpineDirty(book, true) ? _MATCH_DIRTY : 0; + uint256 matchFlags = dirty ? _MATCH_DIRTY : 0; if (hookEnabled) matchFlags |= _MATCH_HOOK; (newRoot, baseFilled, quoteAmount, matchChange) = _matchBidRightSpine(id, book, root, limitPrice, remaining, matchFlags); @@ -369,7 +446,7 @@ contract DeepstateV1 is Ownable { newRemaining = remaining - baseFilled; } if (newRoot != root) book.tree[_ROOT_NODE].rightNode = newRoot; - if (_matchChangeDirty(matchChange) && newRoot != bytes32(0)) _setRightSpineDirty(book, true); + markDirty = _matchChangeDirty(matchChange) && newRoot != bytes32(0); } /// @notice Cancel an open order or claim a filled order. @@ -393,15 +470,19 @@ contract DeepstateV1 is Ownable { /// /// The order state is deleted before payout. If a token transfer reverts, the whole transaction /// reverts and the claim remains live. - function _cancelBook(bytes32 id, Book storage book, bytes32 order, address caller, uint256 hookFlags) - internal - returns (address owner, bool isBid, uint256 baseAmount, uint256 quoteAmount) - { + function _cancelBook( + bytes32 id, + Book storage book, + bytes32 order, + address caller, + uint256 hookFlags, + uint256 nonceAndFlags + ) internal returns (address owner, bool isBid, uint256 baseAmount, uint256 quoteAmount) { bytes32 orderKey = _orderId(id, order); OrderState storage state = orderOf[orderKey]; owner = state.owner; if (owner != caller) { - if (_nextNonce(book) == 0) revert InvalidBook(); + if (nonceAndFlags & _NONCE_MASK == 0) revert InvalidBook(); if (_quantity(order) == 0) revert InvalidOrder(); revert NotOrderOwner(); } @@ -411,7 +492,7 @@ contract DeepstateV1 is Ownable { isBid = state.isBid; bool hookEnabled = hookFlags & (isBid ? _CANCEL_HOOK_BID : _CANCEL_HOOK_ASK) != 0; - bytes32 removed = _removeOrderFromBook(book, order, isBid, hookEnabled); + bytes32 removed = _removeOrderFromBook(book, order, isBid, hookEnabled, nonceAndFlags); (baseAmount, quoteAmount) = _cancelAmounts(order, removed, isBid, originalQuantity); @@ -461,7 +542,7 @@ contract DeepstateV1 is Ownable { /// @param isBid True to remove from the bid tree, false from the ask tree. /// @param hookEnabled True to record a top-order change when the removed leaf was best. /// @return removed Live leaf removed from the tree, or zero if the order was already absent. - function _removeOrderFromBook(Book storage book, bytes32 order, bool isBid, bool hookEnabled) + function _removeOrderFromBook(Book storage book, bytes32 order, bool isBid, bool hookEnabled, uint256 nonceAndFlags) private returns (bytes32 removed) { @@ -475,7 +556,7 @@ contract DeepstateV1 is Ownable { if (removed != bytes32(0) && newRoot != root) { book.tree[_ROOT_NODE].rightNode = newRoot; } - if (dirtyChanged && newRoot != bytes32(0)) _setRightSpineDirty(book, true); + if (dirtyChanged && newRoot != bytes32(0)) _setRightSpineDirty(book, true, nonceAndFlags); if (hookEnabled && removedTop) { _recordTopOrderChange(removed, _replacementTopNonce(book, newRoot)); } @@ -490,7 +571,7 @@ contract DeepstateV1 is Ownable { if (removed != bytes32(0) && newRoot != root) { book.tree[_ROOT_NODE].leftNode = newRoot; } - if (dirtyChanged && newRoot != bytes32(0)) _setRightSpineDirty(book, false); + if (dirtyChanged && newRoot != bytes32(0)) _setRightSpineDirty(book, false, nonceAndFlags); if (hookEnabled && removedTop) { _recordTopOrderChange(removed, _replacementTopNonce(book, newRoot)); } @@ -537,38 +618,75 @@ contract DeepstateV1 is Ownable { nonceAndFlags &= ~dirtyFlag; } - // forge-lint: disable-next-line(unsafe-typecast) - uint32 nonce = uint32(nonceAndFlags & _NONCE_MASK); - if (nonce <= 1) revert NonceExhausted(); - unchecked { - nextNonceAfter = nonce - 1; - } - unchecked { - book.nonceAndFlags = (nonceAndFlags & ~_NONCE_MASK) | uint256(nextNonceAfter); + { + bytes32 root = isBid ? book.tree[_ROOT_NODE].rightNode : book.tree[_ROOT_NODE].leftNode; + uint32 nonce; + uint32 newBranchSerial; + (nonce, newBranchSerial, nextNonceAfter) = _allocateNodeNonces(book, nonceAndFlags, root != bytes32(0)); + + restingOrder = _pack(price, quantity, nonce); + _insertRestingOrder(book, root, restingOrder, isBid, hookEnabled, newBranchSerial); } - restingOrder = _pack(price, quantity, nonce); orderOf[_orderId(id, restingOrder)] = OrderState({owner: owner, isBid: isBid}); - _insertRestingOrder(book, restingOrder, isBid, hookEnabled); - emit OrderRested(id, restingOrder, owner, isBid); } + /// @dev Allocate one order identity and, for insertion into a nonempty side, one branch serial. + function _allocateNodeNonces(Book storage book, uint256 nonceAndFlags, bool createsBranch) + private + returns (uint32 orderNonce, uint32 newBranchSerial, uint32 nextOrderNonce) + { + // Order identities descend from uint32.max while full-width branch serials ascend from one. + // The book rotates before the two 32-bit identity fronts meet. + // forge-lint: disable-next-line(unsafe-typecast) + orderNonce = uint32(nonceAndFlags & _NONCE_MASK); + if (orderNonce <= 1) revert NonceExhausted(); + uint32 nextBranchSerial = _nextBranchSerial(nonceAndFlags); + // The next branch serial is unused; every smaller nonzero serial may already address a + // live branch on either side of the shared book. + if (orderNonce < nextBranchSerial) revert NonceExhausted(); + if (createsBranch) { + if (nextBranchSerial >= orderNonce) revert NonceExhausted(); + newBranchSerial = nextBranchSerial; + } + + unchecked { + nextOrderNonce = orderNonce - 1; + if (createsBranch) ++nextBranchSerial; + } + // `nextBranchSerial` names the next unused branch identity. If the next descending order + // identity would enter the allocated branch range, mark this epoch exhausted. + if (nextBranchSerial > 1 && nextOrderNonce < nextBranchSerial) nextOrderNonce = 1; + + uint256 updated = (nonceAndFlags & ~_NONCE_MASK) | uint256(nextOrderNonce); + if (createsBranch) { + updated = (updated & ~_BRANCH_NONCE_MASK) | (uint256(nextBranchSerial) << _BRANCH_NONCE_SHIFT); + } + book.nonceAndFlags = updated; + } + /// @notice Insert an already nonce-assigned resting order into the selected side tree. /// @param book Book storage containing both side roots. /// @param restingOrder Packed leaf to insert. /// @param isBid True for bid tree, false for ask tree. /// @param hookEnabled True to record a top-order change if insertion improves the book. - function _insertRestingOrder(Book storage book, bytes32 restingOrder, bool isBid, bool hookEnabled) private { + function _insertRestingOrder( + Book storage book, + bytes32 root, + bytes32 restingOrder, + bool isBid, + bool hookEnabled, + uint32 newBranchSerial + ) private { if (isBid) { - bytes32 root = book.tree[_ROOT_NODE].rightNode; - book.tree[_ROOT_NODE].rightNode = - _insertBid(book, root, restingOrder, _bidSortKey(restingOrder), hookEnabled); + book.tree[_ROOT_NODE].rightNode = _insertBid( + book, root, restingOrder, _bidSortKey(restingOrder), hookEnabled, newBranchSerial + ); } else { - bytes32 root = book.tree[_ROOT_NODE].leftNode; book.tree[_ROOT_NODE].leftNode = - _insertAsk(book, root, restingOrder, _askSortKey(restingOrder), hookEnabled); + _insertAsk(book, root, restingOrder, _askSortKey(restingOrder), hookEnabled, newBranchSerial); } } @@ -709,7 +827,7 @@ contract DeepstateV1 is Ownable { { bool samePrice = _correctionCode(node) != 0; if (matchFlags & _MATCH_DIRTY != 0) { - leftNode = _leftNodeAt(branchSlot); + leftNode = _leftNodeAt(branchSlot, node); samePrice = _price(leftNode) == _price(rightNode); } if (samePrice) { @@ -730,7 +848,7 @@ contract DeepstateV1 is Ownable { } if (rightFillQuantity == 0) return (node, 0, 0, bytes32(0)); - if (leftNode == bytes32(0)) leftNode = _leftNodeAt(branchSlot); + if (leftNode == bytes32(0)) leftNode = _leftNodeAt(branchSlot, node); bytes32 newLeftNode = leftNode; unchecked { remaining -= rightFillQuantity; @@ -745,7 +863,7 @@ contract DeepstateV1 is Ownable { (newNode, branchDirty) = _replaceRightmostRightChild(book, node, newLeftNode, newRightNode, false); if (branchDirty) matchChange = _markMatchDirty(matchChange); } else { - newNode = _replaceBranch(book, newLeftNode, newRightNode, false); + newNode = _replaceBranch(book, node, newLeftNode, newRightNode, false); } unchecked { fillQuantity += rightFillQuantity; @@ -769,7 +887,7 @@ contract DeepstateV1 is Ownable { private returns (bytes32 newNode, uint160 fillQuantity, uint256 quoteAmount) { - bytes32 leftNode = book.tree[node].leftNode; + bytes32 leftNode = _leftNode(book, node); if (leftNode == bytes32(0)) { (newNode,, fillQuantity, quoteAmount) = _matchAskLeaf(node, limitPrice, remaining); return (newNode, fillQuantity, quoteAmount); @@ -791,7 +909,7 @@ contract DeepstateV1 is Ownable { uint160 rightFillQuantity; uint256 rightQuoteAmount; { - bytes32 rightNode = book.tree[node].rightNode; + bytes32 rightNode = book.tree[_branchKey(node)].rightNode; (newRightNode, rightFillQuantity, rightQuoteAmount) = _matchAskSubtree(id, book, rightNode, limitPrice, remaining); } @@ -806,7 +924,9 @@ contract DeepstateV1 is Ownable { (newLeftNode, fillQuantity, quoteAmount) = _matchAskSubtree(id, book, leftNode, limitPrice, remaining); } - newNode = _replaceBranch(book, newLeftNode, newRightNode, false); + newNode = fillQuantity == 0 + ? _replaceBranchAfterSingleChildChange(book, node, newLeftNode, newRightNode, false, false) + : _replaceBranch(book, node, newLeftNode, newRightNode, false); unchecked { fillQuantity += rightFillQuantity; quoteAmount += rightQuoteAmount; @@ -851,7 +971,7 @@ contract DeepstateV1 is Ownable { { bool samePrice = _correctionCode(node) != 0; if (matchFlags & _MATCH_DIRTY != 0) { - leftNode = _leftNodeAt(branchSlot); + leftNode = _leftNodeAt(branchSlot, node); samePrice = _price(leftNode) == _price(rightNode); } if (samePrice) { @@ -873,7 +993,7 @@ contract DeepstateV1 is Ownable { } if (rightFillQuantity == 0) return (node, 0, 0, bytes32(0)); - if (leftNode == bytes32(0)) leftNode = _leftNodeAt(branchSlot); + if (leftNode == bytes32(0)) leftNode = _leftNodeAt(branchSlot, node); bytes32 newLeftNode = leftNode; unchecked { remaining -= rightFillQuantity; @@ -888,7 +1008,7 @@ contract DeepstateV1 is Ownable { (newNode, branchDirty) = _replaceRightmostRightChild(book, node, newLeftNode, newRightNode, true); if (branchDirty) matchChange = _markMatchDirty(matchChange); } else { - newNode = _replaceBranch(book, newLeftNode, newRightNode, true); + newNode = _replaceBranch(book, node, newLeftNode, newRightNode, true); } unchecked { fillQuantity += rightFillQuantity; @@ -912,7 +1032,7 @@ contract DeepstateV1 is Ownable { private returns (bytes32 newNode, uint160 fillQuantity, uint256 quoteAmount) { - bytes32 leftNode = book.tree[node].leftNode; + bytes32 leftNode = _leftNode(book, node); if (leftNode == bytes32(0)) { (newNode,, fillQuantity, quoteAmount) = _matchBidLeaf(node, limitPrice, remaining); return (newNode, fillQuantity, quoteAmount); @@ -934,7 +1054,7 @@ contract DeepstateV1 is Ownable { uint160 rightFillQuantity; uint256 rightQuoteAmount; { - bytes32 rightNode = book.tree[node].rightNode; + bytes32 rightNode = book.tree[_branchKey(node)].rightNode; (newRightNode, rightFillQuantity, rightQuoteAmount) = _matchBidSubtree(id, book, rightNode, limitPrice, remaining); } @@ -949,7 +1069,9 @@ contract DeepstateV1 is Ownable { (newLeftNode, fillQuantity, quoteAmount) = _matchBidSubtree(id, book, leftNode, limitPrice, remaining); } - newNode = _replaceBranch(book, newLeftNode, newRightNode, true); + newNode = fillQuantity == 0 + ? _replaceBranchAfterSingleChildChange(book, node, newLeftNode, newRightNode, true, false) + : _replaceBranch(book, node, newLeftNode, newRightNode, true); unchecked { fillQuantity += rightFillQuantity; quoteAmount += rightQuoteAmount; @@ -966,40 +1088,44 @@ contract DeepstateV1 is Ownable { /// Insertion follows Patricia/radix-tree rules. If the new key diverges before the current /// branch split, a new parent branch is created above `root`. Otherwise recursion continues /// into the child selected by the branch split bit. - function _insertBid(Book storage book, bytes32 root, bytes32 node, uint64 nodeKey, bool hookEnabled) - private - returns (bytes32 newRoot) - { + function _insertBid( + Book storage book, + bytes32 root, + bytes32 node, + uint64 nodeKey, + bool hookEnabled, + uint32 newBranchSerial + ) private returns (bytes32 newRoot) { if (root == bytes32(0)) { if (hookEnabled) _recordTopOrderChange(bytes32(0), _nonce(node)); return node; } - bytes32 leftNode = book.tree[root].leftNode; - if (leftNode == bytes32(0)) { - if (hookEnabled && nodeKey > _bidSortKey(root)) { + (bytes32 rightNode, uint256 branchSlot) = _rightNodeAndBranchSlot(book, root); + if (rightNode == bytes32(0)) { + uint64 rootKey = _bidSortKey(root); + if (hookEnabled && nodeKey > rootKey) { _recordTopOrderChange(root, _nonce(node)); } - return _storeBranch(book, root, node, _bidSortKey(root), nodeKey, true); + return _storeBranch(book, root, node, rootKey, nodeKey, true, newBranchSerial); } - bytes32 rightNode = book.tree[root].rightNode; - uint64 leftKey = _bidSortKey(leftNode); - uint8 branchDepth = _commonPrefix(leftKey, _bidSortKey(rightNode)); + (bytes32 leftNode, uint8 branchDepth) = _leftNodeAndDepthAt(branchSlot, root, rightNode); + uint64 leftKey = branchDepth < 32 ? _bidSortKey(root) : _bidNodeKey(book, leftNode); if (_commonPrefix(nodeKey, leftKey) < branchDepth) { if (hookEnabled && _bit(nodeKey, _commonPrefix(nodeKey, leftKey))) { _recordTopOrderChange(_rightmostLeaf(book, root), _nonce(node)); } - return _storeBranch(book, root, node, _bidSortKey(root), nodeKey, true); + return _storeBranch(book, root, node, leftKey, nodeKey, true, newBranchSerial); } if (_bit(nodeKey, branchDepth)) { - rightNode = _insertBid(book, rightNode, node, nodeKey, hookEnabled); + rightNode = _insertBid(book, rightNode, node, nodeKey, hookEnabled, newBranchSerial); + return _replaceBranchAfterSingleChildChange(book, root, leftNode, rightNode, true, false); } else { - leftNode = _insertBid(book, leftNode, node, nodeKey, false); + leftNode = _insertBid(book, leftNode, node, nodeKey, false, newBranchSerial); + return _replaceBranchAfterSingleChildChange(book, root, leftNode, rightNode, true, true); } - - return _replaceBranch(book, leftNode, rightNode, true); } /// @notice Insert a leaf or branch into the ask tree. @@ -1009,40 +1135,44 @@ contract DeepstateV1 is Ownable { /// @param hookEnabled True while this recursive frame can still affect the ask-side best order. /// @return newRoot Updated subtree root. /// @dev Same insertion algorithm as bids, but callers provide inverted-price ask keys. - function _insertAsk(Book storage book, bytes32 root, bytes32 node, uint64 nodeKey, bool hookEnabled) - private - returns (bytes32 newRoot) - { + function _insertAsk( + Book storage book, + bytes32 root, + bytes32 node, + uint64 nodeKey, + bool hookEnabled, + uint32 newBranchSerial + ) private returns (bytes32 newRoot) { if (root == bytes32(0)) { if (hookEnabled) _recordTopOrderChange(bytes32(0), _nonce(node)); return node; } - bytes32 leftNode = book.tree[root].leftNode; - if (leftNode == bytes32(0)) { - if (hookEnabled && nodeKey > _askSortKey(root)) { + (bytes32 rightNode, uint256 branchSlot) = _rightNodeAndBranchSlot(book, root); + if (rightNode == bytes32(0)) { + uint64 rootKey = _askSortKey(root); + if (hookEnabled && nodeKey > rootKey) { _recordTopOrderChange(root, _nonce(node)); } - return _storeBranch(book, root, node, _askSortKey(root), nodeKey, false); + return _storeBranch(book, root, node, rootKey, nodeKey, false, newBranchSerial); } - bytes32 rightNode = book.tree[root].rightNode; - uint64 leftKey = _askSortKey(leftNode); - uint8 branchDepth = _commonPrefix(leftKey, _askSortKey(rightNode)); + (bytes32 leftNode, uint8 branchDepth) = _leftNodeAndDepthAt(branchSlot, root, rightNode); + uint64 leftKey = branchDepth < 32 ? _askSortKey(root) : _askNodeKey(book, leftNode); if (_commonPrefix(nodeKey, leftKey) < branchDepth) { if (hookEnabled && _bit(nodeKey, _commonPrefix(nodeKey, leftKey))) { _recordTopOrderChange(_rightmostLeaf(book, root), _nonce(node)); } - return _storeBranch(book, root, node, _askSortKey(root), nodeKey, false); + return _storeBranch(book, root, node, leftKey, nodeKey, false, newBranchSerial); } if (_bit(nodeKey, branchDepth)) { - rightNode = _insertAsk(book, rightNode, node, nodeKey, hookEnabled); + rightNode = _insertAsk(book, rightNode, node, nodeKey, hookEnabled, newBranchSerial); + return _replaceBranchAfterSingleChildChange(book, root, leftNode, rightNode, false, false); } else { - leftNode = _insertAsk(book, leftNode, node, nodeKey, false); + leftNode = _insertAsk(book, leftNode, node, nodeKey, false, newBranchSerial); + return _replaceBranchAfterSingleChildChange(book, root, leftNode, rightNode, false, true); } - - return _replaceBranch(book, leftNode, rightNode, false); } /// @notice Remove one bid leaf by exact bid sort key. @@ -1060,16 +1190,13 @@ contract DeepstateV1 is Ownable { private returns (bytes32 newRoot, bytes32 removed, bool dirtyChanged, bool removedTop) { - bytes32 leftNode = book.tree[root].leftNode; - if (leftNode == bytes32(0)) { + (bytes32 rightNode, uint256 branchSlot) = _rightNodeAndBranchSlot(book, root); + if (rightNode == bytes32(0)) { return _bidSortKey(root) == targetKey ? (bytes32(0), root, false, rightmost) : (root, bytes32(0), false, false); } - bytes32 rightNode = book.tree[root].rightNode; - uint64 leftKey = _bidSortKey(leftNode); - uint8 branchDepth = _commonPrefix(leftKey, _bidSortKey(rightNode)); - if (_commonPrefix(targetKey, leftKey) < branchDepth) return (root, bytes32(0), false, false); + (bytes32 leftNode, uint8 branchDepth) = _leftNodeAndDepthAt(branchSlot, root, rightNode); bool goRight = _bit(targetKey, branchDepth); if (goRight) { @@ -1084,7 +1211,12 @@ contract DeepstateV1 is Ownable { (newRoot, branchDirty) = _replaceRightmostRightChild(book, root, leftNode, rightNode, true); return (newRoot, removed, dirtyChanged || branchDirty, removedTop); } - return (_replaceBranch(book, leftNode, rightNode, true), removed, dirtyChanged, removedTop); + return ( + _replaceBranchAfterSingleChildChange(book, root, leftNode, rightNode, true, !goRight), + removed, + dirtyChanged, + removedTop + ); } /// @notice Remove one ask leaf by exact ask sort key. @@ -1100,16 +1232,13 @@ contract DeepstateV1 is Ownable { private returns (bytes32 newRoot, bytes32 removed, bool dirtyChanged, bool removedTop) { - bytes32 leftNode = book.tree[root].leftNode; - if (leftNode == bytes32(0)) { + (bytes32 rightNode, uint256 branchSlot) = _rightNodeAndBranchSlot(book, root); + if (rightNode == bytes32(0)) { return _askSortKey(root) == targetKey ? (bytes32(0), root, false, rightmost) : (root, bytes32(0), false, false); } - bytes32 rightNode = book.tree[root].rightNode; - uint64 leftKey = _askSortKey(leftNode); - uint8 branchDepth = _commonPrefix(leftKey, _askSortKey(rightNode)); - if (_commonPrefix(targetKey, leftKey) < branchDepth) return (root, bytes32(0), false, false); + (bytes32 leftNode, uint8 branchDepth) = _leftNodeAndDepthAt(branchSlot, root, rightNode); bool goRight = _bit(targetKey, branchDepth); if (goRight) { @@ -1124,7 +1253,12 @@ contract DeepstateV1 is Ownable { (newRoot, branchDirty) = _replaceRightmostRightChild(book, root, leftNode, rightNode, false); return (newRoot, removed, dirtyChanged || branchDirty, removedTop); } - return (_replaceBranch(book, leftNode, rightNode, false), removed, dirtyChanged, removedTop); + return ( + _replaceBranchAfterSingleChildChange(book, root, leftNode, rightNode, false, !goRight), + removed, + dirtyChanged, + removedTop + ); } /// @notice Update a right-spine branch after only its right child changed. @@ -1147,10 +1281,10 @@ contract DeepstateV1 is Ownable { ) private returns (bytes32 newNode, bool dirtyChanged) { if (rightNode == bytes32(0)) return (leftNode, false); if (rightNode == branchNode || rightNode == leftNode) { - return (_replaceBranch(book, leftNode, rightNode, isBid), false); + return (_replaceBranch(book, branchNode, leftNode, rightNode, isBid), false); } - book.tree[branchNode].rightNode = rightNode; + book.tree[_branchKey(branchNode)].rightNode = rightNode; return (branchNode, true); } @@ -1160,9 +1294,10 @@ contract DeepstateV1 is Ownable { /// @return Replacement subtree root. /// @dev /// If one child was consumed or canceled, the other child is promoted. If both remain, the - /// branch address is recomputed from the child nodes and its pointers are written. Callers pass - /// children in already-valid left/right order. - function _replaceBranch(Book storage book, bytes32 leftNode, bytes32 rightNode, bool isBid) + /// branch summary is recomputed from the child nodes while its stable nonce identity is + /// preserved, and its pointers remain at the same mapping key. Callers pass children in + /// already-valid left/right order. + function _replaceBranch(Book storage book, bytes32 oldBranch, bytes32 leftNode, bytes32 rightNode, bool isBid) private returns (bytes32) { @@ -1172,14 +1307,57 @@ contract DeepstateV1 is Ownable { } else if (rightNode == bytes32(0)) { newBranch = leftNode; } else { - newBranch = _branchNodeForChildren(book, leftNode, rightNode, isBid); - // Replacement callers preserve left/right ordering from an existing valid branch. - book.tree[newBranch] = Branch({leftNode: leftNode, rightNode: rightNode}); + newBranch = _branchNodeForChildren(book, leftNode, rightNode, isBid, _nonce(oldBranch)); + // Right-first matching can leave both children alive only when the left child was not + // touched. The duplicate-child corruption guard also arrives here after changing only + // the right child, so the existing left slot and its cached split depth stay valid. + _rewriteSingleBranchChild(book, newBranch, leftNode, rightNode, false); } return newBranch; } + /// @dev Rebuild an existing branch after exactly one logical child changed. A surviving + /// branch's Patricia split class is fixed by its topology, so the unchanged child slot needs no + /// SSTORE. Same-price left-child rewrites preserve the cached split depth already stored there. + function _replaceBranchAfterSingleChildChange( + Book storage book, + bytes32 oldBranch, + bytes32 leftNode, + bytes32 rightNode, + bool isBid, + bool leftChanged + ) private returns (bytes32 newBranch) { + if (leftNode == bytes32(0)) return rightNode; + if (rightNode == bytes32(0)) return leftNode; + + newBranch = _branchNodeForChildren(book, leftNode, rightNode, isBid, _nonce(oldBranch)); + _rewriteSingleBranchChild(book, newBranch, leftNode, rightNode, leftChanged); + } + + /// @dev Persist the only child pointer changed by a stable-identity branch rewrite. + function _rewriteSingleBranchChild( + Book storage book, + bytes32 branchNode, + bytes32 leftNode, + bytes32 rightNode, + bool leftChanged + ) private { + Branch storage branch = book.tree[_branchKey(branchNode)]; + if (leftChanged) { + if (_correctionCode(branchNode) != 0) { + // Only five cached bits survive the shift. + // forge-lint: disable-next-line(unsafe-typecast) + uint8 depth = 32 + uint8(uint256(branch.leftNode) >> _CACHED_DEPTH_SHIFT); + branch.leftNode = _encodeStoredLeftNode(branchNode, leftNode, depth); + } else { + branch.leftNode = leftNode; + } + } else { + branch.rightNode = rightNode; + } + } + /// @notice Partially consume a clean same-tick subtree while decoding its tick only once. /// @dev The nonzero correction code proves that every descendant has `tick`. Exact quote /// deltas returned by child frames let each surviving ancestor update its correction without @@ -1202,18 +1380,20 @@ contract DeepstateV1 is Ownable { uint16 shift ) private returns (bytes32 newNode, uint160 fillQuantity, uint256 quoteAmount) { uint160 nodeQuantity = _quantity(node); - bytes32 leftNode = book.tree[node].leftNode; + uint32 correctionCode = _correctionCode(node); if (nodeQuantity <= remaining) { - quoteAmount = leftNode == bytes32(0) - ? _quoteAtFactor(factor, shift, nodeQuantity, restingIsBid) - : _uniformNodeQuoteAtFactor(book, node, restingIsBid, factor, shift); - bytes32 eventNode = leftNode == bytes32(0) ? _withQuantityAndCorrection(node, nodeQuantity, 1) : node; + quoteAmount = _quoteAtFactor(factor, shift, nodeQuantity, restingIsBid); + if (correctionCode != 0) { + uint256 correction = uint256(correctionCode) - 1; + quoteAmount = restingIsBid ? quoteAmount + correction : quoteAmount - correction; + } + bytes32 eventNode = correctionCode == 0 ? _withQuantityAndCorrection(node, nodeQuantity, 1) : node; _recordMatch(eventNode); return (bytes32(0), nodeQuantity, quoteAmount); } - if (leftNode == bytes32(0)) { + if (correctionCode == 0) { uint160 newQuantity; unchecked { newQuantity = nodeQuantity - remaining; @@ -1226,7 +1406,8 @@ contract DeepstateV1 is Ownable { return (_withQuantity(node, newQuantity), remaining, quoteAmount); } - bytes32 rightNode = book.tree[node].rightNode; + bytes32 leftNode = _leftNode(book, node); + bytes32 rightNode = book.tree[_branchKey(node)].rightNode; uint160 rightFillQuantity; uint256 rightQuoteAmount; (rightNode, rightFillQuantity, rightQuoteAmount) = @@ -1280,13 +1461,13 @@ contract DeepstateV1 is Ownable { } else { correction = newAggregateQuote + oldCorrection + quoteAmount - oldAggregateQuote; } - uint64 leftKey = _pathKey(leftNode); - uint64 rightKey = _pathKey(rightNode); - // A correction is at most liveLeafCount - 1. The decrementing nonce admits at most - // 2^32 - 2 resting leaves, so correction + 1 is strictly representable in uint32. + // A correction is at most liveLeafCount - 1. The shared node-identity namespace admits + // fewer than 2^32 live nodes, so correction + 1 is strictly representable in uint32. // forge-lint: disable-next-line(unsafe-typecast) - newNode = _branchNode(leftKey > rightKey ? leftKey : rightKey, newQuantity, uint32(correction + 1)); - book.tree[newNode] = Branch({leftNode: leftNode, rightNode: rightNode}); + newNode = _branchNode(_price(oldNode), newQuantity, uint32(correction + 1), _nonce(oldNode)); + // A surviving uniform branch cannot have changed its left child: matching is right-first, + // and reaching the left child requires consuming the right child and collapsing the branch. + _rewriteSingleBranchChild(book, newNode, leftNode, rightNode, false); } /// @notice Rebuild a previously optimized right spine back into exact aggregate branches. @@ -1298,11 +1479,11 @@ contract DeepstateV1 is Ownable { function _materializeRightSpine(Book storage book, bytes32 node, bool isBid) private returns (bytes32) { if (node == bytes32(0)) return bytes32(0); - bytes32 leftNode = book.tree[node].leftNode; + bytes32 leftNode = _leftNode(book, node); if (leftNode == bytes32(0)) return node; - bytes32 rightNode = _materializeRightSpine(book, book.tree[node].rightNode, isBid); - return _replaceBranch(book, leftNode, rightNode, isBid); + bytes32 rightNode = _materializeRightSpine(book, book.tree[_branchKey(node)].rightNode, isBid); + return _replaceBranchAfterSingleChildChange(book, node, leftNode, rightNode, isBid, false); } /// @notice Return the aggregate quantity for a fully crossing same-price subtree on the global right spine. @@ -1354,14 +1535,14 @@ contract DeepstateV1 is Ownable { view returns (bool uniform, int32 tick, uint160 quantity, uint256 quoteAmount, bytes32 rightmostOrder) { - bytes32 leftNode = book.tree[node].leftNode; + bytes32 leftNode = _leftNode(book, node); if (leftNode == bytes32(0)) { quantity = _quantity(node); return (true, _price(node), quantity, _quoteValue(_price(node), quantity, isBid), node); } - bytes32 rightNode = book.tree[node].rightNode; - bool leftUniform = book.tree[leftNode].leftNode == bytes32(0) || _correctionCode(leftNode) != 0; + bytes32 rightNode = book.tree[_branchKey(node)].rightNode; + bool leftUniform = book.tree[_branchKey(leftNode)].leftNode == bytes32(0) || _correctionCode(leftNode) != 0; if (!leftUniform) return (false, 0, 0, 0, 0); (bool rightUniform, int32 rightTick, uint160 rightQuantity, uint256 rightQuote, bytes32 rightmostLeaf) = @@ -1386,10 +1567,15 @@ contract DeepstateV1 is Ownable { /// for honest state because nonce assignment is unique; if corruption makes them equal, this /// reverts before overwriting ownership or branch data. /// Callers pass nonzero children; empty-subtree cases are handled before this helper is reached. - function _storeBranch(Book storage book, bytes32 a, bytes32 b, uint64 aKey, uint64 bKey, bool isBid) - private - returns (bytes32 branchNode) - { + function _storeBranch( + Book storage book, + bytes32 a, + bytes32 b, + uint64 aKey, + uint64 bKey, + bool isBid, + uint32 branchSerial + ) private returns (bytes32 branchNode) { uint8 branchDepth = _commonPrefix(aKey, bKey); if (branchDepth == 64) revert DuplicateOrder(); @@ -1400,70 +1586,71 @@ contract DeepstateV1 is Ownable { rightNode = a; } - branchNode = _branchNodeForChildren(book, a, b, isBid); + branchNode = _branchNodeForChildren(book, leftNode, rightNode, isBid, branchSerial); // Walkers use leftNode as the branch sentinel, so stored branches are always two-child. - book.tree[branchNode] = Branch({leftNode: leftNode, rightNode: rightNode}); + _setNewBranchChildren(book, branchNode, leftNode, rightNode, branchDepth); } - /// @notice Compute the self-addressed branch node for two children. + /// @notice Compute a branch summary for two children while preserving its stable identity. /// @param a First child. /// @param b Second child. - /// @return Branch node whose quantity is the child sum and path is the maximum child path key. - /// @dev Uses the raw price/nonce path, not the bid/ask sort key, so bid and ask branches with - /// different economic meaning can still coexist in the same mapping as long as their resulting - /// `bytes32` branch keys differ. Tests assert that live bid/ask branches do not share storage. - function _branchNodeForChildren(Book storage book, bytes32 a, bytes32 b, bool isBid) + /// @param branchNonce Stable full-width serial identity allocated exclusively to this branch. + /// @return Branch node whose quantity is the child sum and whose nonce is stable across rewrites. + function _branchNodeForChildren(Book storage book, bytes32 a, bytes32 b, bool isBid, uint32 branchNonce) private view returns (bytes32) { - uint64 aAddressKey = _pathKey(a); - uint64 bAddressKey = _pathKey(b); - uint64 boundaryKey = aAddressKey > bAddressKey ? aAddressKey : bAddressKey; uint160 quantity = _quantity(a) + _quantity(b); + // Branches retain the tick of their leftmost representative, so no descendant walk is + // needed when rebuilding an aggregate summary. + int32 tick = _price(a); uint32 correctionCode = 0; if (_price(a) == _price(b) && _uniformNode(book, a) && _uniformNode(book, b)) { - int32 tick = _price(a); + tick = _price(a); (uint256 factor, uint16 shift) = TickMath32.getPriceFactorAtTick(tick); uint256 childQuote = _uniformNodeQuoteAtFactor(book, a, isBid, factor, shift) + _uniformNodeQuoteAtFactor(book, b, isBid, factor, shift); uint256 aggregateQuote = _quoteAtFactor(factor, shift, quantity, isBid); uint256 correction = isBid ? childQuote - aggregateQuote : aggregateQuote - childQuote; - // A correction is at most liveLeafCount - 1. The decrementing nonce admits at most - // 2^32 - 2 resting leaves, so correction + 1 is strictly representable in uint32. + // A correction is at most liveLeafCount - 1. The shared node-identity namespace admits + // fewer than 2^32 live nodes, so correction + 1 is strictly representable in uint32. // forge-lint: disable-next-line(unsafe-typecast) correctionCode = uint32(correction + 1); } - return _branchNode(boundaryKey, quantity, correctionCode); + return _branchNode(tick, quantity, correctionCode, branchNonce); } - /// @notice Pack a branch node from a raw path key and aggregate quantity. - /// @param key Raw `price || nonce` path key used as the branch address suffix. + /// @notice Pack a branch node from aggregate metadata and its stable identity. + /// @param tick Representative tick, or the exact tick for a uniform branch. /// @param quantity Aggregate quantity represented by the branch. /// @return node Packed branch node. - function _branchNode(uint64 key, uint160 quantity, uint32 correctionCode) private pure returns (bytes32 node) { + function _branchNode(int32 tick, uint160 quantity, uint32 correctionCode, uint32 branchNonce) + private + pure + returns (bytes32 node) + { /// @solidity memory-safe-assembly assembly { - let rawTick := xor(shr(32, key), 0x80000000) node := or( - or(shl(_PRICE_SHIFT, rawTick), shl(_QUANTITY_SHIFT, quantity)), - or(shl(_CORRECTION_SHIFT, correctionCode), and(key, 0xffffffff)) + or(shl(_PRICE_SHIFT, and(tick, 0xffffffff)), shl(_QUANTITY_SHIFT, quantity)), + or(shl(_CORRECTION_SHIFT, correctionCode), branchNonce) ) } } /// @notice Return whether a node represents one tick exactly. function _uniformNode(Book storage book, bytes32 node) private view returns (bool) { - return book.tree[node].leftNode == bytes32(0) || _correctionCode(node) != 0; + return _correctionCode(node) != 0 || book.tree[_branchKey(node)].leftNode == bytes32(0); } /// @notice Return the exact sum of leaf-level rounded notionals for a uniform-tick node. - function _uniformNodeQuote(Book storage book, bytes32 node, bool isBid) private view returns (uint256 quoteAmount) { + function _uniformNodeQuote(Book storage, bytes32 node, bool isBid) private pure returns (uint256 quoteAmount) { uint160 quantity = _quantity(node); quoteAmount = _quoteValue(_price(node), quantity, isBid); - if (book.tree[node].leftNode == bytes32(0)) return quoteAmount; + if (_correctionCode(node) == 0) return quoteAmount; return _applyUniformCorrection(node, quoteAmount, isBid); } @@ -1481,20 +1668,20 @@ contract DeepstateV1 is Ownable { } /// @dev Uniform-node quote using a price factor already decoded for the node's tick. - function _uniformNodeQuoteAtFactor(Book storage book, bytes32 node, bool isBid, uint256 factor, uint16 shift) + function _uniformNodeQuoteAtFactor(Book storage, bytes32 node, bool isBid, uint256 factor, uint16 shift) private - view + pure returns (uint256 quoteAmount) { quoteAmount = _quoteAtFactor(factor, shift, _quantity(node), isBid); - if (book.tree[node].leftNode == bytes32(0)) return quoteAmount; + if (_correctionCode(node) == 0) return quoteAmount; uint256 correction = uint256(_correctionCode(node)) - 1; quoteAmount = isBid ? quoteAmount + correction : quoteAmount - correction; } /// @dev Load a branch's right child and retain its mapping slot so the left child can be read - /// later without hashing `tree[node]` a second time. + /// later without hashing the nonce-addressed child slot a second time. function _rightNodeAndBranchSlot(Book storage book, bytes32 node) private view @@ -1502,21 +1689,68 @@ contract DeepstateV1 is Ownable { { /// @solidity memory-safe-assembly assembly { - mstore(0, node) + mstore(0, and(node, 0xffffffff)) mstore(0x20, add(book.slot, 1)) branchSlot := keccak256(0, 0x40) rightNode := sload(add(branchSlot, 1)) } } - /// @dev Load the left child paired with a slot returned by `_rightNodeAndBranchSlot`. - function _leftNodeAt(uint256 branchSlot) private view returns (bytes32 leftNode) { + /// @dev Load and decode the left child paired with a previously computed branch slot. + function _leftNodeAt(uint256 branchSlot, bytes32 branchNode) private view returns (bytes32 leftNode) { + bytes32 storedLeftNode; + /// @solidity memory-safe-assembly + assembly { + storedLeftNode := sload(branchSlot) + } + leftNode = _decodeStoredLeftNode(branchNode, storedLeftNode); + } + + /// @dev Load and decode the left child paired with a slot returned by + /// `_rightNodeAndBranchSlot`, and recover the branch's Patricia split depth without another + /// storage read. Mixed-price depth comes from the child ticks. Same-price depth comes from the + /// five metadata bits embedded in the stored left-child representation. + function _leftNodeAndDepthAt(uint256 branchSlot, bytes32 branchNode, bytes32 rightNode) + private + view + returns (bytes32 leftNode, uint8 depth) + { + bytes32 storedLeftNode; /// @solidity memory-safe-assembly assembly { - leftNode := sload(branchSlot) + storedLeftNode := sload(branchSlot) + } + + if (_correctionCode(branchNode) != 0) { + depth = 32 + uint8(uint256(storedLeftNode) >> _CACHED_DEPTH_SHIFT); + leftNode = _decodeStoredLeftNode(branchNode, storedLeftNode); + } else { + leftNode = storedLeftNode; + uint64 leftPriceKey = uint64(uint32(_price(leftNode))) << 32; + uint64 rightPriceKey = uint64(uint32(_price(rightNode))) << 32; + depth = _commonPrefix(leftPriceKey, rightPriceKey); } } + /// @dev Load a branch's exact left child, reversing the same-price cached-depth encoding. + function _leftNode(Book storage book, bytes32 branchNode) private view returns (bytes32 leftNode) { + leftNode = _decodeStoredLeftNode(branchNode, book.tree[_branchKey(branchNode)].leftNode); + } + + /// @dev Return the cached or tick-derived split depth for invariant harnesses. + function _storedBranchDepth(Book storage book, bytes32 branchNode) internal view returns (uint8 depth) { + Branch storage branch = book.tree[_branchKey(branchNode)]; + bytes32 storedLeftNode = branch.leftNode; + if (_correctionCode(branchNode) != 0) { + return 32 + uint8(uint256(storedLeftNode) >> _CACHED_DEPTH_SHIFT); + } + + bytes32 leftNode = storedLeftNode; + uint64 leftPriceKey = uint64(uint32(_price(leftNode))) << 32; + uint64 rightPriceKey = uint64(uint32(_price(branch.rightNode))) << 32; + depth = _commonPrefix(leftPriceKey, rightPriceKey); + } + /// @notice Return the price of the leftmost leaf in a subtree. /// @param node Subtree root. /// @return price Price of the worst executable leaf in the subtree. @@ -1525,12 +1759,14 @@ contract DeepstateV1 is Ownable { /// highest ask price because ask sort keys invert price. In both cases this is the "worst" /// price that must cross before an entire subtree can be aggregate-consumed. function _leftmostLeafPrice(Book storage book, bytes32 node) private view returns (int32 price) { + price = _price(_leftmostLeaf(book, node)); + } + + /// @dev Return any stable routing representative from a subtree without using branch identity. + function _leftmostLeaf(Book storage book, bytes32 node) private view returns (bytes32 leaf) { while (true) { - bytes32 leftNode = book.tree[node].leftNode; - if (leftNode == bytes32(0)) { - price = _price(node); - break; - } + bytes32 leftNode = _leftNode(book, node); + if (leftNode == bytes32(0)) return node; node = leftNode; } } @@ -1541,7 +1777,7 @@ contract DeepstateV1 is Ownable { /// @dev The right child is always the better sort-key side for both bid and ask trees. function _rightmostLeaf(Book storage book, bytes32 node) private view returns (bytes32 leaf) { while (true) { - bytes32 rightNode = book.tree[node].rightNode; + bytes32 rightNode = book.tree[_branchKey(node)].rightNode; if (rightNode == bytes32(0)) return node; node = rightNode; } @@ -1571,7 +1807,7 @@ contract DeepstateV1 is Ownable { /// when a top removal leaves a branch root and the successor leaf is not encoded in that root. function _replacementTopNonce(Book storage book, bytes32 newNode) private view returns (uint32) { if (newNode == bytes32(0)) return 0; - if (book.tree[newNode].leftNode == bytes32(0)) return _nonce(newNode); + if (book.tree[_branchKey(newNode)].leftNode == bytes32(0)) return _nonce(newNode); return _nonce(_rightmostLeaf(book, newNode)); } @@ -1708,16 +1944,16 @@ contract DeepstateV1 is Ownable { returns (uint256 quoteAmount) { uint160 quantity = _quantity(node); - bytes32 leftNode = book.tree[node].leftNode; - if (leftNode == bytes32(0)) { - return _quoteValue(_price(node), quantity, restingIsBid); + if (_correctionCode(node) != 0) { + return _uniformBranchQuote(node, restingIsBid); } - if (_correctionCode(node) != 0) { - return _uniformNodeQuote(book, node, restingIsBid); + bytes32 leftNode = _leftNode(book, node); + if (leftNode == bytes32(0)) { + return _quoteValue(_price(node), quantity, restingIsBid); } - quoteAmount = _subtreeQuote(book, book.tree[node].rightNode, restingIsBid); + quoteAmount = _subtreeQuote(book, book.tree[_branchKey(node)].rightNode, restingIsBid); unchecked { quoteAmount += _subtreeQuote(book, leftNode, restingIsBid); } @@ -1747,16 +1983,14 @@ contract DeepstateV1 is Ownable { } } - /// @notice Build the raw address path key from a node. - /// @param order Packed node. - /// @return key Raw `price || nonce` key, ignoring quantity. - /// @dev Branch addresses use raw path keys for both sides of the book. - function _pathKey(bytes32 order) private pure returns (uint64 key) { - /// @solidity memory-safe-assembly - assembly { - let tickKey := xor(and(shr(_PRICE_SHIFT, order), 0xffffffff), 0x80000000) - key := or(shl(32, tickKey), and(order, 0xffffffff)) - } + /// @dev Return a bid radix key represented by an actual descendant leaf. + function _bidNodeKey(Book storage book, bytes32 node) private view returns (uint64 key) { + key = _bidSortKey(_leftmostLeaf(book, node)); + } + + /// @dev Return an ask radix key represented by an actual descendant leaf. + function _askNodeKey(Book storage book, bytes32 node) private view returns (uint64 key) { + key = _askSortKey(_leftmostLeaf(book, node)); } /// @notice Count matching leading bits between two 64-bit radix keys. @@ -1789,16 +2023,15 @@ contract DeepstateV1 is Ownable { /// @notice Return whether a side has optimized right-spine anchors that need materialization before insert. /// @param isBid True for the bid tree, false for the ask tree. /// @return dirty True if the side's right spine contains stale branch aggregate words. - function _rightSpineDirty(Book storage book, bool isBid) private view returns (bool dirty) { + function _rightSpineDirty(uint256 nonceAndFlags, bool isBid) private pure returns (bool dirty) { uint256 flag = isBid ? _BID_RIGHT_SPINE_DIRTY : _ASK_RIGHT_SPINE_DIRTY; - dirty = book.nonceAndFlags & flag != 0; + dirty = nonceAndFlags & flag != 0; } /// @notice Mark a side's right spine dirty. /// @param isBid True for the bid tree, false for the ask tree. - function _setRightSpineDirty(Book storage book, bool isBid) private { + function _setRightSpineDirty(Book storage book, bool isBid, uint256 nonceAndFlags) private { uint256 flag = isBid ? _BID_RIGHT_SPINE_DIRTY : _ASK_RIGHT_SPINE_DIRTY; - uint256 nonceAndFlags = book.nonceAndFlags; if (nonceAndFlags & flag == 0) { book.nonceAndFlags = nonceAndFlags | flag; } @@ -1812,6 +2045,15 @@ contract DeepstateV1 is Ownable { return uint32(book.nonceAndFlags & _NONCE_MASK); } + /// @dev Return the next ascending branch serial encoded in a packed book word. + function _nextBranchSerial(uint256 nonceAndFlags) private pure returns (uint32 nonce) { + // A zero field is accepted for test harnesses and legacy-empty books; identity zero remains + // reserved for the root anchor, so allocation begins at one. + // forge-lint: disable-next-line(unsafe-typecast) + nonce = uint32((nonceAndFlags & _BRANCH_NONCE_MASK) >> _BRANCH_NONCE_SHIFT); + if (nonce == 0) nonce = 1; + } + /// @notice Build the globally unique owner key for an order in a book. /// @param bookKey Book id that scopes the order. /// @param order Original packed order node. @@ -2010,8 +2252,8 @@ contract DeepstateV1 is Ownable { { uint256 aggregateQuote = _quoteValue(_price(node), quantity, isBid); uint256 correction = isBid ? quoteAmount - aggregateQuote : aggregateQuote - quoteAmount; - // A correction is at most matchedLeafCount - 1. The decrementing nonce admits at most - // 2^32 - 2 resting leaves, so correction + 1 is strictly representable in uint32. + // A correction is at most matchedLeafCount - 1. The opposing order/branch allocation + // fronts admit at most 2^31 live leaves, so correction + 1 is representable in uint32. // forge-lint: disable-next-line(unsafe-typecast) updated = _withQuantityAndCorrection(node, quantity, uint32(correction + 1)); } @@ -2049,7 +2291,7 @@ contract DeepstateV1 is Ownable { /// @notice Pack price, quantity, and nonce into a node. /// @param price Signed 32-bit logarithmic tick. /// @param quantity 160-bit base quantity. - /// @param nonce 32-bit nonce or branch path suffix. + /// @param nonce Unique 32-bit order nonce. /// @return packed Packed `bytes32` node. function _pack(int32 price, uint160 quantity, uint32 nonce) private pure returns (bytes32 packed) { /// @solidity memory-safe-assembly @@ -2068,9 +2310,9 @@ contract DeepstateV1 is Ownable { } } - /// @notice Extract the low 32-bit nonce/path suffix from a packed node. + /// @notice Extract the low 32-bit identity from a packed node. /// @param order Packed node. - /// @return 32-bit nonce or branch path suffix. + /// @return 32-bit order nonce or branch nonce. function _nonce(bytes32 order) private pure returns (uint32) { // forge-lint: disable-next-line(unsafe-typecast) return uint32(uint256(order)); @@ -2446,8 +2688,9 @@ contract DeepstateV1 is Ownable { bool isBid; address owner; - uint256 hookFlags = _cancelHookFlags(book.nonceAndFlags); - (owner, isBid, baseAmount, quoteAmount) = _cancelBook(id, book, order, msg.sender, hookFlags); + uint256 nonceAndFlags = book.nonceAndFlags; + uint256 hookFlags = _cancelHookFlags(nonceAndFlags); + (owner, isBid, baseAmount, quoteAmount) = _cancelBook(id, book, order, msg.sender, hookFlags, nonceAndFlags); if (_cancelHookEnabled(hookFlags, isBid)) _executeTopOrderHook(token0, token1, id, isBid); if (baseAmount != 0) _safeTransferOut(token0, owner, baseAmount); @@ -2552,14 +2795,14 @@ contract DeepstateV1 is Ownable { bidRoot = root.rightNode; } - /// @notice Branch child pointers for a node in a book. + /// @notice Branch child pointers for a node in a book, addressed by the node nonce. /// @param id Book id. /// @param node Leaf or branch node. /// @return leftNode Stored left child, or zero if `node` is a leaf/empty. /// @return rightNode Stored right child, or zero if `node` is a leaf/empty. function tree(bytes32 id, bytes32 node) external view returns (bytes32 leftNode, bytes32 rightNode) { - Branch storage branch = books[id].tree[node]; - leftNode = branch.leftNode; + Branch storage branch = books[id].tree[bytes32(uint256(_nonce(node)))]; + leftNode = _decodeStoredLeftNode(node, branch.leftNode); rightNode = branch.rightNode; } @@ -2593,12 +2836,12 @@ contract DeepstateV1 is Ownable { uint160 baseFilled; uint256 quoteAmount; - (limitPrice, remaining, baseFilled, quoteAmount) = + (limitPrice, remaining, baseFilled, quoteAmount, routedNonceAndFlags) = _matchOrValidate(params, routedBookId, routedBook, routedNonceAndFlags); - // Rotation initializes the successor as soon as nonce two is assigned, so a nonce-one - // book is exhausted and historical under valid state transitions. It remains matchable, - // but unmatched quantity from it is automatically no-rest. + // Rotation initializes the successor as soon as the order- and branch-identity allocation + // fronts meet, so a nonce-one book is exhausted and historical under valid transitions. It + // remains matchable, but unmatched quantity from it is automatically no-rest. bool restAllowed = !params.noRest && routedNonce != 1; if (remaining != 0 && params.fillOrKill) revert FillOrKill(); @@ -2615,6 +2858,7 @@ contract DeepstateV1 is Ownable { routedBookId, routedBook, routedNonce == 0, + routedNonceAndFlags, limitPrice, remaining, true @@ -2635,6 +2879,7 @@ contract DeepstateV1 is Ownable { routedBookId, routedBook, routedNonce == 0, + routedNonceAndFlags, limitPrice, remaining, false @@ -2716,7 +2961,17 @@ contract DeepstateV1 is Ownable { bytes32 routedBookId, Book storage routedBook, uint256 routedNonceAndFlags - ) private returns (int32 limitPrice, uint160 remaining, uint160 baseFilled, uint256 quoteAmount) { + ) + private + returns ( + int32 limitPrice, + uint160 remaining, + uint160 baseFilled, + uint256 quoteAmount, + uint256 updatedNonceAndFlags + ) + { + updatedNonceAndFlags = routedNonceAndFlags; // forge-lint: disable-next-line(unsafe-typecast) uint32 routedNonce = uint32(routedNonceAndFlags); if (routedNonce == 0) { @@ -2724,8 +2979,8 @@ contract DeepstateV1 is Ownable { (limitPrice, remaining) = _validateIncomingOrder(params.order); } else { bool hookEnabled = _bookHookEnabled(routedNonceAndFlags, !params.isBid); - (limitPrice, remaining, baseFilled, quoteAmount) = - _matchBook(routedBookId, routedBook, params.order, params.isBid, hookEnabled); + (limitPrice, remaining, baseFilled, quoteAmount, updatedNonceAndFlags) = + _matchBook(routedBookId, routedBook, params.order, params.isBid, hookEnabled, routedNonceAndFlags); if (hookEnabled) _executeTopOrderHook(params.token0, params.token1, routedBookId, !params.isBid); } } @@ -2752,6 +3007,7 @@ contract DeepstateV1 is Ownable { bytes32 routedBookId, Book storage routedBook, bool routedBookWasEmpty, + uint256 routedNonceAndFlags, int32 limitPrice, uint160 remaining, bool isBid @@ -2762,7 +3018,7 @@ contract DeepstateV1 is Ownable { if (routedBookWasEmpty) { restNonceAndFlags = _initializeRoutedBook(token0, token1, routedEpoch, routedBookId); } else { - restNonceAndFlags = routedBook.nonceAndFlags; + restNonceAndFlags = routedNonceAndFlags; } uint32 nextNonceAfter; bool hookEnabled = _bookHookEnabled(restNonceAndFlags, isBid); @@ -2811,7 +3067,7 @@ contract DeepstateV1 is Ownable { uint256 bookHookFlags = 0; if (poolState & _POOL_HOOK_ACTIVE_MASK != 0) bookHookFlags = _bookHookFlags(poolState); - nonceAndFlags = uint256(type(uint32).max) | bookHookFlags; + nonceAndFlags = uint256(type(uint32).max) | (uint256(1) << _BRANCH_NONCE_SHIFT) | bookHookFlags; emit BookInitialized(pid, routedBookId, epoch); } @@ -2819,10 +3075,11 @@ contract DeepstateV1 is Ownable { /// @param token0 Lower token address. /// @param token1 Higher token address. /// @dev - /// The order that receives nonce `2` leaves `nextNonce == 1`; that exhausted book remains - /// matchable and cancelable, but later fills against it are automatically no-rest. A caller must - /// route a separate leg to the next active epoch to create maker liquidity. Hook flags are - /// removed from the old book and copied into the newly initialized book. + /// When the descending order front and ascending branch front would meet, `_restBook` stores + /// `nextNonce == 1`; that exhausted book remains matchable and cancelable, but later fills + /// against it are automatically no-rest. A caller must route a separate leg to the next active + /// epoch to create maker liquidity. Hook flags are removed from the old book and copied into the + /// newly initialized book. function _rotateIfExhausted(address token0, address token1) private { bytes32 pid = poolId(token0, token1); uint256 poolState = _poolEpochAndHookFlags[pid]; @@ -2929,7 +3186,7 @@ contract DeepstateV1 is Ownable { /// @notice Initialize a new book with nonce max and inherited hook flags. function _initializeBookWithHookFlags(Book storage book, uint256 flags) private { if (_nextNonce(book) != 0) return; - book.nonceAndFlags = uint256(type(uint32).max) | flags; + book.nonceAndFlags = uint256(type(uint32).max) | (uint256(1) << _BRANCH_NONCE_SHIFT) | flags; } /// @notice Require canonical sorted token addresses; `address(0)` is native ETH and can only be token0. diff --git a/test/DeepstateV1.t.sol b/test/DeepstateV1.t.sol index 6e569e0..15fa32b 100644 --- a/test/DeepstateV1.t.sol +++ b/test/DeepstateV1.t.sol @@ -99,6 +99,65 @@ contract DeepstateV1Test is Test { assertEq(token1.balanceOf(address(engine)), _quoteValue(10, 5, true)); } + function test_BranchNoncesAreUniqueAndDoNotAliasOrderLeaves() public { + bytes32[4] memory orders; + for (uint256 i; i < orders.length; ++i) { + vm.prank(alice); + orders[i] = engine.fill(_fill(0, _order(10, 10, 0), false, false, false)); + assertEq(uint32(uint256(orders[i])), MAX_ORDER_NONCE - uint32(i), "order nonce sequence"); + } + + bytes32 id = engine.bookId(address(token0), address(token1), 0); + (bytes32 root,) = engine.roots(address(token0), address(token1), 0); + (bytes32 leftBranch, bytes32 rightBranch) = engine.tree(id, root); + + assertLe(uint32(uint256(root)), 3, "root branch serial not separately allocated"); + assertLe(uint32(uint256(leftBranch)), 3, "left branch serial not separately allocated"); + assertLe(uint32(uint256(rightBranch)), 3, "right branch serial not separately allocated"); + assertTrue(uint32(uint256(root)) != uint32(uint256(leftBranch)), "root/left branch nonce collision"); + assertTrue(uint32(uint256(root)) != uint32(uint256(rightBranch)), "root/right branch nonce collision"); + assertTrue(uint32(uint256(leftBranch)) != uint32(uint256(rightBranch)), "left/right branch nonce collision"); + + for (uint256 i; i < orders.length; ++i) { + (bytes32 leafLeft, bytes32 leafRight) = engine.tree(id, orders[i]); + assertEq(leafLeft, bytes32(0), "order leaf aliased branch children"); + assertEq(leafRight, bytes32(0), "order leaf aliased branch children"); + } + } + + function test_PartialFillReusesBranchNonceAndChildSlot() public { + for (uint256 i; i < 4; ++i) { + vm.prank(alice); + engine.fill(_fill(0, _order(10, 10, 0), false, false, false)); + } + + bytes32 id = engine.bookId(address(token0), address(token1), 0); + (bytes32 oldRoot,) = engine.roots(address(token0), address(token1), 0); + (bytes32 oldLeftBranch, bytes32 oldRightBranch) = engine.tree(id, oldRoot); + assertTrue(oldLeftBranch != bytes32(0) && oldRightBranch != bytes32(0), "expected balanced branch"); + + (bytes32 oldLeftChild, bytes32 oldRightChild) = engine.tree(id, oldLeftBranch); + + // Consume the two best leaves and half of the next one. The former left subtree survives + // with a different aggregate quantity and is promoted to the root. + vm.prank(bob); + engine.fill(_fill(0, _order(10, 25, 0), true, true, false)); + + (bytes32 newRoot,) = engine.roots(address(token0), address(token1), 0); + assertTrue(newRoot != oldLeftBranch, "aggregate word should change"); + assertEq(uint32(uint256(newRoot)), uint32(uint256(oldLeftBranch)), "branch identity changed"); + assertEq(uint160(uint256(newRoot) >> 64), 15, "surviving aggregate quantity"); + + (bytes32 newLeftChild, bytes32 newRightChild) = engine.tree(id, newRoot); + assertEq(newLeftChild, oldLeftChild, "unchanged child moved"); + assertTrue(newRightChild != oldRightChild, "partially filled child was not rewritten"); + + // The old packed branch word resolves through the same nonce-addressed mapping slot. + (bytes32 oldKeyLeft, bytes32 oldKeyRight) = engine.tree(id, oldLeftBranch); + assertEq(oldKeyLeft, newLeftChild); + assertEq(oldKeyRight, newRightChild); + } + function test_InvalidTokenAndHookConfigBranches() public { vm.expectRevert(bytes4(keccak256("InvalidToken()"))); engine.activeBookId(address(token1), address(token0)); @@ -932,6 +991,67 @@ contract DeepstateV1Test is Test { engine.restBookForTest(id, 1, 10, 5, true, alice); } + function test_RestBookHarnessRejectsExhaustedBranchSerial() public { + vm.prank(alice); + engine.fill(_fill(0, _order(10, 5, 0), true, false, false)); + + bytes32 id = engine.bookId(address(token0), address(token1), 0); + uint256 exhaustedBranchSerial = uint256(type(uint32).max) << 64; + + vm.expectRevert(bytes4(keccak256("NonceExhausted()"))); + engine.restBookForTest(id, exhaustedBranchSerial | MAX_ORDER_NONCE, 11, 5, true, alice); + } + + function test_RestBookHarnessRejectsCollidingIdentityFronts() public { + vm.prank(alice); + engine.fill(_fill(0, _order(10, 5, 0), true, false, false)); + + bytes32 id = engine.bookId(address(token0), address(token1), 0); + // The branch and descending order fronts may not allocate the same identity. + uint256 collidingFronts = (uint256(127) << 64) | 127; + + vm.expectRevert(bytes4(keccak256("NonceExhausted()"))); + engine.restBookForTest(id, collidingFronts, 11, 5, true, alice); + } + + function test_RestBookHarnessRejectsOrderInsideAllocatedBranchRange() public { + bytes32 id = engine.bookId(address(token0), address(token1), 0); + uint256 crossedFronts = (uint256(3) << 64) | 2; + + vm.expectRevert(bytes4(keccak256("NonceExhausted()"))); + engine.restBookForTest(id, crossedFronts, 11, 5, true, alice); + } + + function test_RestBookHarnessPreservesFullWidthBranchIdentity() public { + vm.prank(alice); + engine.fill(_fill(0, _order(10, 5, 0), true, false, false)); + + bytes32 id = engine.bookId(address(token0), address(token1), 0); + uint32 branchIdentity = uint32(1) << 31; + uint256 fullWidthFronts = (uint256(branchIdentity) << 64) | MAX_ORDER_NONCE; + + engine.restBookForTest(id, fullWidthFronts, 11, 5, true, alice); + + (, bytes32 bidRoot) = engine.roots(address(token0), address(token1), 0); + assertEq(uint32(uint256(bidRoot)), branchIdentity); + assertEq(engine.nextNonce(address(token0), address(token1), 0), MAX_ORDER_NONCE - 1); + } + + function test_RestBookHarnessRotatesBeforeOrderFrontEntersAllocatedBranchRange() public { + vm.prank(alice); + engine.fill(_fill(0, _order(10, 5, 0), true, false, false)); + + bytes32 id = engine.bookId(address(token0), address(token1), 0); + // Branch serial 127 and order nonce 128 are adjacent, so this is the final safe rest. + uint256 adjacentFronts = (uint256(127) << 64) | 128; + + (bytes32 restingOrder, uint32 nextNonceAfter) = engine.restBookForTest(id, adjacentFronts, 11, 5, true, alice); + + assertEq(uint32(uint256(restingOrder)), 128); + assertEq(nextNonceAfter, 1); + assertEq(engine.nextNonce(address(token0), address(token1), 0), 1); + } + function testFuzz_IntegratorBidFeeEqualsIndependentProtocolFormula( uint128 quantitySeed, uint8 protocolBpsSeed, diff --git a/test/DeepstateV1Invariant.t.sol b/test/DeepstateV1Invariant.t.sol index 46999e2..e8f54e4 100644 --- a/test/DeepstateV1Invariant.t.sol +++ b/test/DeepstateV1Invariant.t.sol @@ -29,10 +29,16 @@ contract MultiPoolInvariantERC20 is ERC20 { } contract DeepstateV1MultiPoolHarness is DeepstateV1 { - function forceNextNonce(address token0, address token1, uint256 epoch, uint32 nonce) external { + function forceIdentityFrontsToExhaust(address token0, address token1, uint256 epoch, bool isBid) external { bytes32 id = bookId(token0, token1, epoch); uint256 nonceAndFlags = books[id].nonceAndFlags; - books[id].nonceAndFlags = (nonceAndFlags & ~uint256(type(uint32).max)) | uint256(nonce); + // forge-lint: disable-next-line(unsafe-typecast) + uint32 branchSerial = uint32(nonceAndFlags >> 64); + if (branchSerial == 0) branchSerial = 1; + Branch storage root = books[id].tree[bytes32(0)]; + bool createsBranch = isBid ? root.rightNode != bytes32(0) : root.leftNode != bytes32(0); + uint32 orderNonce = createsBranch ? branchSerial + 1 : (branchSerial > 1 ? branchSerial : 2); + books[id].nonceAndFlags = (nonceAndFlags & ~uint256(type(uint32).max)) | uint256(orderNonce); } } @@ -378,7 +384,7 @@ contract DeepstateV1MultiPoolHandler is Test { (address lower, address upper,,) = _pair(poolIndex); bytes32 oldBook = ENGINE.bookId(lower, upper, oldEpoch); - ENGINE.forceNextNonce(lower, upper, oldEpoch, 2); + ENGINE.forceIdentityFrontsToExhaust(lower, upper, oldEpoch, isBid); int32 tick = isBid ? type(int32).min : type(int32).max; DeepstateV1.FillParams memory params = _fillParams(poolIndex, oldEpoch, tick, 1, isBid, false); _performSingle(actorIndex, params); @@ -1338,8 +1344,8 @@ contract DeepstateV1MultiPoolInvariantTest is StdInvariant, Test { (bytes32 leftNode, bytes32 rightNode) = engine.tree(id, root); if (leftNode == bytes32(0)) return _sortKey(root, isBid) == targetKey ? root : bytes32(0); - uint64 leftKey = _sortKey(leftNode, isBid); - uint8 depth = _commonPrefix(leftKey, _sortKey(rightNode, isBid)); + uint64 leftKey = _nodeKey(id, leftNode, isBid); + uint8 depth = _commonPrefix(leftKey, _nodeKey(id, rightNode, isBid)); if (_commonPrefix(targetKey, leftKey) < depth) return bytes32(0); root = _bit(targetKey, depth) ? rightNode : leftNode; } @@ -1355,6 +1361,15 @@ contract DeepstateV1MultiPoolInvariantTest is StdInvariant, Test { return bytes32(0); } + function _nodeKey(bytes32 id, bytes32 node, bool isBid) private view returns (uint64) { + while (node != bytes32(0)) { + (bytes32 leftNode,) = engine.tree(id, node); + if (leftNode == bytes32(0)) return _sortKey(node, isBid); + node = leftNode; + } + return 0; + } + function _pair(uint8 poolIndex) private view returns (address lower, address upper) { if (poolIndex == 0) return (address(token0), address(token1)); if (poolIndex == 1) return (address(token1), address(token2)); diff --git a/test/DeepstateV1NativeETHInvariant.t.sol b/test/DeepstateV1NativeETHInvariant.t.sol index 787aad6..e64a137 100644 --- a/test/DeepstateV1NativeETHInvariant.t.sol +++ b/test/DeepstateV1NativeETHInvariant.t.sol @@ -639,14 +639,23 @@ contract DeepstateV1NativeETHInvariantTest is StdInvariant, Test { (bytes32 leftNode, bytes32 rightNode) = engine.tree(id, root); if (leftNode == bytes32(0)) return _sortKey(root, isBid) == targetKey ? root : bytes32(0); - uint64 leftKey = _sortKey(leftNode, isBid); - uint8 depth = _commonPrefix(leftKey, _sortKey(rightNode, isBid)); + uint64 leftKey = _nodeKey(id, leftNode, isBid); + uint8 depth = _commonPrefix(leftKey, _nodeKey(id, rightNode, isBid)); if (_commonPrefix(targetKey, leftKey) < depth) return bytes32(0); root = _bit(targetKey, depth) ? rightNode : leftNode; } return bytes32(0); } + function _nodeKey(bytes32 id, bytes32 node, bool isBid) private view returns (uint64) { + while (node != bytes32(0)) { + (bytes32 leftNode,) = engine.tree(id, node); + if (leftNode == bytes32(0)) return _sortKey(node, isBid); + node = leftNode; + } + return 0; + } + function _sortKey(bytes32 order, bool isBid) private pure returns (uint64) { uint32 tickKey = uint32(_tick(order)) ^ 0x80000000; if (!isBid) tickKey = type(uint32).max - tickKey; diff --git a/test/RadixMatchingEngine.t.sol b/test/RadixMatchingEngine.t.sol index 9ec926e..a51d7e3 100644 --- a/test/RadixMatchingEngine.t.sol +++ b/test/RadixMatchingEngine.t.sol @@ -127,6 +127,7 @@ contract ReentrantTransferFromERC20 is TestERC20 { contract RadixMatchingEngineTest is Test { uint32 internal constant MAX_ORDER_NONCE = type(uint32).max; + uint256 internal constant BRANCH_NONCE_SHIFT = 64; uint256 internal constant BID_RIGHT_SPINE_DIRTY = uint256(1) << 32; uint256 internal constant ASK_RIGHT_SPINE_DIRTY = uint256(1) << 33; @@ -410,6 +411,11 @@ contract RadixMatchingEngineTest is Test { ); assertTrue(engine.isBidOrder(firstBid), "bid side"); _assertTreeBranchStorage(bidBranch, leftNode, rightNode, "bid"); + assertEq( + engine.branchDepth(bidBranch), + _commonPrefix(_nodeKey(leftNode, true), _nodeKey(rightNode, true)), + "bid branch depth" + ); } { @@ -428,6 +434,11 @@ contract RadixMatchingEngineTest is Test { ); assertFalse(engine.isBidOrder(firstAsk), "ask side"); _assertTreeBranchStorage(askBranch, leftNode, rightNode, "ask"); + assertEq( + engine.branchDepth(askBranch), + _commonPrefix(_nodeKey(leftNode, false), _nodeKey(rightNode, false)), + "ask branch depth" + ); } } @@ -1641,8 +1652,10 @@ contract RadixMatchingEngineTest is Test { bytes32 finalSplit = _branchFor(firstBid, secondBid, true); (bytes32 leftNode, bytes32 rightNode) = engine.tree(finalSplit); - assertEq(_pathKey(finalSplit), _pathKey(firstBid)); + assertEq(_nonce(finalSplit), 1); + assertTrue(_nonce(finalSplit) != _nonce(firstBid)); assertEq(_quantity(finalSplit), _quantity(firstBid) + _quantity(secondBid)); + assertEq(engine.branchDepth(finalSplit), 63); assertEq(leftNode, secondBid); assertEq(rightNode, firstBid); } @@ -1665,8 +1678,10 @@ contract RadixMatchingEngineTest is Test { bytes32 finalSplit = _branchFor(firstAsk, secondAsk, false); (bytes32 leftNode, bytes32 rightNode) = engine.tree(finalSplit); - assertEq(_pathKey(finalSplit), _pathKey(firstAsk)); + assertEq(_nonce(finalSplit), 1); + assertTrue(_nonce(finalSplit) != _nonce(firstAsk)); assertEq(_quantity(finalSplit), _quantity(firstAsk) + _quantity(secondAsk)); + assertEq(engine.branchDepth(finalSplit), 63); assertEq(leftNode, secondAsk); assertEq(rightNode, firstAsk); } @@ -1686,7 +1701,8 @@ contract RadixMatchingEngineTest is Test { bytes32 reusedChild = _branchFor(firstBid, thirdBid, true); bytes32 newRoot = _branchFor(secondBid, reusedChild, true); - assertEq(reusedChild, oldRoot); + assertTrue(_nonce(reusedChild) != _nonce(oldRoot)); + assertEq(_nonce(newRoot), _nonce(oldRoot)); assertEq(engine.bidRoot(), newRoot); (bytes32 rootLeft, bytes32 rootRight) = engine.tree(newRoot); @@ -1715,7 +1731,8 @@ contract RadixMatchingEngineTest is Test { (bytes32 expectedRootLeft, bytes32 expectedRootRight) = _expectedBranchChildren(secondAsk, reusedChild, false); (bytes32 expectedChildLeft, bytes32 expectedChildRight) = _expectedBranchChildren(firstAsk, thirdAsk, false); - assertEq(reusedChild, oldRoot); + assertTrue(_nonce(reusedChild) != _nonce(oldRoot)); + assertEq(_nonce(newRoot), _nonce(oldRoot)); assertEq(engine.askRoot(), newRoot); (bytes32 rootLeft, bytes32 rootRight) = engine.tree(newRoot); @@ -1786,19 +1803,16 @@ contract RadixMatchingEngineTest is Test { function test_FullDepthBidNonceCombFullyMatchesAndClaims() public { uint256 orderCount = 65; uint160 matchQuantity = 65; - uint64 targetKey = type(uint64).max; (bytes32[] memory orders, uint256 quoteTotal) = _buildFullDepthBidNonceComb(); bytes32 node = engine.bidRoot(); for (uint256 depth; depth < 64; ++depth) { (bytes32 leftNode, bytes32 rightNode) = engine.tree(node); - uint64 siblingKey = targetKey ^ uint64(uint256(1) << (63 - depth)); - assertTrue(leftNode != bytes32(0)); assertTrue(rightNode != bytes32(0)); - assertEq(_commonPrefix(_pathKey(leftNode), _pathKey(rightNode)), depth); - assertEq(_pathKey(leftNode), siblingKey); - assertEq(_pathKey(rightNode), targetKey); + assertEq(_commonPrefix(_nodeKey(leftNode, true), _nodeKey(rightNode, true)), depth); + assertEq(leftNode, orders[depth + 1]); + assertEq(_rightmostLeaf(rightNode), orders[0]); // casting to uint160 is safe because the synthetic comb has 65 orders. // forge-lint: disable-next-line(unsafe-typecast) assertEq(_quantity(rightNode), uint160(orderCount - depth - 1)); @@ -1861,7 +1875,7 @@ contract RadixMatchingEngineTest is Test { assertTrue(leftNode != bytes32(0)); assertTrue(rightNode != bytes32(0)); - assertEq(_commonPrefix(_askSortKey(leftNode), _askSortKey(rightNode)), depth); + assertEq(_commonPrefix(_nodeKey(leftNode, false), _nodeKey(rightNode, false)), depth); node = rightNode; } @@ -2493,7 +2507,7 @@ contract RadixMatchingEngineTest is Test { assertEq(_subtreeQuantity(engine.askRoot()), 4); bytes32 dirtyAggregate = - bytes32(uint256(_order(price, 4, _nonce(firstAsk))) | (uint256(_correctionCode(aggregate)) << 32)); + bytes32(uint256(_order(price, 4, _nonce(aggregate))) | (uint256(_correctionCode(aggregate)) << 32)); vm.expectEmit(false, false, false, true, address(engine)); emit AskMatched( _bookId(), @@ -2537,7 +2551,7 @@ contract RadixMatchingEngineTest is Test { assertEq(_subtreeQuantity(engine.bidRoot()), 4); bytes32 dirtyAggregate = - bytes32(uint256(_order(price, 4, _nonce(firstBid))) | (uint256(_correctionCode(aggregate)) << 32)); + bytes32(uint256(_order(price, 4, _nonce(aggregate))) | (uint256(_correctionCode(aggregate)) << 32)); vm.expectEmit(false, false, false, true, address(engine)); emit BidMatched( _bookId(), @@ -2580,7 +2594,8 @@ contract RadixMatchingEngineTest is Test { assertEq(_quantity(engine.askRoot()), 5); assertEq(_subtreeQuantity(engine.askRoot()), 4); assertEq( - uint256(vm.load(address(engine), _nextNonceSlot())), (uint256(MAX_ORDER_NONCE) - 2) | ASK_RIGHT_SPINE_DIRTY + uint256(vm.load(address(engine), _nextNonceSlot())), + (uint256(2) << BRANCH_NONCE_SHIFT) | (uint256(MAX_ORDER_NONCE) - 2) | ASK_RIGHT_SPINE_DIRTY ); _expectAskMatches( @@ -2626,7 +2641,8 @@ contract RadixMatchingEngineTest is Test { assertEq(_quantity(engine.bidRoot()), 5); assertEq(_subtreeQuantity(engine.bidRoot()), 4); assertEq( - uint256(vm.load(address(engine), _nextNonceSlot())), (uint256(MAX_ORDER_NONCE) - 2) | BID_RIGHT_SPINE_DIRTY + uint256(vm.load(address(engine), _nextNonceSlot())), + (uint256(2) << BRANCH_NONCE_SHIFT) | (uint256(MAX_ORDER_NONCE) - 2) | BID_RIGHT_SPINE_DIRTY ); _expectBidMatches( @@ -2677,13 +2693,15 @@ contract RadixMatchingEngineTest is Test { assertEq(base.balanceOf(address(engine)), 0); } - function test_CorruptedRightSpineBranchNodeAliasRecomputesBranch() public { + function test_StableBranchIdentityCannotAliasPartiallyFilledLeaf() public { int32 price = 50; bytes32 leftAsk = _order(price, 2, MAX_ORDER_NONCE - 1); bytes32 rightAsk = _order(price, 5, MAX_ORDER_NONCE); - bytes32 root = _order(price, 4, MAX_ORDER_NONCE); + bytes32 root = bytes32(uint256(_order(price, 4, 1)) | (uint256(1) << 32)); - vm.store(address(engine), _nextNonceSlot(), bytes32(uint256(MAX_ORDER_NONCE))); + vm.store( + address(engine), _nextNonceSlot(), bytes32((uint256(2) << BRANCH_NONCE_SHIFT) | uint256(MAX_ORDER_NONCE)) + ); _storeTreeBranch(root, leftAsk, rightAsk); vm.store(address(engine), _askRootSlot(), root); base.mint(address(engine), 1); @@ -2691,10 +2709,11 @@ contract RadixMatchingEngineTest is Test { vm.prank(carol); bytes32 restingBid = engine.fill(_order(price, 1, 0), true); - bytes32 expectedRoot = _branchFor(leftAsk, root, false); + bytes32 reducedRightAsk = _order(price, 4, _nonce(rightAsk)); assertEq(restingBid, bytes32(0)); - assertEq(engine.askRoot(), expectedRoot); - _assertTreeBranchStorage(expectedRoot, leftAsk, root, "alias-recomputed"); + assertEq(engine.askRoot(), root); + _assertTreeBranchStorage(root, leftAsk, reducedRightAsk, "stable-identity"); + assertEq(_subtreeQuantity(root), 6); assertEq(base.balanceOf(carol), 1_000_001); assertEq(quote.balanceOf(address(engine)), _quoteValue(price, 1, false)); } @@ -2703,7 +2722,7 @@ contract RadixMatchingEngineTest is Test { int32 price = 51; bytes32 leftAsk = _order(price, 4, MAX_ORDER_NONCE - 1); bytes32 rightAsk = _order(price, 5, MAX_ORDER_NONCE - 1); - bytes32 root = _order(price, 6, MAX_ORDER_NONCE); + bytes32 root = bytes32(uint256(_order(price, 6, MAX_ORDER_NONCE)) | (uint256(1) << 32)); vm.store(address(engine), _nextNonceSlot(), bytes32(uint256(MAX_ORDER_NONCE))); _storeTreeBranch(root, leftAsk, rightAsk); @@ -2740,7 +2759,8 @@ contract RadixMatchingEngineTest is Test { assertEq(_subtreeQuantity(engine.askRoot()), 4); assertEq(uint256(engine.nextNonce()), uint256(MAX_ORDER_NONCE) - 2); assertEq( - uint256(vm.load(address(engine), _nextNonceSlot())), (uint256(MAX_ORDER_NONCE) - 2) | ASK_RIGHT_SPINE_DIRTY + uint256(vm.load(address(engine), _nextNonceSlot())), + (uint256(2) << BRANCH_NONCE_SHIFT) | (uint256(MAX_ORDER_NONCE) - 2) | ASK_RIGHT_SPINE_DIRTY ); vm.prank(alice); @@ -2752,7 +2772,8 @@ contract RadixMatchingEngineTest is Test { assertEq(_subtreeQuantity(engine.askRoot()), 4); assertEq(uint256(engine.nextNonce()), uint256(MAX_ORDER_NONCE) - 3); assertEq( - uint256(vm.load(address(engine), _nextNonceSlot())), (uint256(MAX_ORDER_NONCE) - 3) | ASK_RIGHT_SPINE_DIRTY + uint256(vm.load(address(engine), _nextNonceSlot())), + (uint256(2) << BRANCH_NONCE_SHIFT) | (uint256(MAX_ORDER_NONCE) - 3) | ASK_RIGHT_SPINE_DIRTY ); vm.prank(carol); @@ -2762,7 +2783,10 @@ contract RadixMatchingEngineTest is Test { assertEq(_quantity(engine.askRoot()), 5); assertEq(_subtreeQuantity(engine.askRoot()), 5); assertEq(uint256(engine.nextNonce()), uint256(MAX_ORDER_NONCE) - 4); - assertEq(uint256(vm.load(address(engine), _nextNonceSlot())), uint256(MAX_ORDER_NONCE) - 4); + assertEq( + uint256(vm.load(address(engine), _nextNonceSlot())), + (uint256(3) << BRANCH_NONCE_SHIFT) | (uint256(MAX_ORDER_NONCE) - 4) + ); vm.prank(alice); (uint256 firstBaseAmount, uint256 firstQuoteAmount) = engine.cancel(firstAsk); @@ -2804,7 +2828,8 @@ contract RadixMatchingEngineTest is Test { assertEq(_subtreeQuantity(engine.bidRoot()), 4); assertEq(uint256(engine.nextNonce()), uint256(MAX_ORDER_NONCE) - 2); assertEq( - uint256(vm.load(address(engine), _nextNonceSlot())), (uint256(MAX_ORDER_NONCE) - 2) | BID_RIGHT_SPINE_DIRTY + uint256(vm.load(address(engine), _nextNonceSlot())), + (uint256(2) << BRANCH_NONCE_SHIFT) | (uint256(MAX_ORDER_NONCE) - 2) | BID_RIGHT_SPINE_DIRTY ); vm.prank(carol); @@ -2816,7 +2841,8 @@ contract RadixMatchingEngineTest is Test { assertEq(_subtreeQuantity(engine.bidRoot()), 4); assertEq(uint256(engine.nextNonce()), uint256(MAX_ORDER_NONCE) - 3); assertEq( - uint256(vm.load(address(engine), _nextNonceSlot())), (uint256(MAX_ORDER_NONCE) - 3) | BID_RIGHT_SPINE_DIRTY + uint256(vm.load(address(engine), _nextNonceSlot())), + (uint256(2) << BRANCH_NONCE_SHIFT) | (uint256(MAX_ORDER_NONCE) - 3) | BID_RIGHT_SPINE_DIRTY ); vm.prank(alice); @@ -2826,7 +2852,10 @@ contract RadixMatchingEngineTest is Test { assertEq(_quantity(engine.bidRoot()), 5); assertEq(_subtreeQuantity(engine.bidRoot()), 5); assertEq(uint256(engine.nextNonce()), uint256(MAX_ORDER_NONCE) - 4); - assertEq(uint256(vm.load(address(engine), _nextNonceSlot())), uint256(MAX_ORDER_NONCE) - 4); + assertEq( + uint256(vm.load(address(engine), _nextNonceSlot())), + (uint256(3) << BRANCH_NONCE_SHIFT) | (uint256(MAX_ORDER_NONCE) - 4) + ); vm.prank(alice); (uint256 firstBaseAmount, uint256 firstQuoteAmount) = engine.cancel(firstBid); @@ -3977,7 +4006,7 @@ contract RadixMatchingEngineTest is Test { quote.mint(alice, uint256(1) << 200); - vm.store(address(engine), _nextNonceSlot(), bytes32(uint256(MAX_ORDER_NONCE))); + _forceNextOrderNonce(MAX_ORDER_NONCE); vm.prank(alice); orders[0] = engine.fill(_order(type(int32).max, 1, 0), true); @@ -3989,13 +4018,13 @@ contract RadixMatchingEngineTest is Test { // forge-lint: disable-next-line(unsafe-typecast) int32 price = int32(uint32(siblingKey >> 32) ^ 0x80000000); // forge-lint: disable-next-line(unsafe-typecast) - uint32 nonce = uint32(siblingKey); + uint32 nonce = depth < 32 ? MAX_ORDER_NONCE - uint32(depth) - 1 : uint32(siblingKey); - vm.store(address(engine), _nextNonceSlot(), bytes32(uint256(nonce))); + _forceNextOrderNonce(nonce); vm.prank(alice); orders[depth + 1] = engine.fill(_order(price, 1, 0), true); - assertEq(_pathKey(orders[depth + 1]), siblingKey); + assertEq(_commonPrefix(_pathKey(orders[depth + 1]), targetKey), depth); quoteTotal += _quoteValue(price, 1, true); } } @@ -4005,7 +4034,7 @@ contract RadixMatchingEngineTest is Test { uint64 targetSortKey = type(uint64).max; orders = new bytes32[](orderCount); - vm.store(address(engine), _nextNonceSlot(), bytes32(uint256(MAX_ORDER_NONCE))); + _forceNextOrderNonce(MAX_ORDER_NONCE); vm.prank(alice); orders[0] = engine.fill(_order(type(int32).min, 1, 0), false); @@ -4016,13 +4045,13 @@ contract RadixMatchingEngineTest is Test { uint64 sortKey = targetSortKey ^ uint64(uint256(1) << (63 - depth)); uint32 sortableTick = type(uint32).max - uint32(sortKey >> 32); int32 price = int32(sortableTick ^ 0x80000000); - uint32 nonce = uint32(sortKey); + uint32 nonce = depth < 32 ? MAX_ORDER_NONCE - uint32(depth) - 1 : uint32(sortKey); - vm.store(address(engine), _nextNonceSlot(), bytes32(uint256(nonce))); + _forceNextOrderNonce(nonce); vm.prank(alice); orders[depth + 1] = engine.fill(_order(price, 1, 0), false); - assertEq(_askSortKey(orders[depth + 1]), sortKey); + assertEq(_commonPrefix(_askSortKey(orders[depth + 1]), targetSortKey), depth); quoteTotal += _quoteValue(price, 1, false); } } @@ -4090,7 +4119,8 @@ contract RadixMatchingEngineTest is Test { } function _treeSlot(bytes32 node) internal view returns (bytes32) { - return keccak256(abi.encode(node, _treeMappingSlot())); + bytes32 key = node == bytes32(0) ? bytes32(0) : bytes32(uint256(_nonce(node))); + return keccak256(abi.encode(key, _treeMappingSlot())); } function _assertTreeBranchStorage(bytes32 branch, bytes32 leftNode, bytes32 rightNode, string memory label) @@ -4098,7 +4128,11 @@ contract RadixMatchingEngineTest is Test { view { bytes32 branchSlot = _treeSlot(branch); - assertEq(vm.load(address(engine), branchSlot), leftNode, string.concat(label, " tree left slot")); + assertEq( + vm.load(address(engine), branchSlot), + _storedLeftNode(branch, leftNode, rightNode), + string.concat(label, " tree left slot") + ); assertEq( vm.load(address(engine), bytes32(uint256(branchSlot) + 1)), rightNode, @@ -4108,17 +4142,25 @@ contract RadixMatchingEngineTest is Test { function _storeTreeBranch(bytes32 branch, bytes32 leftNode, bytes32 rightNode) internal { bytes32 branchSlot = _treeSlot(branch); - vm.store(address(engine), branchSlot, leftNode); + vm.store(address(engine), branchSlot, _storedLeftNode(branch, leftNode, rightNode)); vm.store(address(engine), bytes32(uint256(branchSlot) + 1), rightNode); } + function _storedLeftNode(bytes32 branch, bytes32 leftNode, bytes32 rightNode) internal view returns (bytes32) { + if (_correctionCode(branch) == 0) return leftNode; + uint8 depth = _commonPrefix(_nodeKey(leftNode, true), _nodeKey(rightNode, true)); + if (depth == 64) depth = 63; + uint256 depthMask = uint256(0x1f) << 251; + return bytes32((uint256(leftNode) & ~depthMask) | (uint256(depth - 32) << 251)); + } + function _expectedBranchChildren(bytes32 a, bytes32 b, bool isBid) internal - pure + view returns (bytes32 left, bytes32 right) { - uint64 aKey = isBid ? _pathKey(a) : _askSortKey(a); - uint64 bKey = isBid ? _pathKey(b) : _askSortKey(b); + uint64 aKey = _nodeKey(a, isBid); + uint64 bKey = _nodeKey(b, isBid); uint8 branchDepth = _commonPrefix(aKey, bKey); left = a; @@ -4141,15 +4183,24 @@ contract RadixMatchingEngineTest is Test { return _bookSlot(); } - function _branchFor(bytes32 a, bytes32 b, bool isBid) internal view returns (bytes32 branch) { - uint64 aKey = _pathKey(a); - uint64 bKey = _pathKey(b); - uint64 boundaryKey = aKey > bKey ? aKey : bKey; + function _forceNextOrderNonce(uint32 nonce) internal { + bytes32 slot = _nextNonceSlot(); + uint256 nonceAndFlags = uint256(vm.load(address(engine), slot)); + vm.store(address(engine), slot, bytes32((nonceAndFlags & ~uint256(type(uint32).max)) | nonce)); + } - int32 prefixPrice = int32(uint32(boundaryKey >> 32) ^ 0x80000000); - uint32 prefixNonce = uint32(boundaryKey); + function _branchFor(bytes32 a, bytes32 b, bool isBid) internal view returns (bytes32 branch) { + (bool found, bytes32 existing) = _findBranch(engine.askRoot(), a, b, 0); + if (!found) (found, existing) = _findBranch(engine.bidRoot(), a, b, 0); + if (found) return existing; + + (bytes32 left,) = _expectedBranchChildren(a, b, isBid); + int32 prefixPrice = _price(_leftmostLeaf(left)); + (bytes32 aLeft,) = engine.tree(a); + (bytes32 bLeft,) = engine.tree(b); + uint32 branchNonce = aLeft == bytes32(0) && bLeft == bytes32(0) ? 1 : 2; uint160 quantity = _quantity(a) + _quantity(b); - branch = _order(prefixPrice, quantity, prefixNonce); + branch = _order(prefixPrice, quantity, branchNonce); if (_price(a) == _price(b) && _uniformNode(a) && _uniformNode(b)) { uint256 childQuote = _uniformQuote(a, isBid) + _uniformQuote(b, isBid); @@ -4159,6 +4210,37 @@ contract RadixMatchingEngineTest is Test { } } + function _findBranch(bytes32 node, bytes32 a, bytes32 b, uint256 depth) + internal + view + returns (bool found, bytes32 branch) + { + if (node == bytes32(0) || depth > 64) return (false, bytes32(0)); + (bytes32 left, bytes32 right) = engine.tree(node); + if (left == bytes32(0)) return (false, bytes32(0)); + if ((left == a && right == b) || (left == b && right == a)) return (true, node); + if (left != node) { + (found, branch) = _findBranch(left, a, b, depth + 1); + if (found) return (found, branch); + } + if (right != node && right != left) return _findBranch(right, a, b, depth + 1); + return (false, bytes32(0)); + } + + function _leftmostLeaf(bytes32 node) internal view returns (bytes32 leaf) { + leaf = node; + for (uint256 depth; depth < 64; ++depth) { + (bytes32 left,) = engine.tree(leaf); + if (left == bytes32(0) || left == leaf) return leaf; + leaf = left; + } + } + + function _nodeKey(bytes32 node, bool isBid) internal view returns (uint64) { + bytes32 leaf = _leftmostLeaf(node); + return isBid ? _pathKey(leaf) : _askSortKey(leaf); + } + function _uniformNode(bytes32 node) internal view returns (bool) { (bytes32 leftNode,) = engine.tree(node); return leftNode == bytes32(0) || _correctionCode(node) != 0; diff --git a/test/RadixMatchingEngineCoverage.t.sol b/test/RadixMatchingEngineCoverage.t.sol index 5f2c253..e2011b1 100644 --- a/test/RadixMatchingEngineCoverage.t.sol +++ b/test/RadixMatchingEngineCoverage.t.sol @@ -50,6 +50,7 @@ contract CoverageHook { contract RadixMatchingEngineCoverageTest is Test { uint32 internal constant MAX_ORDER_NONCE = type(uint32).max; + uint256 internal constant BRANCH_NONCE_SHIFT = 64; uint256 internal constant BID_RIGHT_SPINE_DIRTY = uint256(1) << 32; uint256 internal constant ASK_RIGHT_SPINE_DIRTY = uint256(1) << 33; @@ -866,14 +867,18 @@ contract RadixMatchingEngineCoverageTest is Test { assertEq(engine.askRoot(), askAnchor); assertEq( - uint256(vm.load(address(engine), _nextNonceSlot())), (uint256(MAX_ORDER_NONCE) - 2) | ASK_RIGHT_SPINE_DIRTY + uint256(vm.load(address(engine), _nextNonceSlot())), + (uint256(2) << BRANCH_NONCE_SHIFT) | (uint256(MAX_ORDER_NONCE) - 2) | ASK_RIGHT_SPINE_DIRTY ); vm.prank(carol); bytes32 thirdAsk = engine.fill(_order(61, 1, 0), false); assertTrue(thirdAsk != bytes32(0)); - assertEq(uint256(vm.load(address(engine), _nextNonceSlot())), uint256(MAX_ORDER_NONCE) - 3); + assertEq( + uint256(vm.load(address(engine), _nextNonceSlot())), + (uint256(3) << BRANCH_NONCE_SHIFT) | (uint256(MAX_ORDER_NONCE) - 3) + ); assertEq(_subtreeQuantity(engine.askRoot()), 5); } @@ -889,14 +894,18 @@ contract RadixMatchingEngineCoverageTest is Test { assertEq(engine.bidRoot(), bidAnchor); assertEq( - uint256(vm.load(address(engine), _nextNonceSlot())), (uint256(MAX_ORDER_NONCE) - 2) | BID_RIGHT_SPINE_DIRTY + uint256(vm.load(address(engine), _nextNonceSlot())), + (uint256(2) << BRANCH_NONCE_SHIFT) | (uint256(MAX_ORDER_NONCE) - 2) | BID_RIGHT_SPINE_DIRTY ); vm.prank(carol); bytes32 thirdBid = engine.fill(_order(79, 1, 0), true); assertTrue(thirdBid != bytes32(0)); - assertEq(uint256(vm.load(address(engine), _nextNonceSlot())), uint256(MAX_ORDER_NONCE) - 3); + assertEq( + uint256(vm.load(address(engine), _nextNonceSlot())), + (uint256(3) << BRANCH_NONCE_SHIFT) | (uint256(MAX_ORDER_NONCE) - 3) + ); assertEq(_subtreeQuantity(engine.bidRoot()), 5); } @@ -936,13 +945,15 @@ contract RadixMatchingEngineCoverageTest is Test { engine.fill(_order(79, 4, 0), false); } - function testCoverage_CorruptedAliasRightSpineRecomputesBranch() public { + function testCoverage_StableIdentityPreventsRightSpineLeafAlias() public { int32 price = 50; bytes32 leftAsk = _order(price, 2, MAX_ORDER_NONCE - 1); bytes32 rightAsk = _order(price, 5, MAX_ORDER_NONCE); - bytes32 root = _order(price, 4, MAX_ORDER_NONCE); + bytes32 root = bytes32(uint256(_order(price, 4, 1)) | (uint256(1) << 32)); - vm.store(address(engine), _nextNonceSlot(), bytes32(uint256(MAX_ORDER_NONCE))); + vm.store( + address(engine), _nextNonceSlot(), bytes32((uint256(2) << BRANCH_NONCE_SHIFT) | uint256(MAX_ORDER_NONCE)) + ); _storeTreeBranch(root, leftAsk, rightAsk); vm.store(address(engine), _askRootSlot(), root); base.mint(address(engine), 1); @@ -950,9 +961,9 @@ contract RadixMatchingEngineCoverageTest is Test { vm.prank(carol); engine.fill(_order(price, 1, 0), true); - bytes32 expectedRoot = _branchFor(leftAsk, root, false); - assertEq(engine.askRoot(), expectedRoot); - _assertTreeBranchStorage(expectedRoot, leftAsk, root); + bytes32 reducedRightAsk = _order(price, 4, _nonce(rightAsk)); + assertEq(engine.askRoot(), root); + _assertTreeBranchStorage(root, leftAsk, reducedRightAsk); } function testCoverage_DirtySpineRejectsMixedLeftSubtreeAsUniform() public { @@ -975,6 +986,31 @@ contract RadixMatchingEngineCoverageTest is Test { assertEq(_subtreeQuantity(engine.askRoot()), 2); } + function testCoverage_DirtySpinePricesUniformLeftBranchWithoutSentinelLoad() public { + bytes32 newestAsk = _order(50, 1, MAX_ORDER_NONCE - 2); + bytes32 middleAsk = _order(50, 1, MAX_ORDER_NONCE - 1); + bytes32 oldestAsk = _order(50, 1, MAX_ORDER_NONCE); + + bytes32 uniformLeft = _branchFor(newestAsk, middleAsk, false); + (bytes32 uniformLeftChild, bytes32 uniformRightChild) = _expectedBranchChildren(newestAsk, middleAsk, false); + _storeTreeBranch(uniformLeft, uniformLeftChild, uniformRightChild); + + bytes32 root = _branchFor(uniformLeft, oldestAsk, false); + (bytes32 rootLeft, bytes32 rootRight) = _expectedBranchChildren(uniformLeft, oldestAsk, false); + assertEq(rootLeft, uniformLeft); + assertEq(rootRight, oldestAsk); + _storeTreeBranch(root, rootLeft, rootRight); + vm.store(address(engine), _askRootSlot(), root); + vm.store(address(engine), _nextNonceSlot(), bytes32(uint256(MAX_ORDER_NONCE) | ASK_RIGHT_SPINE_DIRTY)); + base.mint(address(engine), 3); + + vm.prank(carol); + engine.fill(_order(50, 3, 0), true); + + assertEq(engine.askRoot(), bytes32(0)); + assertEq(base.balanceOf(carol), 1_000_000_003); + } + function testCoverage_DirtySpineRejectsNonuniformRightSubtree() public { bytes32 lowAsk = _order(49, 1, MAX_ORDER_NONCE - 2); bytes32 highAsk = _order(50, 1, MAX_ORDER_NONCE - 1); @@ -1026,7 +1062,8 @@ contract RadixMatchingEngineCoverageTest is Test { } function _treeSlot(bytes32 node) internal view returns (bytes32) { - return keccak256(abi.encode(node, _treeMappingSlot())); + bytes32 key = node == bytes32(0) ? bytes32(0) : bytes32(uint256(_nonce(node))); + return keccak256(abi.encode(key, _treeMappingSlot())); } function _askRootSlot() internal view returns (bytes32) { @@ -1043,16 +1080,24 @@ contract RadixMatchingEngineCoverageTest is Test { function _storeTreeBranch(bytes32 branch, bytes32 leftNode, bytes32 rightNode) internal { bytes32 branchSlot = _treeSlot(branch); - vm.store(address(engine), branchSlot, leftNode); + vm.store(address(engine), branchSlot, _storedLeftNode(branch, leftNode, rightNode)); vm.store(address(engine), bytes32(uint256(branchSlot) + 1), rightNode); } function _assertTreeBranchStorage(bytes32 branch, bytes32 leftNode, bytes32 rightNode) internal view { bytes32 branchSlot = _treeSlot(branch); - assertEq(vm.load(address(engine), branchSlot), leftNode); + assertEq(vm.load(address(engine), branchSlot), _storedLeftNode(branch, leftNode, rightNode)); assertEq(vm.load(address(engine), bytes32(uint256(branchSlot) + 1)), rightNode); } + function _storedLeftNode(bytes32 branch, bytes32 leftNode, bytes32 rightNode) internal view returns (bytes32) { + if (_correctionCode(branch) == 0) return leftNode; + uint8 depth = _commonPrefix(_nodeKey(leftNode, true), _nodeKey(rightNode, true)); + if (depth == 64) depth = 63; + uint256 depthMask = uint256(0x1f) << 251; + return bytes32((uint256(leftNode) & ~depthMask) | (uint256(depth - 32) << 251)); + } + function _branchLeft(bytes32 branch) internal view returns (bytes32 leftNode) { (leftNode,) = engine.tree(branch); } @@ -1062,14 +1107,15 @@ contract RadixMatchingEngineCoverageTest is Test { } function _branchFor(bytes32 a, bytes32 b, bool isBid) internal view returns (bytes32) { - uint64 aKey = _pathKey(a); - uint64 bKey = _pathKey(b); - uint64 boundaryKey = aKey > bKey ? aKey : bKey; - - // forge-lint: disable-next-line(unsafe-typecast) - int32 prefixPrice = int32(uint32(boundaryKey >> 32) ^ 0x80000000); - // forge-lint: disable-next-line(unsafe-typecast) - uint32 prefixNonce = uint32(boundaryKey); + (bool found, bytes32 existing) = _findBranch(engine.askRoot(), a, b, 0); + if (!found) (found, existing) = _findBranch(engine.bidRoot(), a, b, 0); + if (found) return existing; + + (bytes32 left,) = _expectedBranchChildren(a, b, isBid); + int32 prefixPrice = _price(_leftmostLeaf(left)); + (bytes32 aLeft,) = engine.tree(a); + (bytes32 bLeft,) = engine.tree(b); + uint32 branchNonce = aLeft == bytes32(0) && bLeft == bytes32(0) ? 1 : 2; uint160 quantity = _quantity(a) + _quantity(b); uint32 correctionCode; if (_price(a) == _price(b) && _uniformNode(a) && _uniformNode(b)) { @@ -1080,10 +1126,54 @@ contract RadixMatchingEngineCoverageTest is Test { } return bytes32( (uint256(uint32(prefixPrice)) << 224) | (uint256(quantity) << 64) | (uint256(correctionCode) << 32) - | uint256(prefixNonce) + | uint256(branchNonce) ); } + function _expectedBranchChildren(bytes32 a, bytes32 b, bool isBid) + internal + view + returns (bytes32 left, bytes32 right) + { + uint64 aKey = _nodeKey(a, isBid); + uint64 bKey = _nodeKey(b, isBid); + uint8 branchDepth = _commonPrefix(aKey, bKey); + left = a; + right = b; + if (_bit(aKey, branchDepth)) (left, right) = (b, a); + } + + function _findBranch(bytes32 node, bytes32 a, bytes32 b, uint256 depth) + internal + view + returns (bool found, bytes32 branch) + { + if (node == bytes32(0) || depth > 64) return (false, bytes32(0)); + (bytes32 left, bytes32 right) = engine.tree(node); + if (left == bytes32(0)) return (false, bytes32(0)); + if ((left == a && right == b) || (left == b && right == a)) return (true, node); + if (left != node) { + (found, branch) = _findBranch(left, a, b, depth + 1); + if (found) return (found, branch); + } + if (right != node && right != left) return _findBranch(right, a, b, depth + 1); + return (false, bytes32(0)); + } + + function _leftmostLeaf(bytes32 node) internal view returns (bytes32 leaf) { + leaf = node; + for (uint256 depth; depth < 64; ++depth) { + (bytes32 left,) = engine.tree(leaf); + if (left == bytes32(0) || left == leaf) return leaf; + leaf = left; + } + } + + function _nodeKey(bytes32 node, bool isBid) internal view returns (uint64) { + bytes32 leaf = _leftmostLeaf(node); + return isBid ? _pathKey(leaf) : _askSortKey(leaf); + } + function _uniformNode(bytes32 node) internal view returns (bool) { (bytes32 leftNode,) = engine.tree(node); return leftNode == bytes32(0) || _correctionCode(node) != 0; diff --git a/test/RadixMatchingEngineGas.t.sol b/test/RadixMatchingEngineGas.t.sol index b825e31..9147a14 100644 --- a/test/RadixMatchingEngineGas.t.sol +++ b/test/RadixMatchingEngineGas.t.sol @@ -33,8 +33,6 @@ contract RadixMatchingEngineGasTest is Test { int32 internal constant LARGE_ASK_BASE_PRICE = 2_000_000; int32 internal constant LARGE_REST_BID_PRICE = 1_750_000; int32 internal constant LARGE_REST_ASK_PRICE = 1_750_001; - uint256 internal constant BOOK_HOOK_TOKEN0_ACTIVE = uint256(1) << 34; - uint256 internal constant BOOK_HOOK_TOKEN1_ACTIVE = uint256(1) << 35; struct LargeRandomBook { bytes32 bestBid; @@ -162,6 +160,40 @@ contract RadixMatchingEngineGasTest is Test { vm.resumeGasMetering(); } + function testGas_FillBidPartiallyMatchesOffSpineAskBranch() public { + vm.pauseGasMetering(); + for (uint256 i; i < 4; ++i) { + vm.prank(bob); + _fill(_order(90, 10, 0), false); + } + + vm.prank(alice); + vm.resumeGasMetering(); + bytes32 restingBid = _fill(_order(90, 25, 0), true); + vm.pauseGasMetering(); + + assertEq(restingBid, bytes32(0)); + assertEq(_quantity(_askRoot()), 15); + vm.resumeGasMetering(); + } + + function testGas_FillAskPartiallyMatchesOffSpineBidBranch() public { + vm.pauseGasMetering(); + for (uint256 i; i < 4; ++i) { + vm.prank(bob); + _fill(_order(90, 10, 0), true); + } + + vm.prank(alice); + vm.resumeGasMetering(); + bytes32 restingAsk = _fill(_order(90, 25, 0), false); + vm.pauseGasMetering(); + + assertEq(restingAsk, bytes32(0)); + assertEq(_quantity(_bidRoot()), 15); + vm.resumeGasMetering(); + } + function testGas_FillBidConsumesAskAndRestsRemainder() public { vm.pauseGasMetering(); vm.prank(bob); @@ -367,6 +399,23 @@ contract RadixMatchingEngineGasTest is Test { vm.resumeGasMetering(); } + function testGas_PathologicalOffSpinePartialFillBidComb() public { + vm.pauseGasMetering(); + _buildOffSpineBidNonceComb(); + + address seller = address(0x5E11E2); + _fundAndApprove(seller); + + vm.prank(seller); + vm.resumeGasMetering(); + bytes32 restingAsk = _fill(_order(type(int32).min, 2, 0), false); + vm.pauseGasMetering(); + + assertEq(restingAsk, bytes32(0)); + assertEq(_subtreeQuantity(_bidRoot()), 135); + vm.resumeGasMetering(); + } + function testGas_CancelFullDepthBidCombRightmost() public { vm.pauseGasMetering(); bytes32 targetBid = _buildFullDepthBidNonceComb(); @@ -608,7 +657,7 @@ contract RadixMatchingEngineGasTest is Test { uint64 targetKey = type(uint64).max; quote.mint(alice, uint256(1) << 200); - vm.store(address(engine), _nextNonceSlot(), bytes32(_nonceAndFlags(MAX_ORDER_NONCE))); + _forceNextOrderNonce(MAX_ORDER_NONCE); vm.prank(alice); targetOrder = _fill(_order(type(int32).max, 1, 0), true); @@ -616,10 +665,13 @@ contract RadixMatchingEngineGasTest is Test { uint64 siblingKey = targetKey ^ uint64(uint256(1) << (63 - depth)); // forge-lint: disable-next-line(unsafe-typecast) int32 price = int32(uint32(siblingKey >> 32) ^ 0x80000000); + // Price bits determine splits above depth 32, so those fixtures can use separate low + // nonces without changing the requested split. Nonce-bit splits already produce + // distinct nonces through depth 63. // forge-lint: disable-next-line(unsafe-typecast) - uint32 nonce = uint32(siblingKey); + uint32 nonce = depth < 32 ? MAX_ORDER_NONCE - uint32(depth) - 1 : uint32(siblingKey); - vm.store(address(engine), _nextNonceSlot(), bytes32(_nonceAndFlags(nonce))); + _forceNextOrderNonce(nonce); vm.prank(alice); _fill(_order(price, 1, 0), true); } @@ -628,7 +680,7 @@ contract RadixMatchingEngineGasTest is Test { function _buildMaxValidDepthAskNonceComb() internal returns (bytes32 targetOrder) { uint64 targetSortKey = type(uint64).max; - vm.store(address(engine), _nextNonceSlot(), bytes32(_nonceAndFlags(MAX_ORDER_NONCE))); + _forceNextOrderNonce(MAX_ORDER_NONCE); vm.prank(alice); targetOrder = _fill(_order(type(int32).min, 1, 0), false); @@ -637,9 +689,9 @@ contract RadixMatchingEngineGasTest is Test { uint32 sortableTick = type(uint32).max - uint32(sortKey >> 32); int32 price = int32(sortableTick ^ 0x80000000); // forge-lint: disable-next-line(unsafe-typecast) - uint32 nonce = uint32(sortKey); + uint32 nonce = depth < 32 ? MAX_ORDER_NONCE - uint32(depth) - 1 : uint32(sortKey); - vm.store(address(engine), _nextNonceSlot(), bytes32(_nonceAndFlags(nonce))); + _forceNextOrderNonce(nonce); vm.prank(alice); _fill(_order(price, 1, 0), false); } @@ -649,7 +701,7 @@ contract RadixMatchingEngineGasTest is Test { uint64 targetKey = (uint64(uint32(price) ^ 0x80000000) << 32) | uint64(MAX_ORDER_NONCE); quote.mint(alice, 2_000_000_000); - vm.store(address(engine), _nextNonceSlot(), bytes32(_nonceAndFlags(MAX_ORDER_NONCE))); + _forceNextOrderNonce(MAX_ORDER_NONCE); vm.prank(alice); targetOrder = _fill(_order(price, 1, 0), true); @@ -658,12 +710,39 @@ contract RadixMatchingEngineGasTest is Test { // forge-lint: disable-next-line(unsafe-typecast) uint32 nonce = uint32(siblingKey); - vm.store(address(engine), _nextNonceSlot(), bytes32(_nonceAndFlags(nonce))); + _forceNextOrderNonce(nonce); vm.prank(alice); _fill(_order(price, 1, 0), true); } } + function _buildOffSpineBidNonceComb() internal { + uint64 targetKey = type(uint64).max >> 1; + quote.mint(alice, uint256(1) << 200); + + _forceNextOrderNonce(MAX_ORDER_NONCE); + vm.prank(alice); + _fill(_order(-1, 10, 0), true); + + for (uint256 depth = 1; depth < 64; ++depth) { + uint64 siblingKey = targetKey ^ uint64(uint256(1) << (63 - depth)); + // forge-lint: disable-next-line(unsafe-typecast) + int32 price = int32(uint32(siblingKey >> 32) ^ 0x80000000); + // forge-lint: disable-next-line(unsafe-typecast) + uint32 nonce = depth < 32 ? MAX_ORDER_NONCE - uint32(depth) - 1 : uint32(siblingKey); + + _forceNextOrderNonce(nonce); + vm.prank(alice); + _fill(_order(price, 2, 0), true); + } + + // Add one globally better leaf so matching it first moves execution into the deep comb via + // `_matchBidSubtree`, where the following partial fill must rewrite every surviving branch. + _forceNextOrderNonce(MAX_ORDER_NONCE - 100); + vm.prank(alice); + _fill(_order(type(int32).max, 1, 0), true); + } + function _order(int32 price, uint160 quantity, uint32 nonce) internal pure returns (bytes32) { return bytes32((uint256(uint32(price)) << 224) | (uint256(quantity) << 64) | uint256(nonce)); } @@ -736,8 +815,10 @@ contract RadixMatchingEngineGasTest is Test { return keccak256(abi.encode(_bookId(), uint256(0))); } - function _nonceAndFlags(uint256 nonce) internal pure virtual returns (uint256) { - return nonce; + function _forceNextOrderNonce(uint32 nonce) internal { + bytes32 slot = _nextNonceSlot(); + uint256 nonceAndFlags = uint256(vm.load(address(engine), slot)); + vm.store(address(engine), slot, bytes32((nonceAndFlags & ~uint256(type(uint32).max)) | nonce)); } } @@ -748,10 +829,6 @@ contract RadixMatchingEngineHookGasTest is RadixMatchingEngineGasTest { hook = new MockHook(); engine.setPoolHookConfig(address(base), address(quote), address(hook), true, true); } - - function _nonceAndFlags(uint256 nonce) internal pure override returns (uint256) { - return nonce | BOOK_HOOK_TOKEN0_ACTIVE | BOOK_HOOK_TOKEN1_ACTIVE; - } } contract RadixMatchingEngineFeeGasTest is RadixMatchingEngineGasTest { diff --git a/test/RadixMatchingEngineInvariant.t.sol b/test/RadixMatchingEngineInvariant.t.sol index 1416eb8..fbfdc47 100644 --- a/test/RadixMatchingEngineInvariant.t.sol +++ b/test/RadixMatchingEngineInvariant.t.sol @@ -1418,14 +1418,16 @@ contract RadixMatchingEngineInvariantTest is StdInvariant, Test { stats.uniform = leftStats.uniform && rightStats.uniform && stats.leftmostPrice == stats.rightmostPrice; stats.exists = true; if (!rightmost) { - assertEq( - node, - _branchNodeForChildren(leftNode, rightNode, isBidTree, stats.quoteAmount, stats.uniform), - "branch address" - ); assertEq(_quantity(node), stats.quantity, "branch quantity"); - assertEq(_pathKey(node), stats.maxPathKey, "branch max path"); assertGt(_quantity(node), stats.maxPathLeafQuantity, "branch quantity over max leaf"); + assertEq(_price(node), leftStats.leftmostPrice, "branch representative price"); + if (stats.uniform) { + uint256 aggregateQuote = _quoteValue(_price(node), stats.quantity, isBidTree); + uint256 correction = isBidTree ? stats.quoteAmount - aggregateQuote : aggregateQuote - stats.quoteAmount; + assertEq(_correctionCode(node), correction + 1, "branch correction"); + } else { + assertEq(_correctionCode(node), 0, "mixed branch correction"); + } _assertStoredNodeKeyRepresentsSubtree(node, stats, isBidTree); } _assertSubtreePricePriority(stats, isBidTree); @@ -1489,7 +1491,7 @@ contract RadixMatchingEngineInvariantTest is StdInvariant, Test { SubtreeStats memory leftStats, SubtreeStats memory rightStats, bool isBidTree - ) private pure { + ) private view { uint8 storedBranchDepth = _commonPrefix( _storedNodeKey(leftNode, isBidTree), _storedNodeKey(rightNode, isBidTree) ); @@ -1498,7 +1500,7 @@ contract RadixMatchingEngineInvariantTest is StdInvariant, Test { function _assertStoredNodeKeyRepresentsSubtree(bytes32 node, SubtreeStats memory stats, bool isBidTree) private - pure + view { uint64 storedKey = _storedNodeKey(node, isBidTree); assertGe(storedKey, stats.minKey, "node key below subtree"); @@ -1552,7 +1554,7 @@ contract RadixMatchingEngineInvariantTest is StdInvariant, Test { function _assertBranchAbsentFromSubtree(bytes32 targetBranch, bytes32 node) private view { if (node == bytes32(0) || !_isBranch(node)) return; - assertTrue(targetBranch != node, "shared branch"); + assertTrue(_nonce(targetBranch) != _nonce(node), "shared branch identity"); (bytes32 leftNode, bytes32 rightNode) = engine.tree(node); _assertBranchAbsentFromSubtree(targetBranch, leftNode); _assertBranchAbsentFromSubtree(targetBranch, rightNode); @@ -1566,7 +1568,7 @@ contract RadixMatchingEngineInvariantTest is StdInvariant, Test { if (node == bytes32(0)) return seenCount; for (uint256 i; i < seenCount; ++i) { - assertTrue(seenNodes[i] != node, "duplicate live node"); + assertTrue(_nonce(seenNodes[i]) != _nonce(node), "duplicate live node identity"); } assertLt(seenCount, seenNodes.length, "seen node capacity"); seenNodes[seenCount++] = node; @@ -1591,33 +1593,13 @@ contract RadixMatchingEngineInvariantTest is StdInvariant, Test { function _assertOwnedLiveBranchesBackedByPartialOrders(bytes32 node, bool isBidTree) private view { if (node == bytes32(0) || !_isBranch(node)) return; - address owner = engine.ownerOfOrder(node); - if (owner != address(0)) _assertOwnedBranchBackedByPartialOrder(node, owner, isBidTree); + assertEq(engine.ownerOfOrder(node), address(0), "branch aliases owned order"); (bytes32 leftNode, bytes32 rightNode) = engine.tree(node); _assertOwnedLiveBranchesBackedByPartialOrders(leftNode, isBidTree); _assertOwnedLiveBranchesBackedByPartialOrders(rightNode, isBidTree); } - function _assertOwnedBranchBackedByPartialOrder(bytes32 branchNode, address owner, bool isBidTree) private view { - uint256 matches; - uint256 length = handler.orderCount(); - - for (uint256 i; i < length; ++i) { - (bytes32 order, address trackedOwner, bool trackedIsBid, bool active) = handler.orderAt(i); - if (order != branchNode) continue; - - assertTrue(active, "owned branch inactive order"); - assertEq(trackedOwner, owner, "owned branch owner"); - assertEq(trackedIsBid, isBidTree, "owned branch side"); - assertGt(handler.remainingQuantityAt(i), 0, "owned branch filled order"); - assertLt(handler.remainingQuantityAt(i), _quantity(order), "owned branch unfilled order"); - ++matches; - } - - assertEq(matches, 1, "owned branch backing"); - } - function _containsBranchByContractRouting(bytes32 root, bytes32 target, bool isBidTree) private view @@ -1732,8 +1714,8 @@ contract RadixMatchingEngineInvariantTest is StdInvariant, Test { (bytes32 leftNode, bytes32 rightNode) = engine.tree(root); if (leftNode == bytes32(0)) return _sortKey(root, isBidTree) == targetKey ? root : bytes32(0); - uint64 leftKey = _sortKey(leftNode, isBidTree); - uint8 branchDepth = _commonPrefix(leftKey, _sortKey(rightNode, isBidTree)); + uint64 leftKey = _nodeKey(leftNode, isBidTree); + uint8 branchDepth = _commonPrefix(leftKey, _nodeKey(rightNode, isBidTree)); if (_commonPrefix(targetKey, leftKey) < branchDepth) return bytes32(0); root = _bit(targetKey, branchDepth) ? rightNode : leftNode; @@ -1749,7 +1731,9 @@ contract RadixMatchingEngineInvariantTest is StdInvariant, Test { function _branchDepth(bytes32 branchNode, bool isBidTree) private view returns (uint8) { (bytes32 leftNode, bytes32 rightNode) = engine.tree(branchNode); - return _commonPrefix(_nodeKey(leftNode, isBidTree), _nodeKey(rightNode, isBidTree)); + uint8 computed = _commonPrefix(_nodeKey(leftNode, isBidTree), _nodeKey(rightNode, isBidTree)); + assertEq(engine.branchDepth(branchNode), computed, "cached branch depth"); + return computed; } function _nodeKey(bytes32 node, bool isBidTree) private view returns (uint64) { @@ -1758,42 +1742,8 @@ contract RadixMatchingEngineInvariantTest is StdInvariant, Test { return _nodeKey(leftNode != bytes32(0) ? leftNode : rightNode, isBidTree); } - function _storedNodeKey(bytes32 node, bool isBidTree) private pure returns (uint64) { - return _sortKey(node, isBidTree); - } - - function _branchNodeForChildren( - bytes32 leftNode, - bytes32 rightNode, - bool isBidTree, - uint256 childQuoteAmount, - bool uniform - ) private pure returns (bytes32) { - uint64 leftAddressKey = _nodeAddressKey(leftNode); - uint64 rightAddressKey = _nodeAddressKey(rightNode); - assertTrue(leftAddressKey != rightAddressKey, "branch address key"); - - uint160 quantity = _quantity(leftNode) + _quantity(rightNode); - uint64 prefix = leftAddressKey > rightAddressKey ? leftAddressKey : rightAddressKey; - // forge-lint: disable-next-line(unsafe-typecast) - int32 prefixPrice = int32(uint32(prefix >> 32) ^ 0x80000000); - // forge-lint: disable-next-line(unsafe-typecast) - uint32 prefixNonce = uint32(prefix); - uint32 correctionCode; - if (uniform) { - uint256 aggregateQuote = _quoteValue(prefixPrice, quantity, isBidTree); - uint256 correction = isBidTree ? childQuoteAmount - aggregateQuote : aggregateQuote - childQuoteAmount; - assertLt(correction, type(uint32).max, "branch correction"); - correctionCode = uint32(correction + 1); - } - return bytes32( - (uint256(uint32(prefixPrice)) << _PRICE_SHIFT) | (uint256(quantity) << _QUANTITY_SHIFT) - | (uint256(correctionCode) << 32) | uint256(prefixNonce) - ); - } - - function _nodeAddressKey(bytes32 node) private pure returns (uint64) { - return _pathKey(node); + function _storedNodeKey(bytes32 node, bool isBidTree) private view returns (uint64) { + return _nodeKey(node, isBidTree); } function _sortKey(bytes32 order, bool isBidTree) private pure returns (uint64) { @@ -1820,6 +1770,11 @@ contract RadixMatchingEngineInvariantTest is StdInvariant, Test { return (uint64(uint32(_price(order)) ^ 0x80000000) << 32) | uint64(_nonce(order)); } + function _correctionCode(bytes32 node) private pure returns (uint32) { + // forge-lint: disable-next-line(unsafe-typecast) + return uint32(uint256(node) >> 32); + } + function _commonPrefix(uint64 a, uint64 b) private pure returns (uint8 prefixLength) { for (; prefixLength < 64; ++prefixLength) { if (_bit(a, prefixLength) != _bit(b, prefixLength)) return prefixLength; diff --git a/test/SinglePairEngineHarness.sol b/test/SinglePairEngineHarness.sol index a725b4d..c69c09d 100644 --- a/test/SinglePairEngineHarness.sol +++ b/test/SinglePairEngineHarness.sol @@ -76,6 +76,10 @@ contract SinglePairEngineHarness is DeepstateV1 { return this.tree(this.bookId(BASE_TOKEN, QUOTE_TOKEN, 0), node); } + function branchDepth(bytes32 node) external view returns (uint8) { + return _storedBranchDepth(books[this.bookId(BASE_TOKEN, QUOTE_TOKEN, 0)], node); + } + function _delegate(bytes memory data) private returns (bytes memory result) { bool success; (success, result) = address(this).delegatecall(data);