From 358f1d1f39d03f631395bba6f2ca4c65f7f6d7d9 Mon Sep 17 00:00:00 2001 From: Edil Medeiros Date: Sat, 2 May 2026 14:01:53 -0300 Subject: [PATCH 1/5] Add check for how many keys sender generated The specification limits how many keys the receiver is allowed to scan. Thus, we should not allow the sender to generate more than that many keys/outputs to prevent loss of funds. Co-authored-by: themhv Co-authored-by: joaozinhom Co-authored-by: IsaqueFranklin Co-authored-by: lucasdcf Co-authored-by: oleonardolima Co-authored-by: Lorenzo --- bip-0352.mediawiki | 1 + 1 file changed, 1 insertion(+) diff --git a/bip-0352.mediawiki b/bip-0352.mediawiki index 785365566d..8d3789f6cf 100644 --- a/bip-0352.mediawiki +++ b/bip-0352.mediawiki @@ -314,6 +314,7 @@ After the inputs have been selected, the sender can create one or more outputs f *** Optionally, repeat with k++ to create additional outputs for the current ''Bm'' *** If no additional outputs are required, continue to the next ''Bm'' with ''k++''''' Why not re-use ''tk'' when paying different labels to the same receiver?''' If paying the same entity but to two separate labeled addresses in the same transaction without incrementing ''k'', an outside observer could subtract the two output values and observe that this value is the same as the difference between two published silent payment addresses and learn who the recipient is. ** Optionally, if the sending wallet implements receiving silent payments, it can create change outputs by sending to its own silent payment address using label ''m = 0'', following the steps above +** If ''k > Kmax'', fail.. Thus, any transaction that generates more than this many keys for a single receiver is to be considered invalid under this specification. We achieve this by mandating the sender to keep track of the number of keys ''k'' they generated for a single group and fail whenever ''k'' exceeds ''Kmax''. All generated outputs MUST be present in the final transaction. If an output ''Pi'' with ''i < k'' is omitted, the receiver will not be able to find outputs ''Pj'' where ''i < j <= k''. From 18f9e281f4cdd7088eb71dc4ba671673f14707ad Mon Sep 17 00:00:00 2001 From: Edil Medeiros Date: Sat, 2 May 2026 14:03:42 -0300 Subject: [PATCH 2/5] Require functional test for number of outputs generated in a transaction Co-Authored-By: themhv Co-Authored-By: joaozinhom Co-Authored-By: IsaqueFranklin Co-Authored-By: lucasdcf Co-Authored-By: oleonardolima Co-Authored-By: Lorenzo --- bip-0352.mediawiki | 1 + 1 file changed, 1 insertion(+) diff --git a/bip-0352.mediawiki b/bip-0352.mediawiki index 8d3789f6cf..7acbde1d0f 100644 --- a/bip-0352.mediawiki +++ b/bip-0352.mediawiki @@ -448,6 +448,7 @@ Below is a list of functional tests which should be included in sending and rece * Ensure taproot outputs are excluded during coin selection if the sender does not have access to the key path private key (unless using ''H'' as the taproot internal key) * Ensure the silent payment address is re-derived if inputs are added or removed during RBF +* Ensure no more than ''Kmax'' outputs are created for a single receiver silent payment address group. ==== Receiving ==== From 47699caa722b5f6e8b0e6ddc0f43f27c52b75e62 Mon Sep 17 00:00:00 2001 From: Edil Medeiros Date: Sat, 2 May 2026 15:59:05 -0300 Subject: [PATCH 3/5] Change reference implementation to be spec compliant The original implementation references does a trick to avoid generating more outputs thatn the receiver is allowed to scan: it clones the payment specification before calling the crate_outputs() function. Then, when creating groups, the function will see the same address many times and create an artifically big group. This is not a reasonable interpretation of the spec, i.e., if I want to send 2324 outputs for the same address, this should be interpretated as a single group containing a single address. This is what this commit implements. Note that this will NOT pass the unit tests. Next commit will fix this. Co-Authored-By: themhv Co-Authored-By: joaozinhom Co-Authored-By: IsaqueFranklin Co-Authored-By: lucasdcf Co-Authored-By: oleonardolima Co-Authored-By: Lorenzo --- bip-0352/reference.py | 21 ++++++++++----------- bip-0352/send_and_receive_test_vectors.json | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/bip-0352/reference.py b/bip-0352/reference.py index 1671c25142..51f6a44049 100755 --- a/bip-0352/reference.py +++ b/bip-0352/reference.py @@ -147,10 +147,11 @@ def create_outputs(input_priv_keys: List[Tuple[Scalar, bool]], outpoints: List[C expected_B_m = GE.from_bytes_compressed(bytes.fromhex(recipient["spend_pub_key"])) assert expected_B_scan == B_scan, "B_scan did not match expected recipient.scan_pub_key" assert expected_B_m == B_m, "B_m did not match expected recipient.spend_pub_key" + count = recipient.get("count", 1) if B_scan in silent_payment_groups: - silent_payment_groups[B_scan].append(B_m) + silent_payment_groups[B_scan].append((B_m, count)) else: - silent_payment_groups[B_scan] = [B_m] + silent_payment_groups[B_scan] = [(B_m, count)] # Fail if per-group recipient limit (K_max) is exceeded if any([len(group) > K_max for group in silent_payment_groups.values()]): @@ -168,11 +169,12 @@ def create_outputs(input_priv_keys: List[Tuple[Scalar, bool]], outpoints: List[C assert ecdh_shared_secret.to_bytes_compressed().hex() == expected_shared_secret_hex, f"ecdh_shared_secret did not match expected, recipient {recipient_idx} ({recipient['address']}): expected={expected_shared_secret_hex}" break k = 0 - for B_m in B_m_values: - t_k = Scalar.from_bytes_checked(tagged_hash("BIP0352/SharedSecret", ecdh_shared_secret.to_bytes_compressed() + ser_uint32(k))) - P_km = B_m + t_k * G - outputs.append(P_km.to_bytes_xonly().hex()) - k += 1 + for B_m, count in B_m_values: + for _ in range(count): + t_k = Scalar.from_bytes_checked(tagged_hash("BIP0352/SharedSecret", ecdh_shared_secret.to_bytes_compressed() + ser_uint32(k))) + P_km = B_m + t_k * G + outputs.append(P_km.to_bytes_xonly().hex()) + k += 1 return list(set(outputs)) @@ -270,10 +272,7 @@ def scanning(b_scan: Scalar, B_spend: GE, A_sum: GE, input_hash: bytes, outputs_ sending_outputs = [] if (len(input_pub_keys) > 0): outpoints = [vin.outpoint for vin in vins] - recipients = [] # expand given recipient entries to full list - for recipient_entry in given["recipients"]: - count = recipient_entry.get("count", 1) - recipients.extend([recipient_entry] * count) + recipients = given["recipients"] sending_outputs = create_outputs(input_priv_keys, outpoints, recipients, expected=expected, hrp="sp") # Note: order doesn't matter for creating/finding the outputs. However, different orderings of the recipient addresses diff --git a/bip-0352/send_and_receive_test_vectors.json b/bip-0352/send_and_receive_test_vectors.json index 3a189757dd..009201ffcf 100644 --- a/bip-0352/send_and_receive_test_vectors.json +++ b/bip-0352/send_and_receive_test_vectors.json @@ -3353,7 +3353,7 @@ [] ], "shared_secrets": [ - null + "03abe0979dbbe4e1bb2cd2e06524a411c0b829c1d2282052da817b46f1d6e598e4" ], "input_private_key_sum": "0000000000000000000000000000000000000000000000000000000000001337", "input_pub_keys": [ From cea7e3ebf73e88dc75d826a496f661c2a7416044 Mon Sep 17 00:00:00 2001 From: Edil Medeiros Date: Sat, 2 May 2026 16:02:37 -0300 Subject: [PATCH 4/5] Add check to the amount of keys generated per group Co-Authored-By: themhv Co-Authored-By: joaozinhom Co-Authored-By: IsaqueFranklin Co-Authored-By: lucasdcf Co-Authored-By: oleonardolima Co-Authored-By: Lorenzo --- bip-0352/reference.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bip-0352/reference.py b/bip-0352/reference.py index 51f6a44049..734f1980d6 100755 --- a/bip-0352/reference.py +++ b/bip-0352/reference.py @@ -175,6 +175,10 @@ def create_outputs(input_priv_keys: List[Tuple[Scalar, bool]], outpoints: List[C P_km = B_m + t_k * G outputs.append(P_km.to_bytes_xonly().hex()) k += 1 + # Should not exceed K_max keys/outputs + if k > K_max: + outputs = [] + return list(set(outputs)) return list(set(outputs)) From 47ef418a112c70b829da03cfe4b875a39098e6ba Mon Sep 17 00:00:00 2001 From: Edil Medeiros Date: Wed, 6 May 2026 13:32:08 -0300 Subject: [PATCH 5/5] Fix typos and suggest minor redaction improvements --- bip-0352.mediawiki | 46 +++++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/bip-0352.mediawiki b/bip-0352.mediawiki index 7acbde1d0f..996c0bf2b8 100644 --- a/bip-0352.mediawiki +++ b/bip-0352.mediawiki @@ -68,13 +68,9 @@ Since ''a·B == b·A'' ([https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2% ''' Creating more than one output ''' -In order to allow Alice to create more than one output for Bob'''Why allow for more than one output?''' Allowing Alice to break her payment to Bob into multiple amounts opens up a number of privacy improving techniques for Alice, making the transaction look like a CoinJoin or better hiding the change amount by splitting both the payment and change outputs into multiple amounts. It also allows for Alice and Carol to both have their own unique output paying Bob in the event they are in a collaborative transaction and both paying Bob's silent payment address., we include an integer in the following manner: +In order to allow Alice to create more than one output for Bob'''Why allow for more than one output?''' Allowing Alice to break her payment to Bob into multiple amounts opens up a number of privacy improving techniques for Alice, making the transaction look like a CoinJoin or better hiding the change amount by splitting both the payment and change outputs into multiple amounts. It also allows for Alice and Carol to both have their own unique output paying Bob in the event they are in a collaborative transaction and both paying Bob's silent payment address., we include an integer ''k'' in the following manner: -* Let ''k = 0'' -* Let ''P0 = B + hash(a·B || k)·G'' -* For additional outputs: -** Increment ''k'' by one (''k++'') -** Let ''Pi = B + hash(a·B || k)·G'' +* Let ''Pk = B + hash(a·B || k)·G = B + hash(b.A || k).G'' Bob detects this output the same as before by searching for ''P0 = B + hash(b·A || 0)·G''. Once he detects the first output, he must: @@ -86,7 +82,7 @@ Since Bob will only perform these subsequent checks after a transaction with at ''' Preventing address reuse ''' -If Alice were to use a different UTXO from the same public key ''A'' for a subsequent payment to Bob, she would end up deriving the same destinations ''Pi''. To prevent this, Alice should include an input hash in the following manner: +If Alice were to use a different UTXO from the same public key ''A'' for a subsequent payment to Bob, she would end up deriving the same destinations ''Pk''. To prevent this, Alice should include an input hash in the following manner: * Let ''input_hash = hash(outpoint || A)'''''Why include A in the input hash calculation?''' By committing to A in input hash, this ensures that the sender cannot maliciously choose a private key ''a′'' in a subsequent transaction where ''a′ = input_hash·a / input_hash′'', which would force address reuse in the protocol. * Let ''P0 = B + hash(input_hash·a·B || 0)·G'' @@ -101,15 +97,15 @@ Alice performs the tweak with the sum of her input private keys in the following * Let ''a = a1 + a2 + ... + an'' * Let ''input_hash = hash(outpointL || (a·G))'', where ''outpointL'' is the smallest outpoint lexicographically'''Why use the lexicographically smallest outpoint for the hash?''' Recall that the purpose of including the input hash is so that the sender and receiver can both come up with a deterministic nonce that ensures that a unique address is generated each time, even when reusing the same scriptPubKey as an input. Choosing the smallest outpoint lexicographically satisfies this requirement, while also ensuring that the generated output is not dependent on the final ordering of inputs in the transaction. Using a single outpoint also works well with memory constrained devices (such as hardware signing devices) as it does not require the device to have the entire transaction in memory in order to generate the silent payment output. -* Let ''P0 = B + hash(input_hash·a·B || 0)·G'' +* Let ''Pk = B + hash(input_hash·a·B || k)·G'' ''' Spend and Scan Key ''' Since Bob needs his private key ''b'' to check for incoming payments, this requires ''b'' to be exposed to an online device. To minimize the risks involved, Bob can instead publish an address of the form ''(Bscan, Bspend)''. This allows Bob to keep ''bspend'' in offline cold storage and perform the scanning with the public key ''Bspend'' and private key ''bscan''. Alice performs the tweak using both of Bob's public keys in the following manner: -* Let ''P0 = Bspend + hash(input_hash·a·Bscan || 0)·G'' +* Let ''Pk = Bspend + hash(input_hash·a·Bscan || k)·G'' -Bob detects this payment by calculating ''P0 = Bspend + hash(input_hash·bscan·A || 0)·G'' with his online device and can spend from his cold storage signing device using ''(bspend + hash(input_hash·bscan·A || 0)) mod n'' as the private key. +Bob detects this payment by calculating ''Pk = Bspend + hash(input_hash·bscan·A || k)·G'' with his online device and can spend from his cold storage signing device using ''(bspend + hash(input_hash·bscan·A || k)) mod n'' as the private key. ''' Labels ''' @@ -120,8 +116,8 @@ For a single silent payment address of the form ''(Bscan, Bspend Alice performs the tweak as before using one of the published ''(Bscan, Bm)'' pairs. Bob detects the labeled payment in the following manner: -* Let ''P0 = Bspend + hash(input_hash·bscan·A || 0)·G'' -* Subtract ''P0'' from each of the transaction outputs and check if the remainder matches any of the labels (''hash(bscan || 1)·G'', ''hash(bscan || 2)·G'' etc.) that the wallet has previously used +* Let ''Pk = Bspend + hash(input_hash·bscan·A || k)·G'' +* Subtract ''Pk'' from each of the transaction outputs and check if the remainder matches any of the labels (''hash(bscan || 1)·G'', ''hash(bscan || 2)·G'' etc.) that the wallet has previously used It is important to note that an outside observer can easily deduce that each published ''(Bscan, Bm)'' pair is owned by the same entity as each published address will have ''Bscan'' in common. As such, labels are not meant as a way for Bob to manage separate identities, but rather a way for Bob to determine the source of an incoming payment. @@ -136,9 +132,9 @@ While the use of labels is optional, every receiving silent payments wallet shou We use the following functions and conventions: * ''outpoint'' (36 bytes): the COutPoint of an input (32-byte txid, least significant byte first || 4-byte vout, least significant byte first)'''Why are outpoints little-endian?''' Despite using big endian throughout the rest of the BIP, outpoints are sorted and hashed matching their transaction serialization, which is little-endian. This allows a wallet to parse a serialized transaction for use in silent payments without needing to re-order the bytes when computing the input hash. Note: despite outpoints being stored and serialized as little-endian, the transaction hash (txid) is always displayed as big-endian. -* ser32(i): serializes a 32-bit unsigned integer ''i'' as a 4-byte sequence, most significant byte first. -* ser256(p): serializes the integer p as a 32-byte sequence, most significant byte first. -* serP(P): serializes the coordinate pair P = (x,y) as a byte sequence using SEC1's compressed form: (0x02 or 0x03) || ser256(x), where the header byte depends on the parity of the omitted Y coordinate. +* ''ser32(i)'': serializes a 32-bit unsigned integer ''i'' as a 4-byte sequence, most significant byte first. +* ''ser256(p)'': serializes the integer ''p'' as a 32-byte sequence, most significant byte first. +* ''serP(P)'': serializes the coordinate pair P = (x,y) as a byte sequence using SEC1's compressed form: ''(0x02 or 0x03) || ser256(x)'', where the header byte depends on the parity of the omitted Y coordinate. For everything not defined above, we use the notation from [https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#specification BIP340]. This includes the ''hashtag(x)'' notation to refer to ''SHA256(SHA256(tag) || SHA256(tag) || x)''. @@ -198,10 +194,10 @@ A silent payment address is constructed in the following manner: * Let ''Bscan, bscan = Receiver's scan public key and corresponding private key'' * Let ''Bspend, bspend = Receiver's spend public key and corresponding private key'' -* Let ''Bm = Bspend + hashBIP0352/Label(ser256(bscan) || ser32(m))·G'', where ''hashBIP0352/Label(ser256(bscan) || ser32(m))·G'' is an optional integer tweak for labeling +* Let ''Bm = Bspend + label_tweak.G'', where ''label_tweak = hashBIP0352/Label(ser256(bscan) || ser32(m))'' is an optional integer tweak for labeling ** If no label is applied then ''Bm = Bspend'' * The final address is a [https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki Bech32m] encoding of: -** The human-readable part "sp" for mainnet, "tsp" for testnets (e.g. signet, testnet) +** The human-readable part "sp" for mainnet, "tsp" for testnets (e.g. signet, testnet) ** The data-part values: *** The character "q", to represent a silent payment address of version 0 *** The 66-byte concatenation of the receiver's public keys, ''serP(Bscan) || serP(Bm)'' @@ -212,7 +208,7 @@ Note: [https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki BIP173] im * HRP [2-3 characters] * separator [1 character] * version [1-2 characters] -* payload, 66 bytes concatenated pubkeys [ceil(66*8/5) = 106 characters] +* payload, 66 bytes concatenated pubkeys ''[ceil(66*8/5) = 106'' characters] * checksum [6 characters] @@ -240,7 +236,7 @@ For all of the output types listed, only X-only and compressed public keys are p scriptPubKey: 1 <32-byte-x-only-key> (0x5120{32-byte-x-only-key}) -The sender uses the private key corresponding to the taproot output key (i.e. the tweaked private key). This can be a single private key or an aggregate key (e.g. taproot outputs using MuSig or FROST)'''Are key aggregation techniques like FROST and MuSig supported?''' While we do not recommend it due to lack of a security proof (except if all participants are trusted or are the same entity), any taproot output able to do a key path theoretically is supported. Any offline key aggregation technique can be used, such as FROST or MuSig. This would require participants to perform the ECDH step collaboratively e.g. ''ECDH = a1·Bscan + a2·Bscan + ... + at·Bscan'' and ''P = Bspend + hash(input_hash·ECDH || 0)·G''. Additionally, it may be necessary for the participants to provide a DLEQ proof to ensure they are not acting maliciously.. The receiver obtains the public key from the ''scriptPubKey'' (i.e. the taproot output key). +The sender uses the private key corresponding to the taproot output key (i.e. the tweaked private key). This can be a single private key or an aggregate key (e.g. taproot outputs using MuSig or FROST)'''Are key aggregation techniques like FROST and MuSig supported?''' While we do not recommend it due to lack of a security proof (except if all participants are trusted or are the same entity), any taproot output able to do a key path theoretically is supported. Any offline key aggregation technique can be used, such as FROST or MuSig. This would require participants to perform the ECDH step collaboratively, e.g. ''ECDH = a1·Bscan + a2·Bscan + ... + at·Bscan'' and ''P = Bspend + hash(input_hash·ECDH || 0)·G''. Additionally, it may be necessary for the participants to provide a DLEQ proof to ensure they are not acting maliciously.. The receiver obtains the public key from the ''scriptPubKey'' (i.e. the taproot output key). '' Script path spend '' @@ -251,7 +247,7 @@ The sender uses the private key corresponding to the taproot output key (i.e. th Same as a keypath spend, the sender MUST use the private key corresponding to the taproot output key. If this key is not available, the output cannot be included as an input to the transaction. Same as a keypath spend, the receiver obtains the public key from the ''scriptPubKey'' (i.e. the taproot output key)''' Why not skip all taproot script path spends? ''' This causes malleability issues for CoinJoins. If the silent payments protocol skipped taproot script path spends, this would allow an attacker to join a CoinJoin round, participate in deriving the silent payment address using the tweaked private key for a key path spend, and then broadcast their own version of the transaction using the script path spend. If the receiver were to only consider key path spends, they would skip the attacker's script path spend input when deriving the shared secret and not be able to find the funds. Additionally, there may be scenarios where the sender can perform ECDH with the key path private key but spends the output using the script path.. -The one exception is script path spends that use NUMS point ''H'' as their internal key (where ''H'' is constructed by taking the hash of the standard uncompressed encoding of the secp256k1 base point ''G'' as X coordinate, see [https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs BIP341: Constructing and spending Taproot outputs] for more details), in which case the input will be skipped for the purposes of shared secret derivation'''Why skip outputs with H as the internal taproot key?''' If use cases get popularized where the taproot key path cannot be used, these outputs can still be included without getting in the way of making a silent payment, provided they specifically use H as their internal taproot key.. The receiver determines whether or not to skip the input by checking in the control block if the taproot internal key is equal to ''H''. +The one exception is script path spends that use the NUMS point ''H'' as their internal key (where ''H'' is constructed by taking the hash of the standard uncompressed encoding of the secp256k1 base point ''G'' as X coordinate, see [https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs BIP341: Constructing and spending Taproot outputs] for more details), in which case the input will be skipped for the purposes of shared secret derivation'''Why skip outputs with H as the internal taproot key?''' If use cases get popularized where the taproot key path cannot be used, these outputs can still be included without getting in the way of making a silent payment, provided they specifically use H as their internal taproot key.. The receiver determines whether or not to skip the input by checking in the control block if the taproot internal key is equal to ''H''. ''' P2WPKH ''' @@ -300,8 +296,8 @@ After the inputs have been selected, the sender can create one or more outputs f ** If ''a = 0'', fail * 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. +* Group receiver silent payment addresses by ''Bscan'', where 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'' @@ -309,9 +305,9 @@ After the inputs have been selected, the sender can create one or more outputs f ** For each ''Bm'' in the group: *** 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 -*** Let ''Pmn = Bm + tk·G'' -*** Encode ''Pmn'' as a [https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki BIP341] taproot output -*** Optionally, repeat with k++ to create additional outputs for the current ''Bm'' +*** Let ''Pkm = Bm + tk·G'' +*** Encode ''Pkm'' as a [https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki BIP341] taproot output +*** Optionally, repeat with ''k++'' to create additional outputs for the current ''Bm'' *** If no additional outputs are required, continue to the next ''Bm'' with ''k++''''' Why not re-use ''tk'' when paying different labels to the same receiver?''' If paying the same entity but to two separate labeled addresses in the same transaction without incrementing ''k'', an outside observer could subtract the two output values and observe that this value is the same as the difference between two published silent payment addresses and learn who the recipient is. ** Optionally, if the sending wallet implements receiving silent payments, it can create change outputs by sending to its own silent payment address using label ''m = 0'', following the steps above ** If ''k > Kmax'', fail.. Thus, any transaction that generates more than this many keys for a single receiver is to be considered invalid under this specification. We achieve this by mandating the sender to keep track of the number of keys ''k'' they generated for a single group and fail whenever ''k'' exceeds ''Kmax''.