Read only the requested inclusion height range from the DB in the wallet box endpoints - #2458
Read only the requested inclusion height range from the DB in the wallet box endpoints#2458Ergologica wants to merge 6 commits into
Conversation
walletUnspentBoxesByInclusionHeight and walletBoxesByInclusionHeight over the existing inclusion-height index for the payments scan, the same one that already backs /scan/unspentBoxes. Part of ergoplatform#1870.
GetWalletBoxes, ErgoWalletReader.walletBoxes and ErgoWalletService.getWalletBoxes carry minHeight and maxHeight, defaulting to 0 and -1 so every existing caller is unaffected. When a range is requested the implementation reads it from the inclusion-height index; without one it keeps the previous limited read of the unspent index, so no query becomes more expensive than it is today. Part of ergoplatform#1870.
/wallet/boxes and /wallet/boxes/unspent hand minInclusionHeight and maxInclusionHeight to the wallet reader instead of only filtering on them afterwards. The in-memory boxConfirmationHeightFilter is kept: it also handles confirmations and preserves the current treatment of off-chain boxes, which have no inclusion height. Part of ergoplatform#1870.
So the route specs cover the parameters being threaded all the way to the actor message. Part of ergoplatform#1870.
Spent/unspent split, inclusive bounds on both ends, empty window, lower bound only, and -1 as upper bound returning exactly what the unspent index returns. Part of ergoplatform#1870.
Two disjoint height windows over distinct boxes, checking neither leaks into the other, that spent boxes stay excluded when unspentOnly is set, that an empty window yields nothing, and that maxHeight = -1 keeps the previous behaviour. Part of ergoplatform#1870.
|
Flagging something a reviewer should not have to find out the hard way: #2284 already exists for #1870, opened in December, and I did not see it before opening this. Here these two do overlap — same approach, same call chain. What is the same: threading Where this one goes further:
And the reason I think this is worth more than an API convenience: I am not asking for #2284 to be closed, and the choice between them is not mine. @glasgowm148 for the registry side — reservation is ErgoDevs/Ergo-Bounties#57, and their earlier submission #35 was closed. |
|
One more thing a reviewer will hit before they read a line of this: #1870 already has a merged PR that says #1872, "i1870 filter /scan/unspent boxes by inclusion height efficiently", merged 2022-11-03. Its author flagged it himself in the body:
It did the // ScanApiRoute.scala:71 - the scan endpoint, done by #1872
withWallet(_.scanUnspentBoxes(scanId, considerUnconfirmed, minHeight, maxHeight)
// WalletApiRoute.scala:307 - the endpoint #1870 actually names, untouched
wallet.walletBoxes(unspentOnly = true, considerUnconfirmed)
.map { boxes => boxes.filter(boxConfirmationHeightFilter(_, minConfNum, maxConfNum, minHeight, maxHeight)) }The scan route gets a database range. Why the in-memory filter is not merely slowerI said above that // WalletRegistry.scala:587 - how an unspent-index key is built
composeKeyWithId(prefix, scanId, trackedBox.box.id)
// :542-546 - the range walletUnspentBoxes scans
composeKey(UnspentIndexPrefix, scanId, 0) .. composeKey(UnspentIndexPrefix, scanId, -1)
// :555-558 - the height index, which exists and the wallet path does not use
composeKey(InclusionHeightScanBoxPrefix, scanId, height)So That is a correctness bug sitting under the performance one, and worth a maintainer's eye independently of which PR you take for #1870. |
Closes #1870.
What the issue asks
@pragmaxim asked on the issue: by
inclusionHeightorspendingHeight? — byinclusionHeight:minInclusionHeight/maxInclusionHeight;WalletApiOperations.boxConfirmationHeightFilter) readstrackedBox.inclusionHeightOpt;WalletRegistry(InclusionHeightScanBoxPrefix) is keyed by inclusion height;The problem
minInclusionHeight/maxInclusionHeightare accepted and documented, but they are applied in memory, after the boxes have been read, andgetWalletBoxesreads the unspent index with a limit:That index (
UnspentIndexPrefix) is keyed by box id, not by height, so the limit (100 x 300 = 30000 by default) takes an arbitrary subset. For a wallet holding more unspent boxes than that, a query for a height window can return an arbitrary subset of it, or nothing at all, with no indication that anything was dropped — andoffset/limitpaging on top is unstable for the same reason. ForunspentOnly = falsethere is no limit, but the whole box space is read and then thrown away.Meanwhile the DB-level primitive already exists and is used only by the scan endpoints:
WalletRegistry.unspentBoxesByInclusionHeight, backed by the inclusion-height index and already exercised by/scan/unspentBoxes. This PR wires the wallet path to it.The change
WalletRegistry: two thin helpers over the existing height index for the payments scan —walletUnspentBoxesByInclusionHeightandwalletBoxesByInclusionHeight.GetWalletBoxes,ErgoWalletReader.walletBoxes,ErgoWalletService.getWalletBoxes: carryminHeight/maxHeight, defaulting to0/-1so every existing caller is unaffected.ErgoWalletServiceImpl.getWalletBoxes: when a height range is requested, read it from the inclusion-height index; otherwise keep reading the unspent index exactly as before.The conditional is deliberate: reading the height index unconditionally would drop the
maxInputsToUse * ScanDepthFactorcap for the common unbounded query and could load the whole box space of a large wallet. This way the new path is never worse than the current one.-1as "unbounded" is the convention already used by/scan/unspentBoxes, and it works directly against the key encoding (putInt(-1)is0xFFFFFFFF, the largest 4-byte prefix). There is now a test pinning that.The in-memory
boxConfirmationHeightFilterin the route is kept: it also handlesminConfirmations/maxConfirmations, and it preserves the current treatment of off-chain boxes, which have no inclusion height. So this PR changes which boxes are read from the database, not which boxes are returned — except that boxes previously lost to the limit are now actually returned./wallet/boxesgets the same treatment as/wallet/boxes/unspent: it is the same function, and the endpoint takes the same two parameters.Tests
WalletRegistrySpec: newshould get wallet boxes by inclusion height— spent/unspent split, inclusive bounds on both ends, empty window, lower bound only, and-1as upper bound returning exactly what the unspent index returns.ErgoWalletServiceSpec: newit should read wallet boxes within an inclusion height range only— two disjoint height windows over distinct boxes, checking neither leaks into the other, that spent boxes stay excluded whenunspentOnlyis set, that an empty window yields nothing, and thatmaxHeight = -1keeps the previous behaviour.Stubs: the wallet actor stub now applies the height range, so the route specs cover the parameters being threaded all the way to the actor message.sbt "testOnly org.ergoplatform.http.routes.* org.ergoplatform.nodeView.wallet.*"— 210 tests, all green.