With a label lookup, secp256k1_silentpayments_recipient_scan_outputs does two point additions and two lookups for every output of the transaction, even when the recipient scans only for the change label. A transaction with 2323 outputs that does not pay the recipient takes 0.86 ms to scan, against 25 µs for one with 2 outputs. One that pays the recipient in all 2323 outputs, the K_max limit, takes about a second, because the outputs are walked again for every k.
This PR adds secp256k1_silentpayments_recipient_scan_outputs_with_labels, which takes the labels as an array instead of a callback. For each k it adds every label to the candidate output and compares the result with the outputs, so its point additions grow with the number of labels, not with the number of outputs. It returns the same outputs, tweaks and labels as scan_outputs, also on malformed transactions, and the tests check that. scan_outputs does not change: the first commit only moves its setup into helpers that both functions use.
I measured both functions on the same transactions with an out-of-tree harness, using a hash table as the label lookup (Apple M5, best of 3 to 7 runs), and can share it. The in-tree nomatch benchmark at 2323 outputs agrees: about 880 µs and 30 µs.
| transaction | labels | scan_outputs |
_with_labels |
|---|---|---|---|
| 2 outputs, no payment | 1 / 10 / 100 | 25 / 25 / 26 µs | 24 / 26 / 43 µs |
| 2323 outputs, no payment | 1 / 10 | 858 / 858 µs | 28 / 37 µs |
| 2323 outputs, all to the recipient (the K_max limit) | 1 / 10 / 100 / 1000 | 1.0 s | 0.027 / 0.040 / 0.18–0.44 / 1.52 s |
| 23255 outputs, last 2323 to the recipient (non-standard, loaded machine) | 1 | 18.7 s | 76 ms |
#1792 proposed this approach as the only scanning function. It was closed because K_max bounded the worst case it was meant to fix, and #1799 found that a cost growing with the number of labels outweighed its simpler API. jonasnick wrote there that "in the long run we should have both the BIP approach and the labelset approach". Now that #1765 has merged the BIP approach, this adds the other one as a second function, so recipients with many labels keep the callback and no label limit is needed. Each function takes the labels once, unlike the hybrid prototype, and the outputs keep their order, unlike in #1792, so the lowest index still wins as in #1765. The cost is one more public function and one struct.
This does not help a recipient with many labels. At 2323 outputs all paying the recipient, scan_outputs is faster from about 650 labels. This PR conflicts with #1912 and #1914, and I will rebase on whichever merges first.