diff --git a/bip-0352.mediawiki b/bip-0352.mediawiki
index 3b200fef..83cbd885 100644
--- a/bip-0352.mediawiki
+++ b/bip-0352.mediawiki
@@ -301,6 +301,8 @@ After the inputs have been selected, the sender can create one or more outputs f
* Let ''input_hash = hashBIP0352/Inputs(outpointL || A)'', where ''outpointL'' is the smallest ''outpoint'' lexicographically used in the transaction and ''A = a·G''
** If ''input_hash'' is not a valid scalar, i.e., if ''input_hash = 0'' or ''input_hash'' is larger or equal to the secp256k1 group order, fail
* Group receiver silent payment addresses by ''Bscan'' (e.g. each group consists of one ''Bscan'' and one or more ''Bm'')
+* If any of the groups exceed the limit of ''Kmax'' (=2323) silent payment addresses, fail.['''Why is the size of groups (i.e. silent payment addresses sharing the same scan public key) limited by ''Kmax''?''' An adversary could construct a block filled with a single transaction consisting of N=23255 outputs (that's the theoretical maximum under current consensus rules, w.r.t. the block weight limit) that all target the same entity, consisting of one large group. Without a limit on the group size, scanning such a block with the algorithm described in this document would have a complexity of ''O(N2)'' for that entity, taking several minutes on modern systems. By capping the group size at ''Kmax'', we reduce the inner loop iterations to ''Kmax'', thereby decreasing the worst-case block scanning complexity to ''O(N·Kmax)''. This cuts down the scanning cost to the order of tens of seconds. The chosen value of ''Kmax'' = 2323 represents the maximum number of P2TR outputs that can fit into a 100kvB transaction, meaning a transaction that adheres to the current standardness rules is guaranteed to be within the limit. This ensures flexibility and also mitigates potential fingerprinting issues.]
+
* For each group:
** Let ''ecdh_shared_secret = input_hash·a·Bscan''
** Let ''k = 0''
@@ -340,6 +342,7 @@ If each of the checks in ''[[#scanning-silent-payment-eligible-transactions|Scan
* Check for outputs:
** Let ''outputs_to_check'' be the taproot output keys from all taproot outputs in the transaction (spent and unspent).
** Starting with ''k = 0'':
+*** If ''k == Kmax'' (=2323), stop scanning.
*** Let ''tk = hashBIP0352/SharedSecret(serP(ecdh_shared_secret) || ser32(k))''
**** If ''tk'' is not a valid scalar, i.e., if ''tk = 0'' or ''tk'' is larger or equal to the secp256k1 group order, fail
*** Compute ''Pk = Bspend + tk·G''
@@ -489,6 +492,8 @@ The MAJOR version is incremented if changes to the BIP are introduc
The MINOR version is incremented whenever the inputs or the output of an algorithm changes in a backward-compatible way or new backward-compatible functionality is added.
The PATCH version is incremented for other changes that are noteworthy (bug fixes, test vectors, important clarifications, etc.).
+* '''1.1.0''' (2026-03-02):
+** Introduce per-group recipient limit ''Kmax'' to mitigate quadratic scanning behavior for adversarial transactions.
* '''1.0.2''' (2025-07-25):
** Clarify how to handle the improbable corner case where the output of SHA256 is equal to 0 or greater than or equal to the secp256k1 curve order.
* '''1.0.1''' (2024-06-22):
diff --git a/bip-0352/reference.py b/bip-0352/reference.py
index c236c0db..65e0d504 100755
--- a/bip-0352/reference.py
+++ b/bip-0352/reference.py
@@ -26,6 +26,9 @@ from bitcoin_utils import (
)
+K_max = 2323 # per-group recipient limit
+
+
def get_pubkey_from_input(vin: VinInfo) -> ECPubKey:
if is_p2pkh(vin.prevout):
# skip the first 3 op_codes and grab the 20 byte hash
@@ -144,6 +147,10 @@ def create_outputs(input_priv_keys: List[Tuple[ECKey, bool]], outpoints: List[CO
else:
silent_payment_groups[B_scan] = [B_m]
+ # Fail if per-group recipient limit (K_max) is exceeded
+ if any([len(group) > K_max for group in silent_payment_groups.values()]):
+ return []
+
outputs = []
for B_scan, B_m_values in silent_payment_groups.items():
ecdh_shared_secret = input_hash * a_sum * B_scan
@@ -175,6 +182,8 @@ def scanning(b_scan: ECKey, B_spend: ECPubKey, A_sum: ECPubKey, input_hash: byte
k = 0
wallet = []
while True:
+ if k == K_max: # Don't look further than the per-group recipient limit (K_max)
+ break
t_k = TaggedHash("BIP0352/SharedSecret", ecdh_shared_secret.get_bytes(False) + ser_uint32(k))
P_k = B_spend + t_k * G
for output in outputs_to_check: