From a9a5fe8e28f2f4dd53fdfb4ce797b23b4452f0a6 Mon Sep 17 00:00:00 2001
From: Sebastian Falbesoner <sebastian.falbesoner@gmail.com>
Date: Wed, 27 Sep 2023 17:22:14 +0200
Subject: [PATCH 01/13] build: add skeleton for new silentpayments (BIP352)
 module

---
 CMakeLists.txt                                |  6 ++++++
 Makefile.am                                   |  4 ++++
 configure.ac                                  | 10 ++++++++++
 include/secp256k1_silentpayments.h            | 20 +++++++++++++++++++
 .../silentpayments/Makefile.am.include        |  2 ++
 src/modules/silentpayments/main_impl.h        | 16 +++++++++++++++
 src/secp256k1.c                               |  4 ++++
 7 files changed, 62 insertions(+)
 create mode 100644 include/secp256k1_silentpayments.h
 create mode 100644 src/modules/silentpayments/Makefile.am.include
 create mode 100644 src/modules/silentpayments/main_impl.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9ef7defe51..d824a6db3e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -60,9 +60,14 @@ option(SECP256K1_ENABLE_MODULE_RECOVERY "Enable ECDSA pubkey recovery module." O
 option(SECP256K1_ENABLE_MODULE_EXTRAKEYS "Enable extrakeys module." ON)
 option(SECP256K1_ENABLE_MODULE_SCHNORRSIG "Enable schnorrsig module." ON)
 option(SECP256K1_ENABLE_MODULE_ELLSWIFT "Enable ElligatorSwift module." ON)
+option(SECP256K1_ENABLE_MODULE_SILENTPAYMENTS "Enable Silent Payments module." OFF)
 
 # Processing must be done in a topological sorting of the dependency graph
 # (dependent module first).
+if(SECP256K1_ENABLE_MODULE_SILENTPAYMENTS)
+  add_compile_definitions(ENABLE_MODULE_SILENTPAYMENTS=1)
+endif()
+
 if(SECP256K1_ENABLE_MODULE_ELLSWIFT)
   add_compile_definitions(ENABLE_MODULE_ELLSWIFT=1)
 endif()
@@ -292,6 +297,7 @@ message("  ECDSA pubkey recovery ............... ${SECP256K1_ENABLE_MODULE_RECOV
 message("  extrakeys ........................... ${SECP256K1_ENABLE_MODULE_EXTRAKEYS}")
 message("  schnorrsig .......................... ${SECP256K1_ENABLE_MODULE_SCHNORRSIG}")
 message("  ElligatorSwift ...................... ${SECP256K1_ENABLE_MODULE_ELLSWIFT}")
+message("  Silent Payments ..................... ${SECP256K1_ENABLE_MODULE_SILENTPAYMENTS}")
 message("Parameters:")
 message("  ecmult window size .................. ${SECP256K1_ECMULT_WINDOW_SIZE}")
 message("  ecmult gen precision bits ........... ${SECP256K1_ECMULT_GEN_PREC_BITS}")
diff --git a/Makefile.am b/Makefile.am
index 5498617915..d9295799bc 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -271,3 +271,7 @@ endif
 if ENABLE_MODULE_ELLSWIFT
 include src/modules/ellswift/Makefile.am.include
 endif
+
+if ENABLE_MODULE_SILENTPAYMENTS
+include src/modules/silentpayments/Makefile.am.include
+endif
diff --git a/configure.ac b/configure.ac
index 158ed5d769..7113752b50 100644
--- a/configure.ac
+++ b/configure.ac
@@ -188,6 +188,10 @@ AC_ARG_ENABLE(module_ellswift,
     AS_HELP_STRING([--enable-module-ellswift],[enable ElligatorSwift module [default=yes]]), [],
     [SECP_SET_DEFAULT([enable_module_ellswift], [yes], [yes])])
 
+AC_ARG_ENABLE(module_silentpayments,
+    AS_HELP_STRING([--enable-module-silentpayments],[enable Silent Payments module [default=no]]), [],
+    [SECP_SET_DEFAULT([enable_module_silentpayments], [no], [yes])])
+
 AC_ARG_ENABLE(external_default_callbacks,
     AS_HELP_STRING([--enable-external-default-callbacks],[enable external default callback functions [default=no]]), [],
     [SECP_SET_DEFAULT([enable_external_default_callbacks], [no], [no])])
@@ -389,6 +393,10 @@ SECP_CFLAGS="$SECP_CFLAGS $WERROR_CFLAGS"
 
 # Processing must be done in a reverse topological sorting of the dependency graph
 # (dependent module first).
+if test x"$enable_module_silentpayments" = x"yes"; then
+  SECP_CONFIG_DEFINES="$SECP_CONFIG_DEFINES -DENABLE_MODULE_SILENTPAYMENTS=1"
+fi
+
 if test x"$enable_module_ellswift" = x"yes"; then
   SECP_CONFIG_DEFINES="$SECP_CONFIG_DEFINES -DENABLE_MODULE_ELLSWIFT=1"
 fi
@@ -450,6 +458,7 @@ AM_CONDITIONAL([ENABLE_MODULE_RECOVERY], [test x"$enable_module_recovery" = x"ye
 AM_CONDITIONAL([ENABLE_MODULE_EXTRAKEYS], [test x"$enable_module_extrakeys" = x"yes"])
 AM_CONDITIONAL([ENABLE_MODULE_SCHNORRSIG], [test x"$enable_module_schnorrsig" = x"yes"])
 AM_CONDITIONAL([ENABLE_MODULE_ELLSWIFT], [test x"$enable_module_ellswift" = x"yes"])
+AM_CONDITIONAL([ENABLE_MODULE_SILENTPAYMENTS], [test x"$enable_module_silentpayments" = x"yes"])
 AM_CONDITIONAL([USE_EXTERNAL_ASM], [test x"$enable_external_asm" = x"yes"])
 AM_CONDITIONAL([USE_ASM_ARM], [test x"$set_asm" = x"arm32"])
 AM_CONDITIONAL([BUILD_WINDOWS], [test "$build_windows" = "yes"])
@@ -472,6 +481,7 @@ echo "  module recovery         = $enable_module_recovery"
 echo "  module extrakeys        = $enable_module_extrakeys"
 echo "  module schnorrsig       = $enable_module_schnorrsig"
 echo "  module ellswift         = $enable_module_ellswift"
+echo "  module silentpayments   = $enable_module_silentpayments"
 echo
 echo "  asm                     = $set_asm"
 echo "  ecmult window size      = $set_ecmult_window"
diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
new file mode 100644
index 0000000000..dd7146648a
--- /dev/null
+++ b/include/secp256k1_silentpayments.h
@@ -0,0 +1,20 @@
+#ifndef SECP256K1_SILENTPAYMENTS_H
+#define SECP256K1_SILENTPAYMENTS_H
+
+#include "secp256k1.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* TODO: add module description */
+
+/* TODO: add function API for sender side. */
+
+/* TODO: add function API for receiver side. */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SECP256K1_SILENTPAYMENTS_H */
diff --git a/src/modules/silentpayments/Makefile.am.include b/src/modules/silentpayments/Makefile.am.include
new file mode 100644
index 0000000000..842a33e2d9
--- /dev/null
+++ b/src/modules/silentpayments/Makefile.am.include
@@ -0,0 +1,2 @@
+include_HEADERS += include/secp256k1_silentpayments.h
+noinst_HEADERS += src/modules/silentpayments/main_impl.h
diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
new file mode 100644
index 0000000000..f8ccdd7baa
--- /dev/null
+++ b/src/modules/silentpayments/main_impl.h
@@ -0,0 +1,16 @@
+/***********************************************************************
+ * Distributed under the MIT software license, see the accompanying    *
+ * file COPYING or https://www.opensource.org/licenses/mit-license.php.*
+ ***********************************************************************/
+
+#ifndef SECP256K1_MODULE_SILENTPAYMENTS_MAIN_H
+#define SECP256K1_MODULE_SILENTPAYMENTS_MAIN_H
+
+#include "../../../include/secp256k1.h"
+#include "../../../include/secp256k1_silentpayments.h"
+
+/* TODO: implement functions for sender side. */
+
+/* TODO: implement functions for receiver side. */
+
+#endif
diff --git a/src/secp256k1.c b/src/secp256k1.c
index 15a5eede67..0ca054d4d9 100644
--- a/src/secp256k1.c
+++ b/src/secp256k1.c
@@ -804,3 +804,7 @@ int secp256k1_tagged_sha256(const secp256k1_context* ctx, unsigned char *hash32,
 #ifdef ENABLE_MODULE_ELLSWIFT
 # include "modules/ellswift/main_impl.h"
 #endif
+
+#ifdef ENABLE_MODULE_SILENTPAYMENTS
+# include "modules/silentpayments/main_impl.h"
+#endif

From 6e3ed2d5df1713f774ed7970a1d1c77ff53a6ab7 Mon Sep 17 00:00:00 2001
From: Sebastian Falbesoner <sebastian.falbesoner@gmail.com>
Date: Sat, 23 Dec 2023 16:28:18 +0100
Subject: [PATCH 02/13] doc: add module description for
 secp256k1-silentpayments

---
 include/secp256k1_silentpayments.h | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
index dd7146648a..a79fc516e5 100644
--- a/include/secp256k1_silentpayments.h
+++ b/include/secp256k1_silentpayments.h
@@ -7,7 +7,26 @@
 extern "C" {
 #endif
 
-/* TODO: add module description */
+/* This module provides an implementation for the ECC related parts of
+ * Silent Payments, as specified in BIP352. This particularly involves
+ * the creation of input tweak data by summing up private or public keys
+ * and the derivation of a shared secret using Elliptic Curve Diffie-Hellman.
+ * Combined are either:
+ *   - spender's private keys and receiver's public key (a * B, sender side)
+ *   - spender's public keys and receiver's private key (A * b, receiver side)
+ * With this result, the necessary key material for ultimately creating/scanning
+ * or spending Silent Payment outputs can be determined.
+ *
+ * Note that this module is _not_ a full implementation of BIP352, as it
+ * inherently doesn't deal with higher-level concepts like addresses, output
+ * script types or transactions. The intent is to provide cryptographical
+ * helpers for low-level calculations that are most error-prone to custom
+ * implementations (e.g. enforcing the right y-parity for key material, ECDH
+ * calculation etc.). For any wallet software already using libsecp256k1, this
+ * API should provide all the functions needed for a Silent Payments
+ * implementation without the need for any further manual elliptic-curve
+ * operations.
+ */
 
 /* TODO: add function API for sender side. */
 

From 81d13038d51fbab400cc247512fbaf939db08d49 Mon Sep 17 00:00:00 2001
From: Sebastian Falbesoner <sebastian.falbesoner@gmail.com>
Date: Fri, 22 Dec 2023 13:40:09 +0100
Subject: [PATCH 03/13] silentpayments: add private tweak data creation routine

---
 include/secp256k1_silentpayments.h     | 39 ++++++++++-
 src/modules/silentpayments/main_impl.h | 89 +++++++++++++++++++++++++-
 2 files changed, 126 insertions(+), 2 deletions(-)

diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
index a79fc516e5..7c6274893f 100644
--- a/include/secp256k1_silentpayments.h
+++ b/include/secp256k1_silentpayments.h
@@ -28,7 +28,44 @@ extern "C" {
  * operations.
  */
 
-/* TODO: add function API for sender side. */
+/** Create Silent Payment tweak data from input private keys.
+ *
+ * Given a list of n private keys a_1...a_n (one for each silent payment
+ * eligible input to spend) and a serialized outpoint_smallest, compute
+ * the corresponding input private keys tweak data:
+ *
+ * a_sum = a_1 + a_2 + ... + a_n
+ * input_hash = hash(outpoint_smallest || (a_sum * G))
+ *
+ * If necessary, the private keys are negated to enforce the right y-parity.
+ * For that reason, the private keys have to be passed in via two different parameter
+ * pairs, depending on whether they were used for creating taproot outputs or not.
+ * The resulting data is needed to create a shared secret for the sender side.
+ *
+ *  Returns: 1 if shared secret creation was successful. 0 if an error occured.
+ *  Args:                  ctx: pointer to a context object
+ *  Out:                 a_sum: pointer to the resulting 32-byte private key sum
+ *                  input_hash: pointer to the resulting 32-byte input hash
+ *  In:          plain_seckeys: pointer to an array of pointers to 32-byte private keys
+ *                              of non-taproot inputs (can be NULL if no private keys of
+ *                              non-taproot inputs are used)
+ *             n_plain_seckeys: the number of sender's non-taproot input private keys
+ *             taproot_seckeys: pointer to an array of pointers to 32-byte private keys
+ *                              of taproot inputs (can be NULL if no private keys of
+ *                              taproot inputs are used)
+ *           n_taproot_seckeys: the number of sender's taproot input private keys
+ *         outpoint_smallest36: serialized smallest outpoint
+ */
+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_create_private_tweak_data(
+    const secp256k1_context *ctx,
+    unsigned char *a_sum,
+    unsigned char *input_hash,
+    const unsigned char * const *plain_seckeys,
+    size_t n_plain_seckeys,
+    const unsigned char * const *taproot_seckeys,
+    size_t n_taproot_seckeys,
+    const unsigned char *outpoint_smallest36
+) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(8);
 
 /* TODO: add function API for receiver side. */
 
diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
index f8ccdd7baa..11672a8696 100644
--- a/src/modules/silentpayments/main_impl.h
+++ b/src/modules/silentpayments/main_impl.h
@@ -9,7 +9,94 @@
 #include "../../../include/secp256k1.h"
 #include "../../../include/secp256k1_silentpayments.h"
 
-/* TODO: implement functions for sender side. */
+/** Set hash state to the BIP340 tagged hash midstate for "BIP0352/Inputs". */
+static void secp256k1_silentpayments_sha256_init_inputs(secp256k1_sha256* hash) {
+    secp256k1_sha256_initialize(hash);
+    hash->s[0] = 0xd4143ffcul;
+    hash->s[1] = 0x012ea4b5ul;
+    hash->s[2] = 0x36e21c8ful;
+    hash->s[3] = 0xf7ec7b54ul;
+    hash->s[4] = 0x4dd4e2acul;
+    hash->s[5] = 0x9bcaa0a4ul;
+    hash->s[6] = 0xe244899bul;
+    hash->s[7] = 0xcd06903eul;
+
+    hash->bytes = 64;
+}
+
+static void secp256k1_silentpayments_calculate_input_hash(unsigned char *input_hash, const unsigned char *outpoint_smallest36, secp256k1_ge *pubkey_sum) {
+    secp256k1_sha256 hash;
+    unsigned char pubkey_sum_ser[33];
+    size_t ser_size;
+    int ser_ret;
+
+    secp256k1_silentpayments_sha256_init_inputs(&hash);
+    secp256k1_sha256_write(&hash, outpoint_smallest36, 36);
+    ser_ret = secp256k1_eckey_pubkey_serialize(pubkey_sum, pubkey_sum_ser, &ser_size, 1);
+    VERIFY_CHECK(ser_ret && ser_size == sizeof(pubkey_sum_ser));
+    (void)ser_ret;
+    secp256k1_sha256_write(&hash, pubkey_sum_ser, sizeof(pubkey_sum_ser));
+    secp256k1_sha256_finalize(&hash, input_hash);
+}
+
+int secp256k1_silentpayments_create_private_tweak_data(const secp256k1_context *ctx, unsigned char *a_sum, unsigned char *input_hash, const unsigned char * const *plain_seckeys, size_t n_plain_seckeys, const unsigned char * const *taproot_seckeys, size_t n_taproot_seckeys, const unsigned char *outpoint_smallest36) {
+    size_t i;
+    secp256k1_scalar a_sum_scalar, addend;
+    secp256k1_ge A_sum_ge;
+    secp256k1_gej A_sum_gej;
+
+    /* Sanity check inputs. */
+    VERIFY_CHECK(ctx != NULL);
+    ARG_CHECK(a_sum != NULL);
+    memset(a_sum, 0, 32);
+    ARG_CHECK(input_hash != NULL);
+    memset(input_hash, 0, 32);
+    ARG_CHECK(secp256k1_ecmult_gen_context_is_built(&ctx->ecmult_gen_ctx));
+    ARG_CHECK(plain_seckeys == NULL || n_plain_seckeys >= 1);
+    ARG_CHECK(taproot_seckeys == NULL || n_taproot_seckeys >= 1);
+    ARG_CHECK((plain_seckeys != NULL) || (taproot_seckeys != NULL));
+    ARG_CHECK((n_plain_seckeys + n_taproot_seckeys) >= 1);
+    ARG_CHECK(outpoint_smallest36 != NULL);
+
+    /* Compute input private keys sum: a_sum = a_1 + a_2 + ... + a_n */
+    a_sum_scalar = secp256k1_scalar_zero;
+    for (i = 0; i < n_plain_seckeys; i++) {
+        int ret = secp256k1_scalar_set_b32_seckey(&addend, plain_seckeys[i]);
+        VERIFY_CHECK(ret);
+        (void)ret;
+
+        secp256k1_scalar_add(&a_sum_scalar, &a_sum_scalar, &addend);
+        VERIFY_CHECK(!secp256k1_scalar_is_zero(&a_sum_scalar));
+    }
+    /* private keys used for taproot outputs have to be negated if they resulted in an odd point */
+    for (i = 0; i < n_taproot_seckeys; i++) {
+        secp256k1_ge addend_point;
+        int ret = secp256k1_ec_pubkey_create_helper(&ctx->ecmult_gen_ctx, &addend, &addend_point, taproot_seckeys[i]);
+        VERIFY_CHECK(ret);
+        (void)ret;
+        /* declassify addend_point to allow using it as a branch point (this is fine because addend_point is not a secret) */
+        secp256k1_declassify(ctx, &addend_point, sizeof(addend_point));
+        secp256k1_fe_normalize_var(&addend_point.y);
+        if (secp256k1_fe_is_odd(&addend_point.y)) {
+            secp256k1_scalar_negate(&addend, &addend);
+        }
+
+        secp256k1_scalar_add(&a_sum_scalar, &a_sum_scalar, &addend);
+        VERIFY_CHECK(!secp256k1_scalar_is_zero(&a_sum_scalar));
+    }
+    if (secp256k1_scalar_is_zero(&a_sum_scalar)) {
+        /* TODO: do we need a special error return code for this case? */
+        return 0;
+    }
+    secp256k1_scalar_get_b32(a_sum, &a_sum_scalar);
+
+    /* Compute input_hash = hash(outpoint_L || (a_sum * G)) */
+    secp256k1_ecmult_gen(&ctx->ecmult_gen_ctx, &A_sum_gej, &a_sum_scalar);
+    secp256k1_ge_set_gej(&A_sum_ge, &A_sum_gej);
+    secp256k1_silentpayments_calculate_input_hash(input_hash, outpoint_smallest36, &A_sum_ge);
+
+    return 1;
+}
 
 /* TODO: implement functions for receiver side. */
 

From 98f5ba4aa69e2b86d32fab649e65730a0a998a12 Mon Sep 17 00:00:00 2001
From: Sebastian Falbesoner <sebastian.falbesoner@gmail.com>
Date: Mon, 18 Dec 2023 17:46:11 +0100
Subject: [PATCH 04/13] silentpayments: add public tweak data creation routine

---
 include/secp256k1_silentpayments.h     | 37 +++++++++++++++++++++++-
 src/modules/silentpayments/main_impl.h | 39 +++++++++++++++++++++++++-
 2 files changed, 74 insertions(+), 2 deletions(-)

diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
index 7c6274893f..bf78037a61 100644
--- a/include/secp256k1_silentpayments.h
+++ b/include/secp256k1_silentpayments.h
@@ -67,7 +67,42 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_create_p
     const unsigned char *outpoint_smallest36
 ) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(8);
 
-/* TODO: add function API for receiver side. */
+/** Create Silent Payment tweak data from input public keys.
+ *
+ * Given a list of n public keys A_1...A_n (one for each silent payment
+ * eligible input to spend) and a serialized outpoint_smallest, compute
+ * the corresponding input public keys tweak data:
+ *
+ * A_sum = A_1 + A_2 + ... + A_n
+ * input_hash = hash(outpoint_lowest || A_sum)
+ *
+ * The public keys have to be passed in via two different parameter pairs,
+ * one for regular and one for x-only public keys, in order to avoid the need
+ * of users converting to a common pubkey format before calling this function.
+ * The resulting data is needed to create a shared secret for the receiver's side.
+ *
+ *  Returns: 1 if tweak data creation was successful. 0 if an error occured.
+ *  Args:                  ctx: pointer to a context object
+ *  Out:                 A_sum: pointer to the resulting public keys sum
+ *                  input_hash: pointer to the resulting 32-byte input hash
+ *  In:          plain_pubkeys: pointer to an array of pointers to non-taproot
+ *                              public keys (can be NULL if no non-taproot inputs are used)
+ *             n_plain_pubkeys: the number of non-taproot input public keys
+ *               xonly_pubkeys: pointer to an array of pointers to taproot x-only
+ *                              public keys (can be NULL if no taproot inputs are used)
+ *             n_xonly_pubkeys: the number of taproot input public keys
+ *         outpoint_smallest36: serialized smallest outpoint
+ */
+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_create_public_tweak_data(
+    const secp256k1_context *ctx,
+    secp256k1_pubkey *A_sum,
+    unsigned char *input_hash,
+    const secp256k1_pubkey * const *plain_pubkeys,
+    size_t n_plain_pubkeys,
+    const secp256k1_xonly_pubkey * const *xonly_pubkeys,
+    size_t n_xonly_pubkeys,
+    const unsigned char *outpoint_smallest36
+) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(8);
 
 #ifdef __cplusplus
 }
diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
index 11672a8696..fca3dfb64c 100644
--- a/src/modules/silentpayments/main_impl.h
+++ b/src/modules/silentpayments/main_impl.h
@@ -98,6 +98,43 @@ int secp256k1_silentpayments_create_private_tweak_data(const secp256k1_context *
     return 1;
 }
 
-/* TODO: implement functions for receiver side. */
+int secp256k1_silentpayments_create_public_tweak_data(const secp256k1_context *ctx, secp256k1_pubkey *A_sum, unsigned char *input_hash, const secp256k1_pubkey * const *plain_pubkeys, size_t n_plain_pubkeys, const secp256k1_xonly_pubkey * const *xonly_pubkeys, size_t n_xonly_pubkeys, const unsigned char *outpoint_smallest36) {
+    size_t i;
+    secp256k1_ge A_sum_ge, addend;
+    secp256k1_gej A_sum_gej;
+
+    /* Sanity check inputs */
+    VERIFY_CHECK(ctx != NULL);
+    ARG_CHECK(A_sum != NULL);
+    ARG_CHECK(input_hash != NULL);
+    memset(input_hash, 0, 32);
+    ARG_CHECK(plain_pubkeys == NULL || n_plain_pubkeys >= 1);
+    ARG_CHECK(xonly_pubkeys == NULL || n_xonly_pubkeys >= 1);
+    ARG_CHECK((plain_pubkeys != NULL) || (xonly_pubkeys != NULL));
+    ARG_CHECK((n_plain_pubkeys + n_xonly_pubkeys) >= 1);
+    ARG_CHECK(outpoint_smallest36 != NULL);
+
+    /* Compute input public keys sum: A_sum = A_1 + A_2 + ... + A_n */
+    secp256k1_gej_set_infinity(&A_sum_gej);
+    for (i = 0; i < n_plain_pubkeys; i++) {
+        secp256k1_pubkey_load(ctx, &addend, plain_pubkeys[i]);
+        secp256k1_gej_add_ge(&A_sum_gej, &A_sum_gej, &addend);
+    }
+    for (i = 0; i < n_xonly_pubkeys; i++) {
+        secp256k1_xonly_pubkey_load(ctx, &addend, xonly_pubkeys[i]);
+        secp256k1_gej_add_ge(&A_sum_gej, &A_sum_gej, &addend);
+    }
+    if (secp256k1_gej_is_infinity(&A_sum_gej)) {
+        /* TODO: do we need a special error return code for this case? */
+        return 0;
+    }
+    secp256k1_ge_set_gej(&A_sum_ge, &A_sum_gej);
+    secp256k1_pubkey_save(A_sum, &A_sum_ge);
+
+    /* Compute input_hash = hash(outpoint_L || A_sum) */
+    secp256k1_silentpayments_calculate_input_hash(input_hash, outpoint_smallest36, &A_sum_ge);
+
+    return 1;
+}
 
 #endif

From 842e5bf427d63b86d55aef3c234c05b42c2a6ad7 Mon Sep 17 00:00:00 2001
From: Sebastian Falbesoner <sebastian.falbesoner@gmail.com>
Date: Fri, 16 Feb 2024 12:17:11 +0100
Subject: [PATCH 05/13] silentpayments: add tweaked pubkey creation routine
 (for light clients / sp index)

---
 include/secp256k1_silentpayments.h     | 22 ++++++++++++++++++++++
 src/modules/silentpayments/main_impl.h | 16 ++++++++++++++++
 2 files changed, 38 insertions(+)

diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
index bf78037a61..9554976b79 100644
--- a/include/secp256k1_silentpayments.h
+++ b/include/secp256k1_silentpayments.h
@@ -104,6 +104,28 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_create_p
     const unsigned char *outpoint_smallest36
 ) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(8);
 
+/** Create Silent Payment tweaked public key from public tweak data.
+ *
+ * Given public tweak data (public keys sum and input hash), calculate the
+ * corresponding tweaked public key:
+ *
+ * A_tweaked = input_hash * A_sum
+ *
+ * The resulting data is useful for light clients and silent payment indexes.
+ *
+ *  Returns: 1 if tweaked public key creation was successful. 0 if an error occured.
+ *  Args:              ctx: pointer to a context object
+ *  Out:         A_tweaked: pointer to the resulting tweaked public key
+ *  In:              A_sum: pointer to the public keys sum
+ *              input_hash: pointer to the 32-byte input hash
+ */
+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_create_tweaked_pubkey(
+    const secp256k1_context *ctx,
+    secp256k1_pubkey *A_tweaked,
+    const secp256k1_pubkey *A_sum,
+    const unsigned char *input_hash
+) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
index fca3dfb64c..b64463012b 100644
--- a/src/modules/silentpayments/main_impl.h
+++ b/src/modules/silentpayments/main_impl.h
@@ -137,4 +137,20 @@ int secp256k1_silentpayments_create_public_tweak_data(const secp256k1_context *c
     return 1;
 }
 
+int secp256k1_silentpayments_create_tweaked_pubkey(const secp256k1_context *ctx, secp256k1_pubkey *A_tweaked, const secp256k1_pubkey *A_sum, const unsigned char *input_hash) {
+    /* Sanity check inputs */
+    VERIFY_CHECK(ctx != NULL);
+    ARG_CHECK(A_tweaked != NULL);
+    ARG_CHECK(A_sum != NULL);
+    ARG_CHECK(input_hash != NULL);
+
+    /* Calculate A_tweaked = input_hash * A_sum */
+    *A_tweaked = *A_sum;
+    if (!secp256k1_ec_pubkey_tweak_mul(ctx, A_tweaked, input_hash)) {
+        return 0;
+    }
+
+    return 1;
+}
+
 #endif

From b0e37968b09b830b422bafb60c0e870d611ef6ed Mon Sep 17 00:00:00 2001
From: Sebastian Falbesoner <sebastian.falbesoner@gmail.com>
Date: Thu, 5 Oct 2023 03:14:00 +0200
Subject: [PATCH 06/13] silentpayments: add shared secret creation routine (a*B
 == A*b)

---
 include/secp256k1_silentpayments.h     | 45 ++++++++++++++++++++++
 src/modules/silentpayments/main_impl.h | 52 ++++++++++++++++++++++++++
 2 files changed, 97 insertions(+)

diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
index 9554976b79..b96b32d6ec 100644
--- a/include/secp256k1_silentpayments.h
+++ b/include/secp256k1_silentpayments.h
@@ -2,6 +2,7 @@
 #define SECP256K1_SILENTPAYMENTS_H
 
 #include "secp256k1.h"
+#include "secp256k1_extrakeys.h"
 
 #ifdef __cplusplus
 extern "C" {
@@ -126,6 +127,50 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_create_t
     const unsigned char *input_hash
 ) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
 
+/** Create Silent Payment shared secret.
+ *
+ * Given a public component Pub, a private component sec and an input_hash,
+ * calculate the corresponding shared secret using ECDH:
+ *
+ * shared_secret = (sec * input_hash) * Pub
+ *
+ * What the components should be set to depends on the role of the caller.
+ * For the sender side, the public component is set the recipient's scan public key
+ * B_scan, and the private component is set to the input's private keys sum:
+ *
+ * shared_secret = (a_sum * input_hash) * B_scan   [Sender]
+ *
+ * For the receiver side, the public component is set to the input's public keys sum,
+ * and the private component is set to the receiver's scan private key:
+ *
+ * shared_secret = (b_scan * input_hash) * A_sum   [Receiver, Full node scenario]
+ *
+ * In the "light client" scenario for receivers, the public component is already
+ * tweaked with the input hash: A_tweaked = input_hash * A_sum
+ * In this case, the input_hash parameter should be set to NULL, to signal that
+ * no further tweaking should be done before the ECDH:
+ *
+ * shared_secret = b_scan * A_tweaked   [Receiver, Light client scenario]
+ *
+ * The resulting shared secret is needed as input for creating silent payments
+ * outputs belonging to the same receiver scan public key.
+ *
+ *  Returns: 1 if shared secret creation was successful. 0 if an error occured.
+ *  Args:                  ctx: pointer to a context object
+ *  Out:       shared_secret33: pointer to the resulting 33-byte shared secret
+ *  In:       public_component: pointer to the public component
+ *           private_component: pointer to 32-byte private component
+ *                  input_hash: pointer to 32-byte input hash (can be NULL if the
+ *                              public component is already tweaked with the input hash)
+ */
+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_create_shared_secret(
+    const secp256k1_context *ctx,
+    unsigned char *shared_secret33,
+    const secp256k1_pubkey *public_component,
+    const unsigned char *secret_component,
+    const unsigned char *input_hash
+) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
index b64463012b..b23701e9ac 100644
--- a/src/modules/silentpayments/main_impl.h
+++ b/src/modules/silentpayments/main_impl.h
@@ -7,6 +7,8 @@
 #define SECP256K1_MODULE_SILENTPAYMENTS_MAIN_H
 
 #include "../../../include/secp256k1.h"
+#include "../../../include/secp256k1_ecdh.h"
+#include "../../../include/secp256k1_extrakeys.h"
 #include "../../../include/secp256k1_silentpayments.h"
 
 /** Set hash state to the BIP340 tagged hash midstate for "BIP0352/Inputs". */
@@ -153,4 +155,54 @@ int secp256k1_silentpayments_create_tweaked_pubkey(const secp256k1_context *ctx,
     return 1;
 }
 
+/* secp256k1_ecdh expects a hash function to be passed in or uses its default
+ * hashing function. We don't want to hash the ECDH result, so we define a
+ * custom function which simply returns the pubkey without hashing.
+ */
+static int secp256k1_silentpayments_ecdh_return_pubkey(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data) {
+    secp256k1_ge point;
+    secp256k1_fe x, y;
+    size_t ser_size;
+    int ser_ret;
+
+    (void)data;
+    /* Parse point as group element */
+    if (!secp256k1_fe_set_b32_limit(&x, x32) || !secp256k1_fe_set_b32_limit(&y, y32)) {
+        return 0;
+    }
+    secp256k1_ge_set_xy(&point, &x, &y);
+
+    /* Serialize as compressed pubkey */
+    ser_ret = secp256k1_eckey_pubkey_serialize(&point, output, &ser_size, 1);
+    VERIFY_CHECK(ser_ret && ser_size == 33);
+    (void)ser_ret;
+
+    return 1;
+}
+
+int secp256k1_silentpayments_create_shared_secret(const secp256k1_context *ctx, unsigned char *shared_secret33, const secp256k1_pubkey *public_component, const unsigned char *secret_component, const unsigned char *input_hash) {
+    unsigned char tweaked_secret_component[32];
+
+    /* Sanity check inputs */
+    ARG_CHECK(shared_secret33 != NULL);
+    memset(shared_secret33, 0, 33);
+    ARG_CHECK(public_component != NULL);
+    ARG_CHECK(secret_component != NULL);
+
+    /* Tweak secret component with input hash, if available */
+    memcpy(tweaked_secret_component, secret_component, 32);
+    if (input_hash != NULL) {
+        if (!secp256k1_ec_seckey_tweak_mul(ctx, tweaked_secret_component, input_hash)) {
+            return 0;
+        }
+    }
+
+    /* Compute shared_secret = tweaked_secret_component * Public_component */
+    if (!secp256k1_ecdh(ctx, shared_secret33, public_component, tweaked_secret_component, secp256k1_silentpayments_ecdh_return_pubkey, NULL)) {
+        return 0;
+    }
+
+    return 1;
+}
+
 #endif

From 2a00e12e58b6afd2b9ea9b3999dedde91f8ff82f Mon Sep 17 00:00:00 2001
From: Sebastian Falbesoner <sebastian.falbesoner@gmail.com>
Date: Mon, 22 Jan 2024 18:56:05 +0100
Subject: [PATCH 07/13] silentpayments: add label tweak calculation routine

---
 include/secp256k1_silentpayments.h     | 22 ++++++++++++++
 src/modules/silentpayments/main_impl.h | 41 ++++++++++++++++++++++++++
 2 files changed, 63 insertions(+)

diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
index b96b32d6ec..6ade98b86f 100644
--- a/include/secp256k1_silentpayments.h
+++ b/include/secp256k1_silentpayments.h
@@ -171,6 +171,28 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_create_s
     const unsigned char *input_hash
 ) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
 
+/** Create Silent Payment label tweak and label.
+ *
+ *  Given a recipient's scan private key b_scan and a label integer m, calculate
+ *  the corresponding label tweak and label:
+ *
+ *  label_tweak = hash(b_scan || m)
+ *  label = label_tweak * G
+ *
+ *  Returns: 1 if label tweak and label creation was successful. 0 if an error occured.
+ *  Args:                  ctx: pointer to a context object
+ *  Out:           label_tweak: pointer to the resulting label tweak
+ *   In:  receiver_scan_seckey: pointer to the receiver's scan private key
+ *                           m: label integer (0 is used for change outputs)
+ */
+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_create_label_tweak(
+    const secp256k1_context *ctx,
+    secp256k1_pubkey *label,
+    unsigned char *label_tweak32,
+    const unsigned char *receiver_scan_seckey,
+    unsigned int m
+) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
index b23701e9ac..48b27b9d45 100644
--- a/src/modules/silentpayments/main_impl.h
+++ b/src/modules/silentpayments/main_impl.h
@@ -205,4 +205,45 @@ int secp256k1_silentpayments_create_shared_secret(const secp256k1_context *ctx,
     return 1;
 }
 
+/** Set hash state to the BIP340 tagged hash midstate for "BIP0352/Label". */
+static void secp256k1_silentpayments_sha256_init_label(secp256k1_sha256* hash) {
+    secp256k1_sha256_initialize(hash);
+    hash->s[0] = 0x26b95d63ul;
+    hash->s[1] = 0x8bf1b740ul;
+    hash->s[2] = 0x10a5986ful;
+    hash->s[3] = 0x06a387a5ul;
+    hash->s[4] = 0x2d1c1c30ul;
+    hash->s[5] = 0xd035951aul;
+    hash->s[6] = 0x2d7f0f96ul;
+    hash->s[7] = 0x29e3e0dbul;
+
+    hash->bytes = 64;
+}
+
+int secp256k1_silentpayments_create_label_tweak(const secp256k1_context *ctx, secp256k1_pubkey *label, unsigned char *label_tweak32, const unsigned char *receiver_scan_seckey, unsigned int m) {
+    secp256k1_sha256 hash;
+    unsigned char m_serialized[4];
+
+    /* Sanity check inputs. */
+    VERIFY_CHECK(ctx != NULL);
+    (void)ctx;
+    VERIFY_CHECK(label != NULL);
+    VERIFY_CHECK(label_tweak32 != NULL);
+    VERIFY_CHECK(receiver_scan_seckey != NULL);
+
+    /* Compute label_tweak = hash(ser_256(b_scan) || ser_32(m))  [sha256 with tag "BIP0352/Label"] */
+    secp256k1_silentpayments_sha256_init_label(&hash);
+    secp256k1_sha256_write(&hash, receiver_scan_seckey, 32);
+    secp256k1_write_be32(m_serialized, m);
+    secp256k1_sha256_write(&hash, m_serialized, sizeof(m_serialized));
+    secp256k1_sha256_finalize(&hash, label_tweak32);
+
+    /* Compute label = label_tweak * G */
+    if (!secp256k1_ec_pubkey_create(ctx, label, label_tweak32)) {
+        return 0;
+    }
+
+    return 1;
+}
+
 #endif

From dbcccbb7cc66917f2cb60f1b22d1b3a24d003c10 Mon Sep 17 00:00:00 2001
From: Sebastian Falbesoner <sebastian.falbesoner@gmail.com>
Date: Tue, 23 Jan 2024 18:43:06 +0100
Subject: [PATCH 08/13] silentpayments: add routine for creating labelled spend
 pubkeys (for addresses)

---
 include/secp256k1_silentpayments.h     | 23 +++++++++++++++++++++
 src/modules/silentpayments/main_impl.h | 28 ++++++++++++++++++++++++++
 2 files changed, 51 insertions(+)

diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
index 6ade98b86f..1fd7247466 100644
--- a/include/secp256k1_silentpayments.h
+++ b/include/secp256k1_silentpayments.h
@@ -193,6 +193,29 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_create_l
     unsigned int m
 ) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
 
+/** Create Silent Payment labelled spend public key.
+ *
+ *  Given a recipient's spend public key B_spend and a label, calculate
+ *  the corresponding serialized labelled spend public key:
+ *
+ *  B_m = B_spend + label
+ *
+ *  The result is used by the receiver to create a Silent Payment address, consisting
+ *  of the serialized and concatenated scan public key and (labelled) spend public key each.
+ *
+ *  Returns: 1 if labelled spend public key creation was successful. 0 if an error occured.
+ *  Args:                  ctx: pointer to a context object
+ *  Out: l_addr_spend_pubkey33: pointer to the resulting labelled spend public key
+ *   In: receiver_spend_pubkey: pointer to the receiver's spend pubkey
+ *                       label: pointer to the the receiver's label
+ */
+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_create_address_spend_pubkey(
+    const secp256k1_context *ctx,
+    unsigned char *l_addr_spend_pubkey33,
+    const secp256k1_pubkey *receiver_spend_pubkey,
+    const secp256k1_pubkey *label
+) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
index 48b27b9d45..11e5e0b36b 100644
--- a/src/modules/silentpayments/main_impl.h
+++ b/src/modules/silentpayments/main_impl.h
@@ -246,4 +246,32 @@ int secp256k1_silentpayments_create_label_tweak(const secp256k1_context *ctx, se
     return 1;
 }
 
+int secp256k1_silentpayments_create_address_spend_pubkey(const secp256k1_context *ctx, unsigned char *l_addr_spend_pubkey33, const secp256k1_pubkey *receiver_spend_pubkey, const secp256k1_pubkey *label) {
+    secp256k1_ge B_m, label_addend;
+    secp256k1_gej result_gej;
+    secp256k1_ge result_ge;
+    size_t ser_size;
+    int ser_ret;
+
+    /* Sanity check inputs. */
+    VERIFY_CHECK(ctx != NULL);
+    VERIFY_CHECK(l_addr_spend_pubkey33 != NULL);
+    VERIFY_CHECK(receiver_spend_pubkey != NULL);
+    VERIFY_CHECK(label != NULL);
+
+    /* Calculate B_m = B_spend + label */
+    secp256k1_pubkey_load(ctx, &B_m, receiver_spend_pubkey);
+    secp256k1_pubkey_load(ctx, &label_addend, label);
+    secp256k1_gej_set_ge(&result_gej, &B_m);
+    secp256k1_gej_add_ge_var(&result_gej, &result_gej, &label_addend, NULL);
+
+    /* Serialize B_m */
+    secp256k1_ge_set_gej(&result_ge, &result_gej);
+    ser_ret = secp256k1_eckey_pubkey_serialize(&result_ge, l_addr_spend_pubkey33, &ser_size, 1);
+    VERIFY_CHECK(ser_ret && ser_size == 33);
+    (void)ser_ret;
+
+    return 1;
+}
+
 #endif

From 8460be58ccad9ca98e27bc60e838633f8abe2ed3 Mon Sep 17 00:00:00 2001
From: Sebastian Falbesoner <sebastian.falbesoner@gmail.com>
Date: Thu, 28 Sep 2023 00:30:20 +0200
Subject: [PATCH 09/13] silentpayments: implement output pubkey creation (for
 sender)

---
 include/secp256k1_silentpayments.h     | 24 ++++++++++++
 src/modules/silentpayments/main_impl.h | 51 ++++++++++++++++++++++++++
 2 files changed, 75 insertions(+)

diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
index 1fd7247466..c28fc680a9 100644
--- a/include/secp256k1_silentpayments.h
+++ b/include/secp256k1_silentpayments.h
@@ -216,6 +216,30 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_create_a
     const secp256k1_pubkey *label
 ) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
 
+/** Create Silent Payment output public key (for sender).
+ *
+ *  Given a shared_secret, a recipient's spend public key B_spend, and
+ *  an output counter k, calculate the corresponding output public key:
+ *
+ *  P_output_xonly = B_spend + hash(shared_secret || ser_32(k)) * G
+ *
+ *  Returns: 1 if output creation was successful. 0 if an error occured.
+ *  Args:               ctx: pointer to a context object
+ *  Out:     P_output_xonly: pointer to the resulting output x-only pubkey
+ *  In:     shared_secret33: shared secret, derived from either sender's
+ *                           or receiver's perspective with routines from above
+ *    receiver_spend_pubkey: pointer to the receiver's spend pubkey
+ *                        k: output counter (usually set to 0, should be increased for
+ *                           every additional output to the same recipient)
+ */
+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_sender_create_output_pubkey(
+    const secp256k1_context *ctx,
+    secp256k1_xonly_pubkey *P_output_xonly,
+    const unsigned char *shared_secret33,
+    const secp256k1_pubkey *receiver_spend_pubkey,
+    unsigned int k
+) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
index 11e5e0b36b..f30a26ec97 100644
--- a/src/modules/silentpayments/main_impl.h
+++ b/src/modules/silentpayments/main_impl.h
@@ -10,6 +10,7 @@
 #include "../../../include/secp256k1_ecdh.h"
 #include "../../../include/secp256k1_extrakeys.h"
 #include "../../../include/secp256k1_silentpayments.h"
+#include "../../hash.h"
 
 /** Set hash state to the BIP340 tagged hash midstate for "BIP0352/Inputs". */
 static void secp256k1_silentpayments_sha256_init_inputs(secp256k1_sha256* hash) {
@@ -274,4 +275,54 @@ int secp256k1_silentpayments_create_address_spend_pubkey(const secp256k1_context
     return 1;
 }
 
+/** Set hash state to the BIP340 tagged hash midstate for "BIP0352/SharedSecret". */
+static void secp256k1_silentpayments_sha256_init_sharedsecret(secp256k1_sha256* hash) {
+    secp256k1_sha256_initialize(hash);
+    hash->s[0] = 0x88831537ul;
+    hash->s[1] = 0x5127079bul;
+    hash->s[2] = 0x69c2137bul;
+    hash->s[3] = 0xab0303e6ul;
+    hash->s[4] = 0x98fa21faul;
+    hash->s[5] = 0x4a888523ul;
+    hash->s[6] = 0xbd99daabul;
+    hash->s[7] = 0xf25e5e0aul;
+
+    hash->bytes = 64;
+}
+
+static void secp256k1_silentpayments_create_t_k(secp256k1_scalar *t_k_scalar, const unsigned char *shared_secret33, unsigned int k) {
+    secp256k1_sha256 hash;
+    unsigned char hash_ser[32];
+    unsigned char k_serialized[4];
+
+    /* Compute t_k = hash(shared_secret || ser_32(k))  [sha256 with tag "BIP0352/SharedSecret"] */
+    secp256k1_silentpayments_sha256_init_sharedsecret(&hash);
+    secp256k1_sha256_write(&hash, shared_secret33, 33);
+    secp256k1_write_be32(k_serialized, k);
+    secp256k1_sha256_write(&hash, k_serialized, sizeof(k_serialized));
+    secp256k1_sha256_finalize(&hash, hash_ser);
+    secp256k1_scalar_set_b32(t_k_scalar, hash_ser, NULL);
+}
+
+int secp256k1_silentpayments_sender_create_output_pubkey(const secp256k1_context *ctx, secp256k1_xonly_pubkey *P_output_xonly, const unsigned char *shared_secret33, const secp256k1_pubkey *receiver_spend_pubkey, unsigned int k) {
+    secp256k1_ge P_output_ge;
+    secp256k1_scalar t_k_scalar;
+
+    /* Sanity check inputs */
+    VERIFY_CHECK(ctx != NULL);
+    ARG_CHECK(P_output_xonly != NULL);
+    ARG_CHECK(shared_secret33 != NULL);
+    ARG_CHECK(receiver_spend_pubkey != NULL);
+
+    /* Calculate and return P_output_xonly = B_spend + t_k * G */
+    secp256k1_silentpayments_create_t_k(&t_k_scalar, shared_secret33, k);
+    secp256k1_pubkey_load(ctx, &P_output_ge, receiver_spend_pubkey);
+    if (!secp256k1_eckey_pubkey_tweak_add(&P_output_ge, &t_k_scalar)) {
+        return 0;
+    }
+    secp256k1_xonly_pubkey_save(P_output_xonly, &P_output_ge);
+
+    return 1;
+}
+
 #endif

From d6c9856bdec79667d1e61b8689668cca2da3a75b Mon Sep 17 00:00:00 2001
From: Sebastian Falbesoner <sebastian.falbesoner@gmail.com>
Date: Mon, 12 Feb 2024 17:43:11 +0100
Subject: [PATCH 10/13] silentpayments: add routine for tx output scanning (for
 receiver)

Co-authored-by: josibake <josibake@protonmail.com>
---
 include/secp256k1_silentpayments.h     | 49 +++++++++++++++++++++
 src/modules/silentpayments/main_impl.h | 59 ++++++++++++++++++++++++++
 2 files changed, 108 insertions(+)

diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
index c28fc680a9..39259d3140 100644
--- a/include/secp256k1_silentpayments.h
+++ b/include/secp256k1_silentpayments.h
@@ -240,6 +240,55 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_sender_c
     unsigned int k
 ) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
 
+typedef struct {
+    secp256k1_pubkey label;
+    secp256k1_pubkey label_negated;
+} secp256k1_silentpayments_label_data;
+
+/** Scan for Silent Payment transaction output (for receiver).
+ *
+ *  Given a shared_secret, a recipient's spend public key B_spend,
+ *  an output counter k, and a scanned tx's output x-only public key tx_output,
+ *  calculate the corresponding scanning data:
+ *
+ *  t_k = hash(shared_secret || ser_32(k))
+ *  P_output = B_spend + t_k * G  [not returned]
+ *  if P_output == tx_output
+ *      direct_match = 1
+ *  else
+ *      label1 =  tx_output - P_output
+ *      label2 = -tx_output - P_output
+ *      direct_match = 0
+ *
+ *  The resulting data is needed for the receiver to efficiently scan for labels
+ *  in silent payments eligible outputs.
+ *
+ *  Returns: 1 if output scanning was successful. 0 if an error occured.
+ *  Args:             ctx: pointer to a context object
+ *  Out:     direct_match: pointer to the resulting boolean indicating whether
+ *                         the calculated output pubkey matches the scanned one
+ *                    t_k: pointer to the resulting tweak t_k
+ *             label_data: pointer to the resulting label structure, containing the
+ *                         two label candidates, only set if direct_match == 0
+ *                         (can be NULL if the data is not needed)
+ *  In:   shared_secret33: shared secret, derived from either sender's
+ *                         or receiver's perspective with routines from above
+ *  receiver_spend_pubkey: pointer to the receiver's spend pubkey
+ *                      k: output counter (usually set to 0, should be increased for
+ *                         every additional output to the same recipient)
+ *              tx_output: pointer to the scanned tx's output x-only public key
+ */
+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_receiver_scan_output(
+    const secp256k1_context *ctx,
+    int *direct_match,
+    unsigned char *t_k,
+    secp256k1_silentpayments_label_data *label_data,
+    const unsigned char *shared_secret33,
+    const secp256k1_pubkey *receiver_spend_pubkey,
+    unsigned int k,
+    const secp256k1_xonly_pubkey *tx_output
+) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(8);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
index f30a26ec97..9b3c613984 100644
--- a/src/modules/silentpayments/main_impl.h
+++ b/src/modules/silentpayments/main_impl.h
@@ -325,4 +325,63 @@ int secp256k1_silentpayments_sender_create_output_pubkey(const secp256k1_context
     return 1;
 }
 
+int secp256k1_silentpayments_receiver_scan_output(const secp256k1_context *ctx, int *direct_match, unsigned char *t_k, secp256k1_silentpayments_label_data *label_data, const unsigned char *shared_secret33, const secp256k1_pubkey *receiver_spend_pubkey, unsigned int k, const secp256k1_xonly_pubkey *tx_output) {
+    secp256k1_scalar t_k_scalar;
+    secp256k1_ge P_output_ge;
+    secp256k1_xonly_pubkey P_output_xonly;
+
+    /* Sanity check inputs */
+    VERIFY_CHECK(ctx != NULL);
+    ARG_CHECK(direct_match != NULL);
+    ARG_CHECK(t_k != NULL);
+    ARG_CHECK(shared_secret33 != NULL);
+    ARG_CHECK(receiver_spend_pubkey != NULL);
+    ARG_CHECK(tx_output != NULL);
+
+    /* Calculate t_k = hash(shared_secret || ser_32(k)) */
+    secp256k1_silentpayments_create_t_k(&t_k_scalar, shared_secret33, k);
+    secp256k1_scalar_get_b32(t_k, &t_k_scalar);
+
+    /* Calculate P_output = B_spend + t_k * G */
+    secp256k1_pubkey_load(ctx, &P_output_ge, receiver_spend_pubkey);
+    if (!secp256k1_eckey_pubkey_tweak_add(&P_output_ge, &t_k_scalar)) {
+        return 0;
+    }
+
+    /* If the calculated output matches the one from the tx, we have a direct match and can
+     * return without labels calculation (one of the two would result in point of infinity) */
+    secp256k1_xonly_pubkey_save(&P_output_xonly, &P_output_ge);
+    if (secp256k1_xonly_pubkey_cmp(ctx, &P_output_xonly, tx_output) == 0) {
+        *direct_match = 1;
+        return 1;
+    }
+    *direct_match = 0;
+
+    /* If desired, also calculate label candidates */
+    if (label_data != NULL) {
+        secp256k1_ge P_output_negated_ge, tx_output_ge;
+        secp256k1_ge label_ge;
+        secp256k1_gej label_gej;
+
+        /* Calculate negated P_output (common addend) first */
+        secp256k1_ge_neg(&P_output_negated_ge, &P_output_ge);
+
+        /* Calculate first scan label candidate: label1 = tx_output - P_output */
+        secp256k1_xonly_pubkey_load(ctx, &tx_output_ge, tx_output);
+        secp256k1_gej_set_ge(&label_gej, &tx_output_ge);
+        secp256k1_gej_add_ge_var(&label_gej, &label_gej, &P_output_negated_ge, NULL);
+        secp256k1_ge_set_gej(&label_ge, &label_gej);
+        secp256k1_pubkey_save(&label_data->label, &label_ge);
+
+        /* Calculate second scan label candidate: label2 = -tx_output - P_output */
+        secp256k1_gej_set_ge(&label_gej, &tx_output_ge);
+        secp256k1_gej_neg(&label_gej, &label_gej);
+        secp256k1_gej_add_ge_var(&label_gej, &label_gej, &P_output_negated_ge, NULL);
+        secp256k1_ge_set_gej(&label_ge, &label_gej);
+        secp256k1_pubkey_save(&label_data->label_negated, &label_ge);
+    }
+
+    return 1;
+}
+
 #endif

From 26bdb5f195ea5e9ebcd96919709c0a8f50156f80 Mon Sep 17 00:00:00 2001
From: Sebastian Falbesoner <sebastian.falbesoner@gmail.com>
Date: Wed, 20 Dec 2023 23:01:58 +0100
Subject: [PATCH 11/13] silentpayments: implement output spending privkey
 creation (for receiver)

---
 include/secp256k1_silentpayments.h     | 24 ++++++++++++++++++++++++
 src/modules/silentpayments/main_impl.h | 23 +++++++++++++++++++++++
 2 files changed, 47 insertions(+)

diff --git a/include/secp256k1_silentpayments.h b/include/secp256k1_silentpayments.h
index 39259d3140..81baf8e655 100644
--- a/include/secp256k1_silentpayments.h
+++ b/include/secp256k1_silentpayments.h
@@ -289,6 +289,30 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_receiver
     const secp256k1_xonly_pubkey *tx_output
 ) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(8);
 
+/** Create Silent Payment output private key (for spending receiver's funds).
+ *
+ *  Given a recipient's spend private key b_spend, a tweak t_k and an
+ *  optional label_tweak, calculate the corresponding output private key d:
+ *
+ *  d = b_spend + t_k + label_tweak
+ *  (if no label tweak is used, then label_tweak = 0)
+ *
+ *  Returns: 1 if private key creation was successful. 0 if an error occured.
+ *  Args:                  ctx: pointer to a context object
+ *  Out:         output_seckey: pointer to the resulting spending private key
+ *  In:  receiver_spend_seckey: pointer to the receiver's spend private key
+ *                         t_k: pointer to the 32-byte output tweak
+ *                 label_tweak: pointer to an optional 32-byte label tweak
+ *                              (can be NULL if no label is used)
+ */
+SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_silentpayments_create_output_seckey(
+    const secp256k1_context *ctx,
+    unsigned char *output_seckey,
+    const unsigned char *receiver_spend_seckey,
+    const unsigned char *t_k,
+    const unsigned char *label_tweak
+) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/modules/silentpayments/main_impl.h b/src/modules/silentpayments/main_impl.h
index 9b3c613984..2b8872ae5a 100644
--- a/src/modules/silentpayments/main_impl.h
+++ b/src/modules/silentpayments/main_impl.h
@@ -384,4 +384,27 @@ int secp256k1_silentpayments_receiver_scan_output(const secp256k1_context *ctx,
     return 1;
 }
 
+int secp256k1_silentpayments_create_output_seckey(const secp256k1_context *ctx, unsigned char *output_seckey, const unsigned char *receiver_spend_seckey, const unsigned char *t_k, const unsigned char *label_tweak) {
+    /* Sanity check inputs */
+    VERIFY_CHECK(ctx != NULL);
+    ARG_CHECK(output_seckey != NULL);
+    memset(output_seckey, 0, 32);
+    ARG_CHECK(receiver_spend_seckey != NULL);
+    ARG_CHECK(t_k != NULL);
+
+    /* Compute d = (b_spend + t_k) mod n */
+    memcpy(output_seckey, receiver_spend_seckey, 32);
+    if (!secp256k1_ec_seckey_tweak_add(ctx, output_seckey, t_k)) {
+        return 0;
+    }
+
+    if (label_tweak != NULL) {
+        if (!secp256k1_ec_seckey_tweak_add(ctx, output_seckey, label_tweak)) {
+            return 0;
+        }
+    }
+
+    return 1;
+}
+
 #endif

From 59488c4dd8a7dbb65f5727ac8d45288d22ba7001 Mon Sep 17 00:00:00 2001
From: Sebastian Falbesoner <sebastian.falbesoner@gmail.com>
Date: Thu, 22 Feb 2024 23:49:23 +0100
Subject: [PATCH 12/13] tests: add BIP-352 test vectors

The vectors are generated with a Python script that converts the .json
file from the BIP to C code:

$ ./tools/tests_silentpayments_generate.py test_vectors.json > ./src/modules/silentpayments/vectors.h
---
 .../silentpayments/Makefile.am.include        |    2 +
 src/modules/silentpayments/tests_impl.h       |  235 ++
 src/modules/silentpayments/vectors.h          | 1979 +++++++++++++++++
 src/tests.c                                   |    8 +
 tools/bech32m.py                              |  135 ++
 tools/ripemd160.py                            |  130 ++
 tools/tests_silentpayments_generate.py        |  289 +++
 7 files changed, 2778 insertions(+)
 create mode 100644 src/modules/silentpayments/tests_impl.h
 create mode 100644 src/modules/silentpayments/vectors.h
 create mode 100644 tools/bech32m.py
 create mode 100644 tools/ripemd160.py
 create mode 100755 tools/tests_silentpayments_generate.py

diff --git a/src/modules/silentpayments/Makefile.am.include b/src/modules/silentpayments/Makefile.am.include
index 842a33e2d9..af917f8908 100644
--- a/src/modules/silentpayments/Makefile.am.include
+++ b/src/modules/silentpayments/Makefile.am.include
@@ -1,2 +1,4 @@
 include_HEADERS += include/secp256k1_silentpayments.h
 noinst_HEADERS += src/modules/silentpayments/main_impl.h
+noinst_HEADERS += src/modules/silentpayments/tests_impl.h
+noinst_HEADERS += src/modules/silentpayments/vectors.h
diff --git a/src/modules/silentpayments/tests_impl.h b/src/modules/silentpayments/tests_impl.h
new file mode 100644
index 0000000000..0fd434b75b
--- /dev/null
+++ b/src/modules/silentpayments/tests_impl.h
@@ -0,0 +1,235 @@
+/***********************************************************************
+ * Distributed under the MIT software license, see the accompanying    *
+ * file COPYING or https://www.opensource.org/licenses/mit-license.php.*
+ ***********************************************************************/
+
+#ifndef SECP256K1_MODULE_SILENTPAYMENTS_TESTS_H
+#define SECP256K1_MODULE_SILENTPAYMENTS_TESTS_H
+
+#include "../../../include/secp256k1_silentpayments.h"
+#include "../../../src/modules/silentpayments/vectors.h"
+
+void run_silentpayments_test_vector_send(const struct bip352_test_vector *test, const unsigned char *a_sum, const unsigned char *input_hash) {
+    secp256k1_pubkey last_scan_pubkey; /* needed for grouping outputs */
+    unsigned char s_one[32] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1};
+    size_t i;
+    unsigned int k = 0;
+
+    /* Check that sender creates expected outputs */
+    CHECK(secp256k1_ec_pubkey_create(CTX, &last_scan_pubkey, s_one)); /* dirty hack: set last_scan_pubkey to generator point initially */
+    for (i = 0; i < test->num_recipient_outputs; i++) {
+        secp256k1_pubkey receiver_scan_pubkey;
+        secp256k1_pubkey receiver_spend_pubkey;
+        unsigned char shared_secret[33];
+        secp256k1_xonly_pubkey created_output_xonly;
+        unsigned char created_output[32];
+
+        CHECK(secp256k1_ec_pubkey_parse(CTX, &receiver_scan_pubkey, test->receiver_pubkeys[i].scan_pubkey, 33));
+        CHECK(secp256k1_ec_pubkey_parse(CTX, &receiver_spend_pubkey, test->receiver_pubkeys[i].spend_pubkey, 33));
+        if (secp256k1_ec_pubkey_cmp(CTX, &receiver_scan_pubkey, &last_scan_pubkey) != 0) {
+            k = 0;
+        } else {
+            k++;
+        }
+        CHECK(secp256k1_silentpayments_create_shared_secret(CTX, shared_secret, &receiver_scan_pubkey, a_sum, input_hash));
+        CHECK(secp256k1_silentpayments_sender_create_output_pubkey(CTX, &created_output_xonly, shared_secret,
+            &receiver_spend_pubkey, k));
+        CHECK(secp256k1_xonly_pubkey_serialize(CTX, created_output, &created_output_xonly));
+        CHECK(secp256k1_memcmp_var(created_output, test->recipient_outputs[i], 32) == 0);
+
+        last_scan_pubkey = receiver_scan_pubkey;
+    }
+}
+
+void run_silentpayments_test_vector_receive(const struct bip352_test_vector *test, const secp256k1_pubkey *A_sum, const unsigned char *input_hash) {
+    struct label_cache_entry {
+        secp256k1_pubkey label;
+        unsigned char label_tweak[32];
+    };
+    struct labels_cache {
+        size_t entries_used;
+        struct label_cache_entry entries[10];
+    };
+
+    struct labels_cache labels_cache;
+    secp256k1_pubkey receiver_scan_pubkey;
+    secp256k1_pubkey receiver_spend_pubkey;
+    unsigned char shared_secret[33];
+    size_t i;
+    unsigned int k = 0;
+    int outputs_to_check[10] = {0};
+    size_t num_found_outputs = 0;
+    unsigned char found_output_pubkeys[10][32];
+    unsigned char found_seckey_tweaks[10][32];
+    unsigned char found_signatures[10][64];
+
+    /* scan / spend pubkeys are not in the given data of the receiver part, so let's compute them */
+    CHECK(secp256k1_ec_pubkey_create(CTX, &receiver_scan_pubkey, test->scan_seckey));
+    CHECK(secp256k1_ec_pubkey_create(CTX, &receiver_spend_pubkey, test->spend_seckey));
+
+    /* create shared secret */
+    {
+        unsigned char shared_secret_fullnode[33];
+        unsigned char shared_secret_lightclient[33];
+        secp256k1_pubkey A_tweaked;
+
+        CHECK(secp256k1_silentpayments_create_shared_secret(CTX, shared_secret_fullnode, A_sum, test->scan_seckey, input_hash));
+        /* check that creating shared secret in light client / index mode (with intermediate A_tweaked) leads to the same result */
+        CHECK(secp256k1_silentpayments_create_tweaked_pubkey(CTX, &A_tweaked, A_sum, input_hash));
+        CHECK(secp256k1_silentpayments_create_shared_secret(CTX, shared_secret_lightclient, &A_tweaked, test->scan_seckey, NULL));
+        CHECK(secp256k1_memcmp_var(shared_secret_fullnode, shared_secret_lightclient, 33) == 0);
+
+        memcpy(shared_secret, shared_secret_fullnode, 33);
+    }
+
+    /* create labels cache */
+    labels_cache.entries_used = 0;
+    for (i = 0; i < test->num_labels; i++) {
+        unsigned int m = test->label_integers[i];
+        struct label_cache_entry *cache_entry = &labels_cache.entries[labels_cache.entries_used];
+        CHECK(secp256k1_silentpayments_create_label_tweak(CTX, &cache_entry->label, cache_entry->label_tweak, test->scan_seckey, m));
+        labels_cache.entries_used++;
+    }
+
+    /* scan through outputs, check that expected found outputs match */
+    for (i = 0; i < test->num_to_scan_outputs; i++) {
+        outputs_to_check[i] = 1;
+    }
+
+    while(1) {
+        int found_something = 0;
+
+        for (i = 0; i < test->num_to_scan_outputs; i++) {
+            int direct_match, output_detected = 0;
+            unsigned char t_k[32];
+            secp256k1_silentpayments_label_data label_candidates;
+            secp256k1_xonly_pubkey to_scan_output;
+
+            if (outputs_to_check[i] == 0)
+                continue;
+
+            CHECK(secp256k1_xonly_pubkey_parse(CTX, &to_scan_output, test->to_scan_outputs[i]));
+            CHECK(secp256k1_silentpayments_receiver_scan_output(CTX, &direct_match, t_k, &label_candidates,
+                shared_secret, &receiver_spend_pubkey, k, &to_scan_output));
+            if (direct_match) {
+                output_detected = 1;
+            } else {
+                /* no direct match, check labels cache */
+                secp256k1_pubkey *label_found = NULL;
+                unsigned char *label_tweak = NULL;
+
+                size_t l;
+                for (l = 0; l < labels_cache.entries_used; l++) {
+                    if (secp256k1_ec_pubkey_cmp(CTX, &label_candidates.label, &labels_cache.entries[l].label) == 0 ||
+                        secp256k1_ec_pubkey_cmp(CTX, &label_candidates.label_negated, &labels_cache.entries[l].label) == 0) {
+                        label_found = &labels_cache.entries[l].label;
+                        label_tweak = labels_cache.entries[l].label_tweak;
+                        break;
+                    }
+                }
+                if (label_found != NULL) {
+                    CHECK(secp256k1_ec_seckey_tweak_add(CTX, t_k, label_tweak));
+                    output_detected = 1;
+                }
+            }
+            if (output_detected) {
+                unsigned char full_seckey[32];
+                secp256k1_keypair keypair;
+                unsigned char signature[64];
+                const unsigned char msg32[32] = /* sha256("message") */
+                    {0xab,0x53,0x0a,0x13,0xe4,0x59,0x14,0x98,0x2b,0x79,0xf9,0xb7,0xe3,0xfb,0xa9,0x94,
+                     0xcf,0xd1,0xf3,0xfb,0x22,0xf7,0x1c,0xea,0x1a,0xfb,0xf0,0x2b,0x46,0x0c,0x6d,0x1d};
+                const unsigned char aux32[32] = /* sha256("random auxiliary data") */
+                    {0x0b,0x3f,0xdd,0xfd,0x67,0xbf,0x76,0xae,0x76,0x39,0xee,0x73,0x5b,0x70,0xff,0x15,
+                     0x83,0xfd,0x92,0x48,0xc0,0x57,0xd2,0x86,0x07,0xa2,0x15,0xf4,0x0b,0x0a,0x3e,0xcc};
+                outputs_to_check[i] = 0;
+                memcpy(found_output_pubkeys[num_found_outputs], test->to_scan_outputs[i], 32);
+                memcpy(found_seckey_tweaks[num_found_outputs], t_k, 32);
+                CHECK(secp256k1_silentpayments_create_output_seckey(CTX, full_seckey, test->spend_seckey, t_k, NULL));
+                CHECK(secp256k1_keypair_create(CTX, &keypair, full_seckey));
+                CHECK(secp256k1_schnorrsig_sign32(CTX, signature, msg32, &keypair, aux32));
+                memcpy(found_signatures[num_found_outputs], signature, 64);
+                num_found_outputs++;
+                k++;
+                found_something = 1;
+            }
+        }
+        if (!found_something)
+            break;
+    }
+
+    /* compare expected and scanned outputs (including calculated seckey tweaks and signatures) */
+    CHECK(num_found_outputs == test->num_found_outputs);
+    for (i = 0; i < num_found_outputs; i++) {
+        CHECK(secp256k1_memcmp_var(found_output_pubkeys[i], test->found_output_pubkeys[i], 32) == 0);
+        CHECK(secp256k1_memcmp_var(found_seckey_tweaks[i], test->found_seckey_tweaks[i], 32) == 0);
+        CHECK(secp256k1_memcmp_var(found_signatures[i], test->found_signatures[i], 64) == 0);
+    }
+}
+
+void run_silentpayments_test_vectors(void) {
+    size_t i, j;
+
+    for (i = 0; i < sizeof(bip352_test_vectors) / sizeof(bip352_test_vectors[0]); i++) {
+        const struct bip352_test_vector *test = &bip352_test_vectors[i];
+        unsigned char a_sum[32];
+        secp256k1_pubkey A_sum;
+        unsigned char input_hash_sender[32];
+        unsigned char input_hash_receiver[32];
+
+        /* Calculate private tweak data (sender perspective) */
+        {
+            unsigned char const *plain_seckeys[MAX_INPUTS_PER_TEST_CASE];
+            unsigned char const *taproot_seckeys[MAX_INPUTS_PER_TEST_CASE];
+            for (j = 0; j < test->num_plain_inputs; j++) {
+                plain_seckeys[j] = test->plain_seckeys[j];
+            }
+            for (j = 0; j < test->num_taproot_inputs; j++) {
+                taproot_seckeys[j] = test->taproot_seckeys[j];
+            }
+            CHECK(secp256k1_silentpayments_create_private_tweak_data(CTX, a_sum, input_hash_sender,
+                test->num_plain_inputs > 0 ? plain_seckeys : NULL, test->num_plain_inputs,
+                test->num_taproot_inputs > 0 ? taproot_seckeys : NULL, test->num_taproot_inputs,
+                test->outpoint_smallest));
+        }
+
+        /* Calculate public tweak data (receiver perspective) */
+        {
+            secp256k1_pubkey plain_pubkeys_objs[MAX_INPUTS_PER_TEST_CASE];
+            secp256k1_xonly_pubkey xonly_pubkeys_objs[MAX_INPUTS_PER_TEST_CASE];
+            secp256k1_pubkey const *plain_pubkeys[MAX_INPUTS_PER_TEST_CASE];
+            secp256k1_xonly_pubkey const *xonly_pubkeys[MAX_INPUTS_PER_TEST_CASE];
+            for (j = 0; j < test->num_plain_inputs; j++) {
+                CHECK(secp256k1_ec_pubkey_parse(CTX, &plain_pubkeys_objs[j], test->plain_pubkeys[j], 33));
+                plain_pubkeys[j] = &plain_pubkeys_objs[j];
+            }
+            for (j = 0; j < test->num_taproot_inputs; j++) {
+                CHECK(secp256k1_xonly_pubkey_parse(CTX, &xonly_pubkeys_objs[j], test->xonly_pubkeys[j]));
+                xonly_pubkeys[j] = &xonly_pubkeys_objs[j];
+            }
+            CHECK(secp256k1_silentpayments_create_public_tweak_data(CTX, &A_sum, input_hash_receiver,
+                test->num_plain_inputs > 0 ? plain_pubkeys : NULL, test->num_plain_inputs,
+                test->num_taproot_inputs > 0 ? xonly_pubkeys : NULL, test->num_taproot_inputs,
+                test->outpoint_smallest));
+        }
+
+        /* First sanity check: verify that a_sum * G == A_sum, and that input hashes match */
+        {
+            secp256k1_pubkey A_sum_calculated;
+            CHECK(secp256k1_ec_pubkey_create(CTX, &A_sum_calculated, a_sum));
+            CHECK(secp256k1_ec_pubkey_cmp(CTX, &A_sum, &A_sum_calculated) == 0);
+            CHECK(secp256k1_memcmp_var(input_hash_sender, input_hash_receiver, 32) == 0);
+        }
+
+        run_silentpayments_test_vector_send(test, a_sum, input_hash_sender);
+        run_silentpayments_test_vector_receive(test, &A_sum, input_hash_receiver);
+    }
+}
+
+void run_silentpayments_tests(void) {
+    run_silentpayments_test_vectors();
+
+    /* TODO: add a few manual tests here, that target the ECC-related parts of silent payments */
+}
+
+#endif
diff --git a/src/modules/silentpayments/vectors.h b/src/modules/silentpayments/vectors.h
new file mode 100644
index 0000000000..49ab1e6c1f
--- /dev/null
+++ b/src/modules/silentpayments/vectors.h
@@ -0,0 +1,1979 @@
+/* Note: this file was autogenerated using tests_silentpayments_generate.py. Do not edit. */
+#define SECP256K1_SILENTPAYMENTS_NUMBER_TESTVECTORS (23)
+
+#define MAX_INPUTS_PER_TEST_CASE  3
+#define MAX_OUTPUTS_PER_TEST_CASE 4
+
+struct bip352_receiver_addressdata {
+    unsigned char scan_pubkey[33];
+    unsigned char spend_pubkey[33];
+};
+
+struct bip352_test_vector {
+    /* Inputs (private keys / public keys + smallest outpoint) */
+    size_t num_plain_inputs;
+    unsigned char plain_seckeys[MAX_INPUTS_PER_TEST_CASE][32];
+    unsigned char plain_pubkeys[MAX_INPUTS_PER_TEST_CASE][33];
+
+    size_t num_taproot_inputs;
+    unsigned char taproot_seckeys[MAX_INPUTS_PER_TEST_CASE][32];
+    unsigned char xonly_pubkeys[MAX_INPUTS_PER_TEST_CASE][32];
+
+    unsigned char outpoint_smallest[36];
+
+    /* Given sender data (pubkeys encoded per output address to send to) */
+    size_t num_recipient_outputs;
+    struct bip352_receiver_addressdata receiver_pubkeys[MAX_OUTPUTS_PER_TEST_CASE];
+
+    /* Expected sender data */
+    unsigned char recipient_outputs[MAX_OUTPUTS_PER_TEST_CASE][32];
+
+    /* Given receiver data */
+    unsigned char scan_seckey[32];
+    unsigned char spend_seckey[32];
+    size_t num_to_scan_outputs;
+    unsigned char to_scan_outputs[MAX_OUTPUTS_PER_TEST_CASE][32];
+    size_t num_labels;
+    unsigned int label_integers[MAX_OUTPUTS_PER_TEST_CASE];
+
+    /* Expected receiver data */
+    size_t num_found_outputs;
+    unsigned char found_output_pubkeys[MAX_OUTPUTS_PER_TEST_CASE][32];
+    unsigned char found_seckey_tweaks[MAX_OUTPUTS_PER_TEST_CASE][32];
+    unsigned char found_signatures[MAX_OUTPUTS_PER_TEST_CASE][64];
+};
+
+static const struct bip352_test_vector bip352_test_vectors[SECP256K1_SILENTPAYMENTS_NUMBER_TESTVECTORS] = {
+    /* ----- Simple send: two inputs (1) ----- */
+    {
+        2,
+        { /* input plain seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            {0x93,0xf5,0xed,0x90,0x7a,0xd5,0xb2,0xbd,0xbb,0xdc,0xb5,0xd9,0x11,0x6e,0xbc,0x0a,0x4e,0x1f,0x92,0xf9,0x10,0xd5,0x26,0x02,0x37,0xfa,0x45,0xa9,0x40,0x8a,0xad,0x16},
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            {0x03,0xbd,0x85,0x68,0x5d,0x03,0xd1,0x11,0x69,0x9b,0x15,0xd0,0x46,0x31,0x9f,0xeb,0xe7,0x7f,0x8d,0xe5,0x28,0x6e,0x9e,0x51,0x27,0x03,0xcd,0xee,0x1b,0xf3,0xbe,0x37,0x92},
+            ""
+        },
+        0,
+        { /* input taproot seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            "",
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        1,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0x3e,0x9f,0xce,0x73,0xd4,0xe7,0x7a,0x48,0x09,0x90,0x8e,0x3c,0x3a,0x2e,0x54,0xee,0x14,0x7b,0x93,0x12,0xdc,0x50,0x44,0xa1,0x93,0xd1,0xfc,0x85,0xde,0x46,0xe3,0xc1},
+            "",
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        1,
+        { /* outputs to scan */
+            {0x3e,0x9f,0xce,0x73,0xd4,0xe7,0x7a,0x48,0x09,0x90,0x8e,0x3c,0x3a,0x2e,0x54,0xee,0x14,0x7b,0x93,0x12,0xdc,0x50,0x44,0xa1,0x93,0xd1,0xfc,0x85,0xde,0x46,0xe3,0xc1},
+            "",
+            "",
+            ""
+        },
+        0, {0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        1,
+        {
+            {0x3e,0x9f,0xce,0x73,0xd4,0xe7,0x7a,0x48,0x09,0x90,0x8e,0x3c,0x3a,0x2e,0x54,0xee,0x14,0x7b,0x93,0x12,0xdc,0x50,0x44,0xa1,0x93,0xd1,0xfc,0x85,0xde,0x46,0xe3,0xc1},
+            "",
+            "",
+            ""
+        },
+        {
+            {0xf4,0x38,0xb4,0x01,0x79,0xa3,0xc4,0x26,0x2d,0xe1,0x29,0x86,0xc0,0xe6,0xcc,0xe0,0x63,0x40,0x07,0xcd,0xc7,0x9c,0x1d,0xcd,0x3e,0x20,0xb9,0xeb,0xc2,0xe7,0xee,0xf6},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x74,0xf8,0x5b,0x85,0x63,0x37,0xfb,0xe8,0x37,0x64,0x3b,0x86,0xf4,0x62,0x11,0x81,0x59,0xf9,0x3a,0xc4,0xac,0xc2,0x67,0x15,0x22,0xf2,0x7e,0x8f,0x67,0xb0,0x79,0x95,0x91,0x95,0xcc,0xc7,0xa5,0xdb,0xee,0x39,0x6d,0x29,0x09,0xf5,0xd6,0x80,0xd6,0xe3,0x0c,0xda,0x73,0x59,0xaa,0x27,0x55,0x82,0x25,0x09,0xb7,0x0d,0x6b,0x06,0x87,0xa1},
+            "",
+            "",
+            ""
+        }
+    },
+
+    /* ----- Simple send: two inputs, order reversed (2) ----- */
+    {
+        2,
+        { /* input plain seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            {0x93,0xf5,0xed,0x90,0x7a,0xd5,0xb2,0xbd,0xbb,0xdc,0xb5,0xd9,0x11,0x6e,0xbc,0x0a,0x4e,0x1f,0x92,0xf9,0x10,0xd5,0x26,0x02,0x37,0xfa,0x45,0xa9,0x40,0x8a,0xad,0x16},
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            {0x03,0xbd,0x85,0x68,0x5d,0x03,0xd1,0x11,0x69,0x9b,0x15,0xd0,0x46,0x31,0x9f,0xeb,0xe7,0x7f,0x8d,0xe5,0x28,0x6e,0x9e,0x51,0x27,0x03,0xcd,0xee,0x1b,0xf3,0xbe,0x37,0x92},
+            ""
+        },
+        0,
+        { /* input taproot seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            "",
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        1,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0x3e,0x9f,0xce,0x73,0xd4,0xe7,0x7a,0x48,0x09,0x90,0x8e,0x3c,0x3a,0x2e,0x54,0xee,0x14,0x7b,0x93,0x12,0xdc,0x50,0x44,0xa1,0x93,0xd1,0xfc,0x85,0xde,0x46,0xe3,0xc1},
+            "",
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        1,
+        { /* outputs to scan */
+            {0x3e,0x9f,0xce,0x73,0xd4,0xe7,0x7a,0x48,0x09,0x90,0x8e,0x3c,0x3a,0x2e,0x54,0xee,0x14,0x7b,0x93,0x12,0xdc,0x50,0x44,0xa1,0x93,0xd1,0xfc,0x85,0xde,0x46,0xe3,0xc1},
+            "",
+            "",
+            ""
+        },
+        0, {0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        1,
+        {
+            {0x3e,0x9f,0xce,0x73,0xd4,0xe7,0x7a,0x48,0x09,0x90,0x8e,0x3c,0x3a,0x2e,0x54,0xee,0x14,0x7b,0x93,0x12,0xdc,0x50,0x44,0xa1,0x93,0xd1,0xfc,0x85,0xde,0x46,0xe3,0xc1},
+            "",
+            "",
+            ""
+        },
+        {
+            {0xf4,0x38,0xb4,0x01,0x79,0xa3,0xc4,0x26,0x2d,0xe1,0x29,0x86,0xc0,0xe6,0xcc,0xe0,0x63,0x40,0x07,0xcd,0xc7,0x9c,0x1d,0xcd,0x3e,0x20,0xb9,0xeb,0xc2,0xe7,0xee,0xf6},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x74,0xf8,0x5b,0x85,0x63,0x37,0xfb,0xe8,0x37,0x64,0x3b,0x86,0xf4,0x62,0x11,0x81,0x59,0xf9,0x3a,0xc4,0xac,0xc2,0x67,0x15,0x22,0xf2,0x7e,0x8f,0x67,0xb0,0x79,0x95,0x91,0x95,0xcc,0xc7,0xa5,0xdb,0xee,0x39,0x6d,0x29,0x09,0xf5,0xd6,0x80,0xd6,0xe3,0x0c,0xda,0x73,0x59,0xaa,0x27,0x55,0x82,0x25,0x09,0xb7,0x0d,0x6b,0x06,0x87,0xa1},
+            "",
+            "",
+            ""
+        }
+    },
+
+    /* ----- Simple send: two inputs from the same transaction (3) ----- */
+    {
+        2,
+        { /* input plain seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            {0x93,0xf5,0xed,0x90,0x7a,0xd5,0xb2,0xbd,0xbb,0xdc,0xb5,0xd9,0x11,0x6e,0xbc,0x0a,0x4e,0x1f,0x92,0xf9,0x10,0xd5,0x26,0x02,0x37,0xfa,0x45,0xa9,0x40,0x8a,0xad,0x16},
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            {0x03,0xbd,0x85,0x68,0x5d,0x03,0xd1,0x11,0x69,0x9b,0x15,0xd0,0x46,0x31,0x9f,0xeb,0xe7,0x7f,0x8d,0xe5,0x28,0x6e,0x9e,0x51,0x27,0x03,0xcd,0xee,0x1b,0xf3,0xbe,0x37,0x92},
+            ""
+        },
+        0,
+        { /* input taproot seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            "",
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x03,0x00,0x00,0x00},
+        1,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0x79,0xe7,0x1b,0xaa,0x2b,0xa3,0xfc,0x66,0x39,0x6d,0xe3,0xa0,0x4f,0x16,0x8c,0x7b,0xf2,0x4d,0x68,0x70,0xec,0x88,0xca,0x87,0x77,0x54,0x79,0x0c,0x1d,0xb3,0x57,0xb6},
+            "",
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        1,
+        { /* outputs to scan */
+            {0x79,0xe7,0x1b,0xaa,0x2b,0xa3,0xfc,0x66,0x39,0x6d,0xe3,0xa0,0x4f,0x16,0x8c,0x7b,0xf2,0x4d,0x68,0x70,0xec,0x88,0xca,0x87,0x77,0x54,0x79,0x0c,0x1d,0xb3,0x57,0xb6},
+            "",
+            "",
+            ""
+        },
+        0, {0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        1,
+        {
+            {0x79,0xe7,0x1b,0xaa,0x2b,0xa3,0xfc,0x66,0x39,0x6d,0xe3,0xa0,0x4f,0x16,0x8c,0x7b,0xf2,0x4d,0x68,0x70,0xec,0x88,0xca,0x87,0x77,0x54,0x79,0x0c,0x1d,0xb3,0x57,0xb6},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x48,0x51,0x45,0x5b,0xfb,0xe1,0xab,0x4f,0x80,0x15,0x65,0x70,0xaa,0x45,0x06,0x32,0x01,0xaa,0x5c,0x9e,0x1b,0x1d,0xcd,0x29,0xf0,0xf8,0xc3,0x3d,0x10,0xbf,0x77,0xae},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x10,0x33,0x2e,0xea,0x80,0x8b,0x6a,0x13,0xf7,0x00,0x59,0xa8,0xa7,0x31,0x95,0x80,0x8d,0xb7,0x82,0x01,0x29,0x07,0xf5,0xba,0x32,0xb6,0xea,0xe6,0x6a,0x2f,0x66,0xb4,0xf6,0x51,0x47,0xe2,0xb9,0x68,0xa1,0x67,0x8c,0x5f,0x73,0xd5,0x7d,0x5d,0x19,0x5d,0xba,0xf6,0x67,0xb6,0x06,0xff,0x80,0xc8,0x49,0x0e,0xac,0x1f,0x3b,0x71,0x06,0x57},
+            "",
+            "",
+            ""
+        }
+    },
+
+    /* ----- Simple send: two inputs from the same transaction, order reversed (4) ----- */
+    {
+        2,
+        { /* input plain seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            {0x93,0xf5,0xed,0x90,0x7a,0xd5,0xb2,0xbd,0xbb,0xdc,0xb5,0xd9,0x11,0x6e,0xbc,0x0a,0x4e,0x1f,0x92,0xf9,0x10,0xd5,0x26,0x02,0x37,0xfa,0x45,0xa9,0x40,0x8a,0xad,0x16},
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            {0x03,0xbd,0x85,0x68,0x5d,0x03,0xd1,0x11,0x69,0x9b,0x15,0xd0,0x46,0x31,0x9f,0xeb,0xe7,0x7f,0x8d,0xe5,0x28,0x6e,0x9e,0x51,0x27,0x03,0xcd,0xee,0x1b,0xf3,0xbe,0x37,0x92},
+            ""
+        },
+        0,
+        { /* input taproot seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            "",
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x8d,0xd4,0xf5,0xfb,0xd5,0xe9,0x80,0xfc,0x02,0xf3,0x5c,0x6c,0xe1,0x45,0x93,0x5b,0x11,0xe2,0x84,0x60,0x5b,0xf5,0x99,0xa1,0x3c,0x6d,0x41,0x5d,0xb5,0x5d,0x07,0xa1,0x03,0x00,0x00,0x00},
+        1,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0xf4,0xc2,0xda,0x80,0x7f,0x89,0xcb,0x15,0x01,0xf1,0xa7,0x73,0x22,0xa8,0x95,0xac,0xfb,0x93,0xc2,0x8e,0x08,0xed,0x27,0x24,0xd2,0xbe,0xb8,0xe4,0x45,0x39,0xba,0x38},
+            "",
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        1,
+        { /* outputs to scan */
+            {0xf4,0xc2,0xda,0x80,0x7f,0x89,0xcb,0x15,0x01,0xf1,0xa7,0x73,0x22,0xa8,0x95,0xac,0xfb,0x93,0xc2,0x8e,0x08,0xed,0x27,0x24,0xd2,0xbe,0xb8,0xe4,0x45,0x39,0xba,0x38},
+            "",
+            "",
+            ""
+        },
+        0, {0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        1,
+        {
+            {0xf4,0xc2,0xda,0x80,0x7f,0x89,0xcb,0x15,0x01,0xf1,0xa7,0x73,0x22,0xa8,0x95,0xac,0xfb,0x93,0xc2,0x8e,0x08,0xed,0x27,0x24,0xd2,0xbe,0xb8,0xe4,0x45,0x39,0xba,0x38},
+            "",
+            "",
+            ""
+        },
+        {
+            {0xab,0x0c,0x9b,0x87,0x18,0x1b,0xf5,0x27,0x87,0x9f,0x48,0xdb,0x9f,0x14,0xa0,0x22,0x33,0x61,0x9b,0x98,0x6f,0x8e,0x8f,0x2d,0x5d,0x40,0x8c,0xe6,0x8a,0x70,0x9f,0x51},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x39,0x8a,0x97,0x90,0x86,0x57,0x91,0xa9,0xdb,0x41,0xa8,0x01,0x5a,0xfa,0xd3,0xa4,0x7d,0x60,0xfe,0xc5,0x08,0x6c,0x50,0x55,0x78,0x06,0xa4,0x9a,0x1b,0xc0,0x38,0x80,0x86,0x32,0xb8,0xfe,0x67,0x9a,0x7b,0xb6,0x5f,0xc6,0xb4,0x55,0xbe,0x99,0x45,0x02,0xee,0xd8,0x49,0xf1,0xda,0x37,0x29,0xcd,0x94,0x8f,0xc7,0xbe,0x73,0xd6,0x72,0x95},
+            "",
+            "",
+            ""
+        }
+    },
+
+    /* ----- Single recipient: multiple UTXOs from the same public key (5) ----- */
+    {
+        2,
+        { /* input plain seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            ""
+        },
+        0,
+        { /* input taproot seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            "",
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        1,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0x54,0x8a,0xe5,0x5c,0x8e,0xec,0x1e,0x73,0x6e,0x8d,0x3e,0x52,0x0f,0x01,0x1f,0x1f,0x42,0xa5,0x6d,0x16,0x61,0x16,0xad,0x21,0x0b,0x39,0x37,0x59,0x9f,0x87,0xf5,0x66},
+            "",
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        1,
+        { /* outputs to scan */
+            {0x54,0x8a,0xe5,0x5c,0x8e,0xec,0x1e,0x73,0x6e,0x8d,0x3e,0x52,0x0f,0x01,0x1f,0x1f,0x42,0xa5,0x6d,0x16,0x61,0x16,0xad,0x21,0x0b,0x39,0x37,0x59,0x9f,0x87,0xf5,0x66},
+            "",
+            "",
+            ""
+        },
+        0, {0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        1,
+        {
+            {0x54,0x8a,0xe5,0x5c,0x8e,0xec,0x1e,0x73,0x6e,0x8d,0x3e,0x52,0x0f,0x01,0x1f,0x1f,0x42,0xa5,0x6d,0x16,0x61,0x16,0xad,0x21,0x0b,0x39,0x37,0x59,0x9f,0x87,0xf5,0x66},
+            "",
+            "",
+            ""
+        },
+        {
+            {0xf0,0x32,0x69,0x5e,0x26,0x36,0x61,0x9e,0xfa,0x52,0x3f,0xff,0xaa,0x9e,0xf9,0x3c,0x88,0x02,0x29,0x91,0x81,0xfd,0x04,0x61,0x91,0x3c,0x1b,0x8d,0xaf,0x97,0x84,0xcd},
+            "",
+            "",
+            ""
+        },
+        {
+            {0xf2,0x38,0x38,0x6c,0x5d,0x5e,0x54,0x44,0xf8,0xd2,0xc7,0x5a,0xab,0xbc,0xb2,0x8c,0x34,0x6f,0x20,0x8c,0x76,0xf6,0x08,0x23,0xf5,0xde,0x3b,0x67,0xb7,0x9e,0x0e,0xc7,0x2e,0xa5,0xde,0x2d,0x7c,0xae,0xc3,0x14,0xe0,0x97,0x1d,0x34,0x54,0xf1,0x22,0xdd,0xa3,0x42,0xb3,0xee,0xde,0x01,0xb3,0x85,0x7e,0x83,0x65,0x4e,0x36,0xb2,0x5f,0x76},
+            "",
+            "",
+            ""
+        }
+    },
+
+    /* ----- Single recipient: taproot only inputs with even y-values (6) ----- */
+    {
+        0,
+        { /* input plain seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input plain pubkeys */
+            "",
+            "",
+            ""
+        },
+        2,
+        { /* input taproot seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            {0xfc,0x87,0x16,0xa9,0x7a,0x48,0xba,0x9a,0x05,0xa9,0x8a,0xe4,0x7b,0x5c,0xd2,0x01,0xa2,0x5a,0x7f,0xd5,0xd8,0xb7,0x3c,0x20,0x3c,0x5f,0x7b,0x6b,0x6b,0x3b,0x6a,0xd7},
+            ""
+        },
+        { /* input x-only pubkeys */
+            {0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            {0x78,0x2e,0xeb,0x91,0x34,0x31,0xca,0x6e,0x9b,0x8c,0x2f,0xd8,0x0a,0x5f,0x72,0xed,0x20,0x24,0xef,0x72,0xa3,0xc6,0xfb,0x10,0x26,0x3c,0x37,0x99,0x37,0x32,0x33,0x38},
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        1,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0xde,0x88,0xbe,0xa8,0xe7,0xff,0xc9,0xce,0x1a,0xf3,0x0d,0x11,0x32,0xf9,0x10,0x32,0x3c,0x50,0x51,0x85,0xae,0xc8,0xea,0xe3,0x61,0x67,0x04,0x21,0xe7,0x49,0xa1,0xfb},
+            "",
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        1,
+        { /* outputs to scan */
+            {0xde,0x88,0xbe,0xa8,0xe7,0xff,0xc9,0xce,0x1a,0xf3,0x0d,0x11,0x32,0xf9,0x10,0x32,0x3c,0x50,0x51,0x85,0xae,0xc8,0xea,0xe3,0x61,0x67,0x04,0x21,0xe7,0x49,0xa1,0xfb},
+            "",
+            "",
+            ""
+        },
+        0, {0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        1,
+        {
+            {0xde,0x88,0xbe,0xa8,0xe7,0xff,0xc9,0xce,0x1a,0xf3,0x0d,0x11,0x32,0xf9,0x10,0x32,0x3c,0x50,0x51,0x85,0xae,0xc8,0xea,0xe3,0x61,0x67,0x04,0x21,0xe7,0x49,0xa1,0xfb},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x3f,0xb9,0xce,0x5c,0xe1,0x74,0x6c,0xed,0x10,0x3c,0x8e,0xd2,0x54,0xe8,0x1f,0x66,0x90,0x76,0x46,0x37,0xdd,0xbc,0x87,0x6e,0xc1,0xf9,0xb3,0xdd,0xab,0x77,0x6b,0x03},
+            "",
+            "",
+            ""
+        },
+        {
+            {0xc5,0xac,0xd2,0x5a,0x8f,0x02,0x1a,0x41,0x92,0xf9,0x3b,0xc3,0x44,0x03,0xfd,0x8b,0x76,0x48,0x46,0x13,0x46,0x63,0x36,0xfb,0x25,0x9c,0x72,0xd0,0x4c,0x16,0x98,0x24,0xf2,0x69,0x0c,0xa3,0x4e,0x96,0xce,0xe8,0x6b,0x69,0xf3,0x76,0xc8,0x37,0x70,0x03,0x26,0x8f,0xda,0x56,0xfe,0xeb,0x1b,0x87,0x3e,0x57,0x83,0xd7,0xe1,0x9b,0xcc,0xa5},
+            "",
+            "",
+            ""
+        }
+    },
+
+    /* ----- Single recipient: taproot only with mixed even/odd y-values (7) ----- */
+    {
+        0,
+        { /* input plain seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input plain pubkeys */
+            "",
+            "",
+            ""
+        },
+        2,
+        { /* input taproot seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            {0x1d,0x37,0x78,0x7c,0x2b,0x71,0x16,0xee,0x98,0x3e,0x9f,0x9c,0x13,0x26,0x9d,0xf2,0x90,0x91,0xb3,0x91,0xc0,0x4d,0xb9,0x42,0x39,0xe0,0xd2,0xbc,0x21,0x82,0xc3,0xbf},
+            ""
+        },
+        { /* input x-only pubkeys */
+            {0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            {0x8c,0x8d,0x23,0xd4,0x76,0x4f,0xef,0xfc,0xd5,0xe7,0x2e,0x38,0x08,0x02,0x54,0x0f,0xa0,0xf8,0x8e,0x3d,0x62,0xad,0x5e,0x0b,0x47,0x95,0x5f,0x74,0xd7,0xb2,0x83,0xc4},
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        1,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0x77,0xca,0xb7,0xdd,0x12,0xb1,0x02,0x59,0xee,0x82,0xc6,0xea,0x4b,0x50,0x97,0x74,0xe3,0x3e,0x70,0x78,0xe7,0x13,0x8f,0x56,0x80,0x92,0x24,0x1b,0xf2,0x6b,0x99,0xf1},
+            "",
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        1,
+        { /* outputs to scan */
+            {0x77,0xca,0xb7,0xdd,0x12,0xb1,0x02,0x59,0xee,0x82,0xc6,0xea,0x4b,0x50,0x97,0x74,0xe3,0x3e,0x70,0x78,0xe7,0x13,0x8f,0x56,0x80,0x92,0x24,0x1b,0xf2,0x6b,0x99,0xf1},
+            "",
+            "",
+            ""
+        },
+        0, {0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        1,
+        {
+            {0x77,0xca,0xb7,0xdd,0x12,0xb1,0x02,0x59,0xee,0x82,0xc6,0xea,0x4b,0x50,0x97,0x74,0xe3,0x3e,0x70,0x78,0xe7,0x13,0x8f,0x56,0x80,0x92,0x24,0x1b,0xf2,0x6b,0x99,0xf1},
+            "",
+            "",
+            ""
+        },
+        {
+            {0xf5,0x38,0x25,0x08,0x60,0x97,0x71,0x06,0x8e,0xd0,0x79,0xb2,0x4e,0x1f,0x72,0xe4,0xa1,0x7e,0xe6,0xd1,0xc9,0x79,0x06,0x6b,0xf1,0xd4,0xe2,0xa5,0x67,0x6f,0x09,0xd4},
+            "",
+            "",
+            ""
+        },
+        {
+            {0xff,0x65,0x83,0x3b,0x8f,0xd1,0xed,0x3e,0xf9,0xd0,0x44,0x3b,0x4f,0x70,0x2b,0x45,0xa3,0xf2,0xdd,0x45,0x7b,0xa2,0x47,0x68,0x7e,0x82,0x07,0x74,0x5c,0x3b,0xe9,0xd2,0xbd,0xad,0x0a,0xb3,0xf0,0x71,0x18,0xf8,0xb2,0xef,0xc6,0xa0,0x4b,0x95,0xf7,0xb3,0xe2,0x18,0xda,0xf8,0xa6,0x41,0x37,0xec,0x91,0xbd,0x2f,0xc6,0x7f,0xc1,0x37,0xa5},
+            "",
+            "",
+            ""
+        }
+    },
+
+    /* ----- Single recipient: taproot input with even y-value and non-taproot input (8) ----- */
+    {
+        1,
+        { /* input plain seckeys */
+            {0x8d,0x47,0x51,0xf6,0xe8,0xa3,0x58,0x68,0x80,0xfb,0x66,0xc1,0x9a,0xe2,0x77,0x96,0x9b,0xd5,0xaa,0x06,0xf6,0x1c,0x4e,0xe2,0xf1,0xe2,0x48,0x6e,0xfd,0xf6,0x66,0xd3},
+            "",
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x03,0xe0,0xec,0x4f,0x64,0xb3,0xfa,0x2e,0x46,0x3c,0xcf,0xcf,0x4e,0x85,0x6e,0x37,0xd5,0xe1,0xe2,0x02,0x75,0xbc,0x89,0xec,0x1d,0xef,0x9e,0xb0,0x98,0xef,0xf1,0xf8,0x5d},
+            "",
+            ""
+        },
+        1,
+        { /* input taproot seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            {0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        1,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0x30,0x52,0x3c,0xca,0x96,0xb2,0xa9,0xae,0x3c,0x98,0xbe,0xb5,0xe6,0x0f,0x7d,0x19,0x0e,0xc5,0xbc,0x79,0xb2,0xd1,0x1a,0x0b,0x2d,0x4d,0x09,0xa6,0x08,0xc4,0x48,0xf0},
+            "",
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        1,
+        { /* outputs to scan */
+            {0x30,0x52,0x3c,0xca,0x96,0xb2,0xa9,0xae,0x3c,0x98,0xbe,0xb5,0xe6,0x0f,0x7d,0x19,0x0e,0xc5,0xbc,0x79,0xb2,0xd1,0x1a,0x0b,0x2d,0x4d,0x09,0xa6,0x08,0xc4,0x48,0xf0},
+            "",
+            "",
+            ""
+        },
+        0, {0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        1,
+        {
+            {0x30,0x52,0x3c,0xca,0x96,0xb2,0xa9,0xae,0x3c,0x98,0xbe,0xb5,0xe6,0x0f,0x7d,0x19,0x0e,0xc5,0xbc,0x79,0xb2,0xd1,0x1a,0x0b,0x2d,0x4d,0x09,0xa6,0x08,0xc4,0x48,0xf0},
+            "",
+            "",
+            ""
+        },
+        {
+            {0xb4,0x00,0x17,0x86,0x5c,0x79,0xb1,0xfc,0xbe,0xd6,0x88,0x96,0x79,0x1b,0xe9,0x31,0x86,0xd0,0x8f,0x47,0xe4,0x16,0xb2,0x89,0xb8,0xc0,0x63,0x77,0x7e,0x14,0xe8,0xdf},
+            "",
+            "",
+            ""
+        },
+        {
+            {0xd1,0xed,0xee,0xa2,0x8c,0xf1,0x03,0x3b,0xcb,0x3d,0x89,0x37,0x6c,0xab,0xaa,0xaa,0x28,0x86,0xcb,0xd8,0xfd,0xa1,0x12,0xb5,0xc6,0x1c,0xc9,0x0a,0x4e,0x7f,0x18,0x78,0xbd,0xd6,0x21,0x80,0xb0,0x7d,0x1d,0xfc,0x8f,0xfe,0xe1,0x86,0x3c,0x52,0x5a,0x0c,0x7b,0x5b,0xcd,0x41,0x31,0x83,0x28,0x2c,0xfd,0xa7,0x56,0xcb,0x65,0x78,0x72,0x66},
+            "",
+            "",
+            ""
+        }
+    },
+
+    /* ----- Single recipient: taproot input with odd y-value and non-taproot input (9) ----- */
+    {
+        1,
+        { /* input plain seckeys */
+            {0x8d,0x47,0x51,0xf6,0xe8,0xa3,0x58,0x68,0x80,0xfb,0x66,0xc1,0x9a,0xe2,0x77,0x96,0x9b,0xd5,0xaa,0x06,0xf6,0x1c,0x4e,0xe2,0xf1,0xe2,0x48,0x6e,0xfd,0xf6,0x66,0xd3},
+            "",
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x03,0xe0,0xec,0x4f,0x64,0xb3,0xfa,0x2e,0x46,0x3c,0xcf,0xcf,0x4e,0x85,0x6e,0x37,0xd5,0xe1,0xe2,0x02,0x75,0xbc,0x89,0xec,0x1d,0xef,0x9e,0xb0,0x98,0xef,0xf1,0xf8,0x5d},
+            "",
+            ""
+        },
+        1,
+        { /* input taproot seckeys */
+            {0x1d,0x37,0x78,0x7c,0x2b,0x71,0x16,0xee,0x98,0x3e,0x9f,0x9c,0x13,0x26,0x9d,0xf2,0x90,0x91,0xb3,0x91,0xc0,0x4d,0xb9,0x42,0x39,0xe0,0xd2,0xbc,0x21,0x82,0xc3,0xbf},
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            {0x8c,0x8d,0x23,0xd4,0x76,0x4f,0xef,0xfc,0xd5,0xe7,0x2e,0x38,0x08,0x02,0x54,0x0f,0xa0,0xf8,0x8e,0x3d,0x62,0xad,0x5e,0x0b,0x47,0x95,0x5f,0x74,0xd7,0xb2,0x83,0xc4},
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        1,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0x35,0x93,0x58,0xf5,0x9e,0xe9,0xe9,0xee,0xc3,0xf0,0x0b,0xdf,0x48,0x82,0x57,0x0f,0xd5,0xc1,0x82,0xe4,0x51,0xaa,0x26,0x50,0xb7,0x88,0x54,0x4a,0xff,0x01,0x2a,0x3a},
+            "",
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        1,
+        { /* outputs to scan */
+            {0x35,0x93,0x58,0xf5,0x9e,0xe9,0xe9,0xee,0xc3,0xf0,0x0b,0xdf,0x48,0x82,0x57,0x0f,0xd5,0xc1,0x82,0xe4,0x51,0xaa,0x26,0x50,0xb7,0x88,0x54,0x4a,0xff,0x01,0x2a,0x3a},
+            "",
+            "",
+            ""
+        },
+        0, {0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        1,
+        {
+            {0x35,0x93,0x58,0xf5,0x9e,0xe9,0xe9,0xee,0xc3,0xf0,0x0b,0xdf,0x48,0x82,0x57,0x0f,0xd5,0xc1,0x82,0xe4,0x51,0xaa,0x26,0x50,0xb7,0x88,0x54,0x4a,0xff,0x01,0x2a,0x3a},
+            "",
+            "",
+            ""
+        },
+        {
+            {0xa2,0xf9,0xdd,0x05,0xd1,0xd3,0x98,0x34,0x7c,0x88,0x5d,0x9c,0x61,0xa6,0x4d,0x18,0xa2,0x64,0xde,0x6d,0x49,0xce,0xa4,0x32,0x6b,0xaf,0xc2,0x79,0x1d,0x62,0x7f,0xa7},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x96,0x03,0x8a,0xd2,0x33,0xd8,0xbe,0xfe,0x34,0x25,0x73,0xa6,0xe5,0x48,0x28,0xd8,0x63,0x47,0x1f,0xb2,0xaf,0xba,0xd5,0x75,0xcc,0x65,0x27,0x1a,0x2a,0x64,0x94,0x80,0xea,0x14,0x91,0x2b,0x6a,0xbb,0xd3,0xfb,0xf9,0x2e,0xfc,0x19,0x28,0xc0,0x36,0xf6,0xe3,0xee,0xf9,0x27,0x10,0x5a,0xf4,0xec,0x1d,0xd5,0x7c,0xb9,0x09,0xf3,0x60,0xb8},
+            "",
+            "",
+            ""
+        }
+    },
+
+    /* ----- Multiple outputs: multiple outputs, same recipient (10) ----- */
+    {
+        2,
+        { /* input plain seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            {0x03,0x78,0xe9,0x56,0x85,0xb7,0x45,0x65,0xfa,0x56,0x75,0x1b,0x84,0xa3,0x2d,0xfd,0x18,0x54,0x5d,0x10,0xd6,0x91,0x64,0x1b,0x83,0x72,0xe3,0x21,0x64,0xfa,0xd6,0x6a},
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            {0x03,0x78,0x2e,0xeb,0x91,0x34,0x31,0xca,0x6e,0x9b,0x8c,0x2f,0xd8,0x0a,0x5f,0x72,0xed,0x20,0x24,0xef,0x72,0xa3,0xc6,0xfb,0x10,0x26,0x3c,0x37,0x99,0x37,0x32,0x33,0x38},
+            ""
+        },
+        0,
+        { /* input taproot seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            "",
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        2,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0xf2,0x07,0x16,0x2b,0x1a,0x7a,0xbc,0x51,0xc4,0x20,0x17,0xbe,0xf0,0x55,0xe9,0xec,0x1e,0xfc,0x3d,0x35,0x67,0xcb,0x72,0x03,0x57,0xe2,0xb8,0x43,0x25,0xdb,0x33,0xac},
+            {0xe9,0x76,0xa5,0x8f,0xbd,0x38,0xae,0xb4,0xe6,0x09,0x3d,0x4d,0xf0,0x2e,0x9c,0x1d,0xe0,0xc4,0x51,0x3a,0xe0,0xc5,0x88,0xce,0xf6,0x8c,0xda,0x5b,0x2f,0x88,0x34,0xca},
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        4,
+        { /* outputs to scan */
+            {0xf2,0x07,0x16,0x2b,0x1a,0x7a,0xbc,0x51,0xc4,0x20,0x17,0xbe,0xf0,0x55,0xe9,0xec,0x1e,0xfc,0x3d,0x35,0x67,0xcb,0x72,0x03,0x57,0xe2,0xb8,0x43,0x25,0xdb,0x33,0xac},
+            {0xe9,0x76,0xa5,0x8f,0xbd,0x38,0xae,0xb4,0xe6,0x09,0x3d,0x4d,0xf0,0x2e,0x9c,0x1d,0xe0,0xc4,0x51,0x3a,0xe0,0xc5,0x88,0xce,0xf6,0x8c,0xda,0x5b,0x2f,0x88,0x34,0xca},
+            {0x84,0x17,0x92,0xc3,0x3c,0x9d,0xc6,0x19,0x3e,0x76,0x74,0x41,0x34,0x12,0x5d,0x40,0xad,0xd8,0xf2,0xf4,0xa9,0x64,0x75,0xf2,0x8b,0xa1,0x50,0xbe,0x03,0x2d,0x64,0xe8},
+            {0x2e,0x84,0x7b,0xb0,0x1d,0x1b,0x49,0x1d,0xa5,0x12,0xdd,0xd7,0x60,0xb8,0x50,0x96,0x17,0xee,0x38,0x05,0x70,0x03,0xd6,0x11,0x5d,0x00,0xba,0x56,0x24,0x51,0x32,0x3a}
+        },
+        0, {0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        2,
+        {
+            {0xf2,0x07,0x16,0x2b,0x1a,0x7a,0xbc,0x51,0xc4,0x20,0x17,0xbe,0xf0,0x55,0xe9,0xec,0x1e,0xfc,0x3d,0x35,0x67,0xcb,0x72,0x03,0x57,0xe2,0xb8,0x43,0x25,0xdb,0x33,0xac},
+            {0xe9,0x76,0xa5,0x8f,0xbd,0x38,0xae,0xb4,0xe6,0x09,0x3d,0x4d,0xf0,0x2e,0x9c,0x1d,0xe0,0xc4,0x51,0x3a,0xe0,0xc5,0x88,0xce,0xf6,0x8c,0xda,0x5b,0x2f,0x88,0x34,0xca},
+            "",
+            ""
+        },
+        {
+            {0x33,0xce,0x08,0x5c,0x3c,0x11,0xea,0xad,0x13,0x69,0x4a,0xae,0x3c,0x20,0x30,0x1a,0x6c,0x83,0x38,0x2e,0xc8,0x9a,0x7c,0xde,0x96,0xc6,0x79,0x9e,0x2f,0x88,0x80,0x5a},
+            {0xd9,0x7e,0x44,0x2d,0x11,0x0c,0x0b,0xdd,0x31,0x16,0x1a,0x7b,0xb6,0xe7,0x86,0x2e,0x03,0x8d,0x02,0xa0,0x9b,0x14,0x84,0xdf,0xbb,0x46,0x3f,0x2e,0x0f,0x7c,0x92,0x30},
+            "",
+            ""
+        },
+        {
+            {0x33,0x56,0x67,0xca,0x6c,0xae,0x7a,0x26,0x43,0x8f,0x5c,0xfd,0xd7,0x3b,0x3d,0x48,0xfa,0x83,0x2f,0xa9,0x76,0x85,0x21,0xd7,0xd5,0x44,0x5f,0x22,0xc2,0x03,0xab,0x0d,0x74,0xed,0x85,0x08,0x8f,0x27,0xd2,0x99,0x59,0xba,0x62,0x7a,0x45,0x09,0x99,0x66,0x76,0xf4,0x7d,0xf8,0xff,0x28,0x4d,0x29,0x25,0x67,0xb1,0xbe,0xef,0x0e,0x39,0x12},
+            {0x29,0xbd,0x25,0xd0,0xf8,0x08,0xd7,0xfc,0xd2,0xaa,0x6d,0x5e,0xd2,0x06,0x05,0x38,0x99,0x19,0x83,0x97,0x50,0x6c,0x30,0x1b,0x21,0x8a,0x9e,0x47,0xa3,0xd7,0x07,0x0a,0xf0,0x3e,0x90,0x3f,0xf7,0x18,0x97,0x8d,0x50,0xd1,0xb6,0xb9,0xaf,0x8c,0xc0,0xe3,0x13,0xd8,0x4e,0xda,0x5d,0x5b,0x1e,0x8e,0x85,0xe5,0x51,0x6d,0x63,0x0b,0xbe,0xb9},
+            "",
+            ""
+        }
+    },
+
+    /* ----- Multiple outputs: multiple outputs, multiple recipients (11) ----- */
+    {
+        2,
+        { /* input plain seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            {0x03,0x78,0xe9,0x56,0x85,0xb7,0x45,0x65,0xfa,0x56,0x75,0x1b,0x84,0xa3,0x2d,0xfd,0x18,0x54,0x5d,0x10,0xd6,0x91,0x64,0x1b,0x83,0x72,0xe3,0x21,0x64,0xfa,0xd6,0x6a},
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            {0x03,0x78,0x2e,0xeb,0x91,0x34,0x31,0xca,0x6e,0x9b,0x8c,0x2f,0xd8,0x0a,0x5f,0x72,0xed,0x20,0x24,0xef,0x72,0xa3,0xc6,0xfb,0x10,0x26,0x3c,0x37,0x99,0x37,0x32,0x33,0x38},
+            ""
+        },
+        0,
+        { /* input taproot seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            "",
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        4,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                {0x02,0x06,0x2d,0x49,0xff,0xc0,0x27,0x87,0xd5,0x86,0xc6,0x08,0xdf,0xbe,0xc1,0x84,0xaa,0x91,0xa6,0x59,0x7d,0x97,0xb4,0x63,0xea,0x5c,0x6b,0xab,0xd9,0xd1,0x7a,0x95,0xa3},
+                {0x03,0x81,0xeb,0x9a,0x9a,0x9e,0xc7,0x39,0xd5,0x27,0xc1,0x63,0x1b,0x31,0xb4,0x21,0x56,0x6f,0x5c,0x2a,0x47,0xb4,0xab,0x5b,0x1f,0x6a,0x68,0x6d,0xfb,0x68,0xea,0xb7,0x16}
+            },
+            {
+                {0x02,0x06,0x2d,0x49,0xff,0xc0,0x27,0x87,0xd5,0x86,0xc6,0x08,0xdf,0xbe,0xc1,0x84,0xaa,0x91,0xa6,0x59,0x7d,0x97,0xb4,0x63,0xea,0x5c,0x6b,0xab,0xd9,0xd1,0x7a,0x95,0xa3},
+                {0x03,0x81,0xeb,0x9a,0x9a,0x9e,0xc7,0x39,0xd5,0x27,0xc1,0x63,0x1b,0x31,0xb4,0x21,0x56,0x6f,0x5c,0x2a,0x47,0xb4,0xab,0x5b,0x1f,0x6a,0x68,0x6d,0xfb,0x68,0xea,0xb7,0x16}
+            }
+        },
+        { /* recipient outputs */
+            {0xf2,0x07,0x16,0x2b,0x1a,0x7a,0xbc,0x51,0xc4,0x20,0x17,0xbe,0xf0,0x55,0xe9,0xec,0x1e,0xfc,0x3d,0x35,0x67,0xcb,0x72,0x03,0x57,0xe2,0xb8,0x43,0x25,0xdb,0x33,0xac},
+            {0xe9,0x76,0xa5,0x8f,0xbd,0x38,0xae,0xb4,0xe6,0x09,0x3d,0x4d,0xf0,0x2e,0x9c,0x1d,0xe0,0xc4,0x51,0x3a,0xe0,0xc5,0x88,0xce,0xf6,0x8c,0xda,0x5b,0x2f,0x88,0x34,0xca},
+            {0x84,0x17,0x92,0xc3,0x3c,0x9d,0xc6,0x19,0x3e,0x76,0x74,0x41,0x34,0x12,0x5d,0x40,0xad,0xd8,0xf2,0xf4,0xa9,0x64,0x75,0xf2,0x8b,0xa1,0x50,0xbe,0x03,0x2d,0x64,0xe8},
+            {0x2e,0x84,0x7b,0xb0,0x1d,0x1b,0x49,0x1d,0xa5,0x12,0xdd,0xd7,0x60,0xb8,0x50,0x96,0x17,0xee,0x38,0x05,0x70,0x03,0xd6,0x11,0x5d,0x00,0xba,0x56,0x24,0x51,0x32,0x3a}
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        4,
+        { /* outputs to scan */
+            {0xf2,0x07,0x16,0x2b,0x1a,0x7a,0xbc,0x51,0xc4,0x20,0x17,0xbe,0xf0,0x55,0xe9,0xec,0x1e,0xfc,0x3d,0x35,0x67,0xcb,0x72,0x03,0x57,0xe2,0xb8,0x43,0x25,0xdb,0x33,0xac},
+            {0xe9,0x76,0xa5,0x8f,0xbd,0x38,0xae,0xb4,0xe6,0x09,0x3d,0x4d,0xf0,0x2e,0x9c,0x1d,0xe0,0xc4,0x51,0x3a,0xe0,0xc5,0x88,0xce,0xf6,0x8c,0xda,0x5b,0x2f,0x88,0x34,0xca},
+            {0x84,0x17,0x92,0xc3,0x3c,0x9d,0xc6,0x19,0x3e,0x76,0x74,0x41,0x34,0x12,0x5d,0x40,0xad,0xd8,0xf2,0xf4,0xa9,0x64,0x75,0xf2,0x8b,0xa1,0x50,0xbe,0x03,0x2d,0x64,0xe8},
+            {0x2e,0x84,0x7b,0xb0,0x1d,0x1b,0x49,0x1d,0xa5,0x12,0xdd,0xd7,0x60,0xb8,0x50,0x96,0x17,0xee,0x38,0x05,0x70,0x03,0xd6,0x11,0x5d,0x00,0xba,0x56,0x24,0x51,0x32,0x3a}
+        },
+        0, {0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        2,
+        {
+            {0xf2,0x07,0x16,0x2b,0x1a,0x7a,0xbc,0x51,0xc4,0x20,0x17,0xbe,0xf0,0x55,0xe9,0xec,0x1e,0xfc,0x3d,0x35,0x67,0xcb,0x72,0x03,0x57,0xe2,0xb8,0x43,0x25,0xdb,0x33,0xac},
+            {0xe9,0x76,0xa5,0x8f,0xbd,0x38,0xae,0xb4,0xe6,0x09,0x3d,0x4d,0xf0,0x2e,0x9c,0x1d,0xe0,0xc4,0x51,0x3a,0xe0,0xc5,0x88,0xce,0xf6,0x8c,0xda,0x5b,0x2f,0x88,0x34,0xca},
+            "",
+            ""
+        },
+        {
+            {0x33,0xce,0x08,0x5c,0x3c,0x11,0xea,0xad,0x13,0x69,0x4a,0xae,0x3c,0x20,0x30,0x1a,0x6c,0x83,0x38,0x2e,0xc8,0x9a,0x7c,0xde,0x96,0xc6,0x79,0x9e,0x2f,0x88,0x80,0x5a},
+            {0xd9,0x7e,0x44,0x2d,0x11,0x0c,0x0b,0xdd,0x31,0x16,0x1a,0x7b,0xb6,0xe7,0x86,0x2e,0x03,0x8d,0x02,0xa0,0x9b,0x14,0x84,0xdf,0xbb,0x46,0x3f,0x2e,0x0f,0x7c,0x92,0x30},
+            "",
+            ""
+        },
+        {
+            {0x33,0x56,0x67,0xca,0x6c,0xae,0x7a,0x26,0x43,0x8f,0x5c,0xfd,0xd7,0x3b,0x3d,0x48,0xfa,0x83,0x2f,0xa9,0x76,0x85,0x21,0xd7,0xd5,0x44,0x5f,0x22,0xc2,0x03,0xab,0x0d,0x74,0xed,0x85,0x08,0x8f,0x27,0xd2,0x99,0x59,0xba,0x62,0x7a,0x45,0x09,0x99,0x66,0x76,0xf4,0x7d,0xf8,0xff,0x28,0x4d,0x29,0x25,0x67,0xb1,0xbe,0xef,0x0e,0x39,0x12},
+            {0x29,0xbd,0x25,0xd0,0xf8,0x08,0xd7,0xfc,0xd2,0xaa,0x6d,0x5e,0xd2,0x06,0x05,0x38,0x99,0x19,0x83,0x97,0x50,0x6c,0x30,0x1b,0x21,0x8a,0x9e,0x47,0xa3,0xd7,0x07,0x0a,0xf0,0x3e,0x90,0x3f,0xf7,0x18,0x97,0x8d,0x50,0xd1,0xb6,0xb9,0xaf,0x8c,0xc0,0xe3,0x13,0xd8,0x4e,0xda,0x5d,0x5b,0x1e,0x8e,0x85,0xe5,0x51,0x6d,0x63,0x0b,0xbe,0xb9},
+            "",
+            ""
+        }
+    },
+
+    /* ----- Receiving with labels: label with odd parity (12) ----- */
+    {
+        2,
+        { /* input plain seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            {0x03,0x78,0xe9,0x56,0x85,0xb7,0x45,0x65,0xfa,0x56,0x75,0x1b,0x84,0xa3,0x2d,0xfd,0x18,0x54,0x5d,0x10,0xd6,0x91,0x64,0x1b,0x83,0x72,0xe3,0x21,0x64,0xfa,0xd6,0x6a},
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            {0x03,0x78,0x2e,0xeb,0x91,0x34,0x31,0xca,0x6e,0x9b,0x8c,0x2f,0xd8,0x0a,0x5f,0x72,0xed,0x20,0x24,0xef,0x72,0xa3,0xc6,0xfb,0x10,0x26,0x3c,0x37,0x99,0x37,0x32,0x33,0x38},
+            ""
+        },
+        0,
+        { /* input taproot seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            "",
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        1,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x59,0x35,0x2a,0xdd,0x83,0x7b,0x66,0x86,0xe8,0xd2,0x2b,0x87,0x01,0x78,0x14,0xa4,0x6b,0x3a,0xd3,0x08,0x70,0x21,0x67,0xc6,0x5b,0xd5,0xc8,0x59,0x9c,0xd2,0x8d,0x1c}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0xd0,0x14,0xd4,0x86,0x0f,0x67,0xd6,0x07,0xd6,0x0b,0x1a,0xf7,0x0e,0x0e,0xe2,0x36,0xb9,0x96,0x58,0xb6,0x1b,0xb7,0x69,0x83,0x2a,0xcb,0xbe,0x87,0xc3,0x74,0x43,0x9a},
+            "",
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        1,
+        { /* outputs to scan */
+            {0xd0,0x14,0xd4,0x86,0x0f,0x67,0xd6,0x07,0xd6,0x0b,0x1a,0xf7,0x0e,0x0e,0xe2,0x36,0xb9,0x96,0x58,0xb6,0x1b,0xb7,0x69,0x83,0x2a,0xcb,0xbe,0x87,0xc3,0x74,0x43,0x9a},
+            "",
+            "",
+            ""
+        },
+        3, {2, 3, 1001337, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        1,
+        {
+            {0xd0,0x14,0xd4,0x86,0x0f,0x67,0xd6,0x07,0xd6,0x0b,0x1a,0xf7,0x0e,0x0e,0xe2,0x36,0xb9,0x96,0x58,0xb6,0x1b,0xb7,0x69,0x83,0x2a,0xcb,0xbe,0x87,0xc3,0x74,0x43,0x9a},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x51,0xd4,0xe9,0xd0,0xd4,0x82,0xb5,0x70,0x01,0x09,0xb4,0xb2,0xe1,0x6f,0xf5,0x08,0x26,0x9b,0x03,0xd8,0x00,0x19,0x2a,0x04,0x3d,0x61,0xdc,0xa4,0xa0,0xa7,0x2a,0x52},
+            "",
+            "",
+            ""
+        },
+        {
+            {0xc3,0x0f,0xa6,0x3b,0xad,0x6f,0x0a,0x31,0x7f,0x39,0xa7,0x73,0xa5,0xcb,0xf0,0xb0,0xf8,0x19,0x3c,0x71,0xdf,0xeb,0xba,0x05,0xee,0x6a,0xe4,0xed,0x28,0xe3,0x77,0x5e,0x6e,0x04,0xc3,0xea,0x70,0xa8,0x37,0x03,0xbb,0x88,0x81,0x22,0x85,0x5d,0xc8,0x94,0xca,0xb6,0x16,0x92,0xe7,0xfd,0x10,0xc9,0xb3,0x49,0x4d,0x47,0x9a,0x60,0x78,0x5e},
+            "",
+            "",
+            ""
+        }
+    },
+
+    /* ----- Receiving with labels: label with odd parity (13) ----- */
+    {
+        2,
+        { /* input plain seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            {0x03,0x78,0xe9,0x56,0x85,0xb7,0x45,0x65,0xfa,0x56,0x75,0x1b,0x84,0xa3,0x2d,0xfd,0x18,0x54,0x5d,0x10,0xd6,0x91,0x64,0x1b,0x83,0x72,0xe3,0x21,0x64,0xfa,0xd6,0x6a},
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            {0x03,0x78,0x2e,0xeb,0x91,0x34,0x31,0xca,0x6e,0x9b,0x8c,0x2f,0xd8,0x0a,0x5f,0x72,0xed,0x20,0x24,0xef,0x72,0xa3,0xc6,0xfb,0x10,0x26,0x3c,0x37,0x99,0x37,0x32,0x33,0x38},
+            ""
+        },
+        0,
+        { /* input taproot seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            "",
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        1,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x08,0xa1,0x44,0xa1,0x84,0x33,0xa8,0x3f,0x63,0x3c,0x82,0x2c,0x1b,0xf5,0xee,0x4c,0x8c,0x8e,0x24,0x60,0x1d,0x6c,0xa7,0x5e,0x20,0xa7,0xdc,0x57,0xa0,0xff,0x92,0x80}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0x67,0x62,0x6a,0xeb,0xb3,0xc4,0x30,0x7c,0xf0,0xf6,0xc3,0x9c,0xa2,0x32,0x47,0x59,0x8f,0xab,0xf6,0x75,0xab,0x78,0x32,0x92,0xeb,0x2f,0x81,0xae,0x75,0xad,0x1f,0x8c},
+            "",
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        1,
+        { /* outputs to scan */
+            {0x67,0x62,0x6a,0xeb,0xb3,0xc4,0x30,0x7c,0xf0,0xf6,0xc3,0x9c,0xa2,0x32,0x47,0x59,0x8f,0xab,0xf6,0x75,0xab,0x78,0x32,0x92,0xeb,0x2f,0x81,0xae,0x75,0xad,0x1f,0x8c},
+            "",
+            "",
+            ""
+        },
+        3, {2, 3, 1001337, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        1,
+        {
+            {0x67,0x62,0x6a,0xeb,0xb3,0xc4,0x30,0x7c,0xf0,0xf6,0xc3,0x9c,0xa2,0x32,0x47,0x59,0x8f,0xab,0xf6,0x75,0xab,0x78,0x32,0x92,0xeb,0x2f,0x81,0xae,0x75,0xad,0x1f,0x8c},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x60,0x24,0xae,0x21,0x48,0x76,0x35,0x6b,0x8d,0x91,0x77,0x16,0xe7,0x70,0x7d,0x26,0x7a,0xe1,0x6a,0x0f,0xdb,0x07,0xde,0x2a,0x78,0x6b,0x74,0xa7,0xbb,0xcd,0xde,0xad},
+            "",
+            "",
+            ""
+        },
+        {
+            {0xa8,0x6d,0x55,0x4d,0x0d,0x6b,0x7a,0xa0,0x90,0x71,0x55,0xf7,0xe0,0xb4,0x7f,0x01,0x82,0x75,0x24,0x72,0xff,0xfa,0xed,0xdd,0x68,0xda,0x90,0xe9,0x9b,0x94,0x02,0xf1,0x66,0xfd,0x9b,0x33,0x03,0x9c,0x30,0x2c,0x71,0x15,0x09,0x8d,0x97,0x1c,0x13,0x99,0xe6,0x7c,0x19,0xe9,0xe4,0xde,0x18,0x0b,0x10,0xea,0x0b,0x9d,0x6f,0x0d,0xb8,0x32},
+            "",
+            "",
+            ""
+        }
+    },
+
+    /* ----- Receiving with labels: label with odd parity (14) ----- */
+    {
+        2,
+        { /* input plain seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            {0x03,0x78,0xe9,0x56,0x85,0xb7,0x45,0x65,0xfa,0x56,0x75,0x1b,0x84,0xa3,0x2d,0xfd,0x18,0x54,0x5d,0x10,0xd6,0x91,0x64,0x1b,0x83,0x72,0xe3,0x21,0x64,0xfa,0xd6,0x6a},
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            {0x03,0x78,0x2e,0xeb,0x91,0x34,0x31,0xca,0x6e,0x9b,0x8c,0x2f,0xd8,0x0a,0x5f,0x72,0xed,0x20,0x24,0xef,0x72,0xa3,0xc6,0xfb,0x10,0x26,0x3c,0x37,0x99,0x37,0x32,0x33,0x38},
+            ""
+        },
+        0,
+        { /* input taproot seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            "",
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        1,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x03,0xd8,0x50,0x92,0xbb,0xe3,0x46,0x8f,0x68,0x4c,0xe1,0xd8,0xa2,0xa6,0x6e,0xbe,0xc9,0x6a,0x9e,0x6e,0x09,0xe7,0x11,0x07,0x20,0xa5,0xd5,0xfa,0xa4,0xaa,0x78,0x80,0xd0}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0x7e,0xfa,0x60,0xce,0x78,0xac,0x34,0x3d,0xf8,0xa0,0x13,0xa2,0x02,0x7c,0x6c,0x5e,0xf2,0x9f,0x95,0x02,0xed,0xcb,0xd7,0x69,0xd2,0xc2,0x17,0x17,0xfe,0xcc,0x59,0x51},
+            "",
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        1,
+        { /* outputs to scan */
+            {0x7e,0xfa,0x60,0xce,0x78,0xac,0x34,0x3d,0xf8,0xa0,0x13,0xa2,0x02,0x7c,0x6c,0x5e,0xf2,0x9f,0x95,0x02,0xed,0xcb,0xd7,0x69,0xd2,0xc2,0x17,0x17,0xfe,0xcc,0x59,0x51},
+            "",
+            "",
+            ""
+        },
+        3, {2, 3, 1001337, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        1,
+        {
+            {0x7e,0xfa,0x60,0xce,0x78,0xac,0x34,0x3d,0xf8,0xa0,0x13,0xa2,0x02,0x7c,0x6c,0x5e,0xf2,0x9f,0x95,0x02,0xed,0xcb,0xd7,0x69,0xd2,0xc2,0x17,0x17,0xfe,0xcc,0x59,0x51},
+            "",
+            "",
+            ""
+        },
+        {
+            {0xe3,0x36,0xb9,0x23,0x30,0xc3,0x30,0x30,0x28,0x5c,0xe4,0x2e,0x41,0x15,0xad,0x92,0xd5,0x19,0x79,0x13,0xc8,0x8e,0x06,0xb9,0x07,0x2b,0x4a,0x9b,0x47,0xc6,0x64,0xa2},
+            "",
+            "",
+            ""
+        },
+        {
+            {0xc9,0xe8,0x0d,0xd3,0xbd,0xd2,0x5c,0xa2,0xd3,0x52,0xce,0x77,0x51,0x0f,0x1a,0xed,0x37,0xba,0x35,0x09,0xdc,0x8c,0xc0,0x67,0x7f,0x2d,0x7c,0x2d,0xd0,0x40,0x90,0x70,0x79,0x50,0xce,0x9d,0xd6,0xc8,0x3d,0x2a,0x42,0x80,0x63,0x06,0x3a,0xff,0x5c,0x04,0xf1,0x74,0x4e,0x33,0x4f,0x66,0x1f,0x2f,0xc0,0x1b,0x4e,0xf8,0x0b,0x50,0xf7,0x39},
+            "",
+            "",
+            ""
+        }
+    },
+
+    /* ----- Multiple outputs with labels: multiple outputs for labeled address; same recipient (15) ----- */
+    {
+        2,
+        { /* input plain seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            {0x03,0x78,0xe9,0x56,0x85,0xb7,0x45,0x65,0xfa,0x56,0x75,0x1b,0x84,0xa3,0x2d,0xfd,0x18,0x54,0x5d,0x10,0xd6,0x91,0x64,0x1b,0x83,0x72,0xe3,0x21,0x64,0xfa,0xd6,0x6a},
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            {0x03,0x78,0x2e,0xeb,0x91,0x34,0x31,0xca,0x6e,0x9b,0x8c,0x2f,0xd8,0x0a,0x5f,0x72,0xed,0x20,0x24,0xef,0x72,0xa3,0xc6,0xfb,0x10,0x26,0x3c,0x37,0x99,0x37,0x32,0x33,0x38},
+            ""
+        },
+        0,
+        { /* input taproot seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            "",
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        2,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x03,0xa6,0x73,0x94,0x99,0xdc,0x66,0x7d,0x30,0x8b,0xae,0xfe,0xa4,0xde,0x0c,0x4a,0x85,0xcc,0x72,0xae,0xce,0x18,0x1b,0xc0,0x57,0x12,0xd3,0x91,0x96,0x62,0x61,0x0f,0xf1}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0xf2,0x07,0x16,0x2b,0x1a,0x7a,0xbc,0x51,0xc4,0x20,0x17,0xbe,0xf0,0x55,0xe9,0xec,0x1e,0xfc,0x3d,0x35,0x67,0xcb,0x72,0x03,0x57,0xe2,0xb8,0x43,0x25,0xdb,0x33,0xac},
+            {0x39,0xf4,0x26,0x24,0xd5,0xc3,0x2a,0x77,0xfd,0xa8,0x0f,0xf0,0xac,0xee,0x26,0x9a,0xfe,0xc6,0x01,0xd3,0x79,0x18,0x03,0xe8,0x02,0x52,0xae,0x04,0xe4,0xff,0xcf,0x4c},
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        2,
+        { /* outputs to scan */
+            {0xf2,0x07,0x16,0x2b,0x1a,0x7a,0xbc,0x51,0xc4,0x20,0x17,0xbe,0xf0,0x55,0xe9,0xec,0x1e,0xfc,0x3d,0x35,0x67,0xcb,0x72,0x03,0x57,0xe2,0xb8,0x43,0x25,0xdb,0x33,0xac},
+            {0x39,0xf4,0x26,0x24,0xd5,0xc3,0x2a,0x77,0xfd,0xa8,0x0f,0xf0,0xac,0xee,0x26,0x9a,0xfe,0xc6,0x01,0xd3,0x79,0x18,0x03,0xe8,0x02,0x52,0xae,0x04,0xe4,0xff,0xcf,0x4c},
+            "",
+            ""
+        },
+        1, {1, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        2,
+        {
+            {0xf2,0x07,0x16,0x2b,0x1a,0x7a,0xbc,0x51,0xc4,0x20,0x17,0xbe,0xf0,0x55,0xe9,0xec,0x1e,0xfc,0x3d,0x35,0x67,0xcb,0x72,0x03,0x57,0xe2,0xb8,0x43,0x25,0xdb,0x33,0xac},
+            {0x39,0xf4,0x26,0x24,0xd5,0xc3,0x2a,0x77,0xfd,0xa8,0x0f,0xf0,0xac,0xee,0x26,0x9a,0xfe,0xc6,0x01,0xd3,0x79,0x18,0x03,0xe8,0x02,0x52,0xae,0x04,0xe4,0xff,0xcf,0x4c},
+            "",
+            ""
+        },
+        {
+            {0x33,0xce,0x08,0x5c,0x3c,0x11,0xea,0xad,0x13,0x69,0x4a,0xae,0x3c,0x20,0x30,0x1a,0x6c,0x83,0x38,0x2e,0xc8,0x9a,0x7c,0xde,0x96,0xc6,0x79,0x9e,0x2f,0x88,0x80,0x5a},
+            {0x43,0x10,0x0f,0x89,0xf1,0xa6,0xbf,0x10,0x08,0x1c,0x92,0xb4,0x73,0xff,0xc5,0x7c,0xea,0xc7,0xdb,0xed,0x60,0x0b,0x6a,0xba,0x9b,0xb3,0x97,0x6f,0x17,0xdb,0xb9,0x14},
+            "",
+            ""
+        },
+        {
+            {0x33,0x56,0x67,0xca,0x6c,0xae,0x7a,0x26,0x43,0x8f,0x5c,0xfd,0xd7,0x3b,0x3d,0x48,0xfa,0x83,0x2f,0xa9,0x76,0x85,0x21,0xd7,0xd5,0x44,0x5f,0x22,0xc2,0x03,0xab,0x0d,0x74,0xed,0x85,0x08,0x8f,0x27,0xd2,0x99,0x59,0xba,0x62,0x7a,0x45,0x09,0x99,0x66,0x76,0xf4,0x7d,0xf8,0xff,0x28,0x4d,0x29,0x25,0x67,0xb1,0xbe,0xef,0x0e,0x39,0x12},
+            {0x15,0xc9,0x25,0x09,0xb6,0x7a,0x6c,0x21,0x1e,0xbb,0x4a,0x51,0xb7,0x52,0x8d,0x06,0x66,0xe6,0x72,0x0d,0xe2,0x34,0x3b,0x2e,0x92,0xcf,0xb9,0x79,0x42,0xca,0x14,0x69,0x3c,0x1f,0x1f,0xdc,0x84,0x51,0xac,0xfd,0xb2,0x64,0x40,0x39,0xf8,0xf5,0xc7,0x61,0x14,0x80,0x7f,0xdc,0x3d,0x3a,0x00,0x2d,0x8a,0x46,0xaf,0xab,0x67,0x56,0xbd,0x75},
+            "",
+            ""
+        }
+    },
+
+    /* ----- Multiple outputs with labels: multiple outputs for labeled address; same recipient (16) ----- */
+    {
+        2,
+        { /* input plain seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            {0x03,0x78,0xe9,0x56,0x85,0xb7,0x45,0x65,0xfa,0x56,0x75,0x1b,0x84,0xa3,0x2d,0xfd,0x18,0x54,0x5d,0x10,0xd6,0x91,0x64,0x1b,0x83,0x72,0xe3,0x21,0x64,0xfa,0xd6,0x6a},
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            {0x03,0x78,0x2e,0xeb,0x91,0x34,0x31,0xca,0x6e,0x9b,0x8c,0x2f,0xd8,0x0a,0x5f,0x72,0xed,0x20,0x24,0xef,0x72,0xa3,0xc6,0xfb,0x10,0x26,0x3c,0x37,0x99,0x37,0x32,0x33,0x38},
+            ""
+        },
+        0,
+        { /* input taproot seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            "",
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        2,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x03,0xa6,0x73,0x94,0x99,0xdc,0x66,0x7d,0x30,0x8b,0xae,0xfe,0xa4,0xde,0x0c,0x4a,0x85,0xcc,0x72,0xae,0xce,0x18,0x1b,0xc0,0x57,0x12,0xd3,0x91,0x96,0x62,0x61,0x0f,0xf1}
+            },
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x03,0xa6,0x73,0x94,0x99,0xdc,0x66,0x7d,0x30,0x8b,0xae,0xfe,0xa4,0xde,0x0c,0x4a,0x85,0xcc,0x72,0xae,0xce,0x18,0x1b,0xc0,0x57,0x12,0xd3,0x91,0x96,0x62,0x61,0x0f,0xf1}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0x83,0xdc,0x94,0x4e,0x61,0x60,0x31,0x37,0x29,0x48,0x29,0xae,0xd5,0x6c,0x74,0xc9,0xb0,0x87,0xd8,0x0f,0x2c,0x02,0x1b,0x98,0xa7,0xfa,0xe5,0x79,0x90,0x00,0x69,0x6c},
+            {0x39,0xf4,0x26,0x24,0xd5,0xc3,0x2a,0x77,0xfd,0xa8,0x0f,0xf0,0xac,0xee,0x26,0x9a,0xfe,0xc6,0x01,0xd3,0x79,0x18,0x03,0xe8,0x02,0x52,0xae,0x04,0xe4,0xff,0xcf,0x4c},
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        2,
+        { /* outputs to scan */
+            {0x83,0xdc,0x94,0x4e,0x61,0x60,0x31,0x37,0x29,0x48,0x29,0xae,0xd5,0x6c,0x74,0xc9,0xb0,0x87,0xd8,0x0f,0x2c,0x02,0x1b,0x98,0xa7,0xfa,0xe5,0x79,0x90,0x00,0x69,0x6c},
+            {0x39,0xf4,0x26,0x24,0xd5,0xc3,0x2a,0x77,0xfd,0xa8,0x0f,0xf0,0xac,0xee,0x26,0x9a,0xfe,0xc6,0x01,0xd3,0x79,0x18,0x03,0xe8,0x02,0x52,0xae,0x04,0xe4,0xff,0xcf,0x4c},
+            "",
+            ""
+        },
+        1, {1, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        2,
+        {
+            {0x83,0xdc,0x94,0x4e,0x61,0x60,0x31,0x37,0x29,0x48,0x29,0xae,0xd5,0x6c,0x74,0xc9,0xb0,0x87,0xd8,0x0f,0x2c,0x02,0x1b,0x98,0xa7,0xfa,0xe5,0x79,0x90,0x00,0x69,0x6c},
+            {0x39,0xf4,0x26,0x24,0xd5,0xc3,0x2a,0x77,0xfd,0xa8,0x0f,0xf0,0xac,0xee,0x26,0x9a,0xfe,0xc6,0x01,0xd3,0x79,0x18,0x03,0xe8,0x02,0x52,0xae,0x04,0xe4,0xff,0xcf,0x4c},
+            "",
+            ""
+        },
+        {
+            {0x9d,0x5f,0xd3,0xb9,0x1c,0xac,0x9d,0xdf,0xea,0x6f,0xc2,0xe6,0xf9,0x38,0x6f,0x68,0x0e,0x6c,0xee,0x62,0x3c,0xda,0x02,0xf5,0x37,0x06,0x30,0x6c,0x08,0x1d,0xe8,0x7f},
+            {0x43,0x10,0x0f,0x89,0xf1,0xa6,0xbf,0x10,0x08,0x1c,0x92,0xb4,0x73,0xff,0xc5,0x7c,0xea,0xc7,0xdb,0xed,0x60,0x0b,0x6a,0xba,0x9b,0xb3,0x97,0x6f,0x17,0xdb,0xb9,0x14},
+            "",
+            ""
+        },
+        {
+            {0xdb,0x0d,0xfa,0xcc,0x98,0xb6,0xa6,0xfc,0xc6,0x7c,0xc4,0x63,0x1f,0x08,0x0b,0x1c,0xa3,0x8c,0x60,0xd8,0xc3,0x97,0xf2,0xf1,0x98,0x43,0xf8,0xf9,0x5e,0xc9,0x15,0x94,0xb2,0x4e,0x47,0xc5,0xbd,0x39,0x48,0x0a,0x86,0x1c,0x12,0x09,0xf7,0xe3,0x14,0x5c,0x44,0x03,0x71,0xf9,0x19,0x1f,0xb9,0x6e,0x32,0x46,0x90,0x10,0x1e,0xac,0x8e,0x8e},
+            {0x15,0xc9,0x25,0x09,0xb6,0x7a,0x6c,0x21,0x1e,0xbb,0x4a,0x51,0xb7,0x52,0x8d,0x06,0x66,0xe6,0x72,0x0d,0xe2,0x34,0x3b,0x2e,0x92,0xcf,0xb9,0x79,0x42,0xca,0x14,0x69,0x3c,0x1f,0x1f,0xdc,0x84,0x51,0xac,0xfd,0xb2,0x64,0x40,0x39,0xf8,0xf5,0xc7,0x61,0x14,0x80,0x7f,0xdc,0x3d,0x3a,0x00,0x2d,0x8a,0x46,0xaf,0xab,0x67,0x56,0xbd,0x75},
+            "",
+            ""
+        }
+    },
+
+    /* ----- Multiple outputs with labels: multiple outputs for labeled address; same recipient (17) ----- */
+    {
+        2,
+        { /* input plain seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            {0x03,0x78,0xe9,0x56,0x85,0xb7,0x45,0x65,0xfa,0x56,0x75,0x1b,0x84,0xa3,0x2d,0xfd,0x18,0x54,0x5d,0x10,0xd6,0x91,0x64,0x1b,0x83,0x72,0xe3,0x21,0x64,0xfa,0xd6,0x6a},
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            {0x03,0x78,0x2e,0xeb,0x91,0x34,0x31,0xca,0x6e,0x9b,0x8c,0x2f,0xd8,0x0a,0x5f,0x72,0xed,0x20,0x24,0xef,0x72,0xa3,0xc6,0xfb,0x10,0x26,0x3c,0x37,0x99,0x37,0x32,0x33,0x38},
+            ""
+        },
+        0,
+        { /* input taproot seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            "",
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        4,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x03,0xa6,0x73,0x94,0x99,0xdc,0x66,0x7d,0x30,0x8b,0xae,0xfe,0xa4,0xde,0x0c,0x4a,0x85,0xcc,0x72,0xae,0xce,0x18,0x1b,0xc0,0x57,0x12,0xd3,0x91,0x96,0x62,0x61,0x0f,0xf1}
+            },
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x44,0xba,0xa5,0xcf,0x5d,0xb4,0x44,0xa9,0xe9,0x22,0x83,0x2f,0xf2,0xc8,0x87,0x16,0xb5,0x66,0xa8,0x5d,0x62,0xe8,0x23,0x5a,0xeb,0xd9,0x18,0x84,0xd4,0xf6,0x49,0x42}
+            },
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x44,0xba,0xa5,0xcf,0x5d,0xb4,0x44,0xa9,0xe9,0x22,0x83,0x2f,0xf2,0xc8,0x87,0x16,0xb5,0x66,0xa8,0x5d,0x62,0xe8,0x23,0x5a,0xeb,0xd9,0x18,0x84,0xd4,0xf6,0x49,0x42}
+            }
+        },
+        { /* recipient outputs */
+            {0xf2,0x07,0x16,0x2b,0x1a,0x7a,0xbc,0x51,0xc4,0x20,0x17,0xbe,0xf0,0x55,0xe9,0xec,0x1e,0xfc,0x3d,0x35,0x67,0xcb,0x72,0x03,0x57,0xe2,0xb8,0x43,0x25,0xdb,0x33,0xac},
+            {0x39,0xf4,0x26,0x24,0xd5,0xc3,0x2a,0x77,0xfd,0xa8,0x0f,0xf0,0xac,0xee,0x26,0x9a,0xfe,0xc6,0x01,0xd3,0x79,0x18,0x03,0xe8,0x02,0x52,0xae,0x04,0xe4,0xff,0xcf,0x4c},
+            {0xae,0x1a,0x78,0x0c,0x04,0x23,0x7b,0xd5,0x77,0x28,0x3c,0x3d,0xdb,0x2e,0x49,0x97,0x67,0xc3,0x21,0x41,0x60,0xd5,0xa6,0xb0,0x76,0x7e,0x6b,0x8c,0x27,0x8b,0xd7,0x01},
+            {0xf4,0x56,0x9f,0xc5,0xf6,0x9c,0x10,0xf0,0x08,0x2c,0xfb,0xb8,0xe0,0x72,0xe6,0x26,0x6e,0xc5,0x5f,0x69,0xfb,0xa8,0xcf,0xfc,0xa4,0xcb,0xb4,0xc1,0x44,0xb7,0xe5,0x9b}
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        4,
+        { /* outputs to scan */
+            {0xf2,0x07,0x16,0x2b,0x1a,0x7a,0xbc,0x51,0xc4,0x20,0x17,0xbe,0xf0,0x55,0xe9,0xec,0x1e,0xfc,0x3d,0x35,0x67,0xcb,0x72,0x03,0x57,0xe2,0xb8,0x43,0x25,0xdb,0x33,0xac},
+            {0x39,0xf4,0x26,0x24,0xd5,0xc3,0x2a,0x77,0xfd,0xa8,0x0f,0xf0,0xac,0xee,0x26,0x9a,0xfe,0xc6,0x01,0xd3,0x79,0x18,0x03,0xe8,0x02,0x52,0xae,0x04,0xe4,0xff,0xcf,0x4c},
+            {0xae,0x1a,0x78,0x0c,0x04,0x23,0x7b,0xd5,0x77,0x28,0x3c,0x3d,0xdb,0x2e,0x49,0x97,0x67,0xc3,0x21,0x41,0x60,0xd5,0xa6,0xb0,0x76,0x7e,0x6b,0x8c,0x27,0x8b,0xd7,0x01},
+            {0xf4,0x56,0x9f,0xc5,0xf6,0x9c,0x10,0xf0,0x08,0x2c,0xfb,0xb8,0xe0,0x72,0xe6,0x26,0x6e,0xc5,0x5f,0x69,0xfb,0xa8,0xcf,0xfc,0xa4,0xcb,0xb4,0xc1,0x44,0xb7,0xe5,0x9b}
+        },
+        1, {1, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        2,
+        {
+            {0xf2,0x07,0x16,0x2b,0x1a,0x7a,0xbc,0x51,0xc4,0x20,0x17,0xbe,0xf0,0x55,0xe9,0xec,0x1e,0xfc,0x3d,0x35,0x67,0xcb,0x72,0x03,0x57,0xe2,0xb8,0x43,0x25,0xdb,0x33,0xac},
+            {0x39,0xf4,0x26,0x24,0xd5,0xc3,0x2a,0x77,0xfd,0xa8,0x0f,0xf0,0xac,0xee,0x26,0x9a,0xfe,0xc6,0x01,0xd3,0x79,0x18,0x03,0xe8,0x02,0x52,0xae,0x04,0xe4,0xff,0xcf,0x4c},
+            "",
+            ""
+        },
+        {
+            {0x33,0xce,0x08,0x5c,0x3c,0x11,0xea,0xad,0x13,0x69,0x4a,0xae,0x3c,0x20,0x30,0x1a,0x6c,0x83,0x38,0x2e,0xc8,0x9a,0x7c,0xde,0x96,0xc6,0x79,0x9e,0x2f,0x88,0x80,0x5a},
+            {0x43,0x10,0x0f,0x89,0xf1,0xa6,0xbf,0x10,0x08,0x1c,0x92,0xb4,0x73,0xff,0xc5,0x7c,0xea,0xc7,0xdb,0xed,0x60,0x0b,0x6a,0xba,0x9b,0xb3,0x97,0x6f,0x17,0xdb,0xb9,0x14},
+            "",
+            ""
+        },
+        {
+            {0x33,0x56,0x67,0xca,0x6c,0xae,0x7a,0x26,0x43,0x8f,0x5c,0xfd,0xd7,0x3b,0x3d,0x48,0xfa,0x83,0x2f,0xa9,0x76,0x85,0x21,0xd7,0xd5,0x44,0x5f,0x22,0xc2,0x03,0xab,0x0d,0x74,0xed,0x85,0x08,0x8f,0x27,0xd2,0x99,0x59,0xba,0x62,0x7a,0x45,0x09,0x99,0x66,0x76,0xf4,0x7d,0xf8,0xff,0x28,0x4d,0x29,0x25,0x67,0xb1,0xbe,0xef,0x0e,0x39,0x12},
+            {0x15,0xc9,0x25,0x09,0xb6,0x7a,0x6c,0x21,0x1e,0xbb,0x4a,0x51,0xb7,0x52,0x8d,0x06,0x66,0xe6,0x72,0x0d,0xe2,0x34,0x3b,0x2e,0x92,0xcf,0xb9,0x79,0x42,0xca,0x14,0x69,0x3c,0x1f,0x1f,0xdc,0x84,0x51,0xac,0xfd,0xb2,0x64,0x40,0x39,0xf8,0xf5,0xc7,0x61,0x14,0x80,0x7f,0xdc,0x3d,0x3a,0x00,0x2d,0x8a,0x46,0xaf,0xab,0x67,0x56,0xbd,0x75},
+            "",
+            ""
+        }
+    },
+
+    /* ----- Single recipient: use silent payments for sender change (18) ----- */
+    {
+        2,
+        { /* input plain seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            {0x03,0x78,0xe9,0x56,0x85,0xb7,0x45,0x65,0xfa,0x56,0x75,0x1b,0x84,0xa3,0x2d,0xfd,0x18,0x54,0x5d,0x10,0xd6,0x91,0x64,0x1b,0x83,0x72,0xe3,0x21,0x64,0xfa,0xd6,0x6a},
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            {0x03,0x78,0x2e,0xeb,0x91,0x34,0x31,0xca,0x6e,0x9b,0x8c,0x2f,0xd8,0x0a,0x5f,0x72,0xed,0x20,0x24,0xef,0x72,0xa3,0xc6,0xfb,0x10,0x26,0x3c,0x37,0x99,0x37,0x32,0x33,0x38},
+            ""
+        },
+        0,
+        { /* input taproot seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            "",
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        2,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                {0x03,0xb4,0xcc,0x0b,0x09,0x0b,0x6f,0x49,0xa6,0x84,0x55,0x88,0x52,0xdb,0x60,0xee,0x5e,0xb1,0xc5,0xf7,0x43,0x52,0x83,0x9c,0x3d,0x18,0xa8,0xfc,0x04,0xef,0x73,0x54,0xe0},
+                {0x03,0xec,0xd4,0x3b,0x9f,0xda,0xd4,0x84,0xff,0x57,0x27,0x8b,0x21,0x87,0x8b,0x84,0x42,0x76,0xce,0x39,0x06,0x22,0xd0,0x3d,0xd0,0xcf,0xb4,0x28,0x8b,0x7e,0x02,0xa6,0xf5}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0xf2,0x07,0x16,0x2b,0x1a,0x7a,0xbc,0x51,0xc4,0x20,0x17,0xbe,0xf0,0x55,0xe9,0xec,0x1e,0xfc,0x3d,0x35,0x67,0xcb,0x72,0x03,0x57,0xe2,0xb8,0x43,0x25,0xdb,0x33,0xac},
+            {0xbe,0x36,0x8e,0x28,0x97,0x9d,0x95,0x02,0x45,0xd7,0x42,0x89,0x1a,0xe6,0x06,0x40,0x20,0xba,0x54,0x8c,0x1e,0x2e,0x65,0xa6,0x39,0xa8,0xbb,0x06,0x75,0xd9,0x5c,0xff},
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x11,0xb7,0xa8,0x2e,0x06,0xca,0x26,0x48,0xd5,0xfd,0xed,0x23,0x66,0x47,0x80,0x78,0xec,0x4f,0xc9,0xdc,0x1d,0x8f,0xf4,0x87,0x51,0x82,0x26,0xf2,0x29,0xd7,0x68,0xfd},
+        {0xb8,0xf8,0x73,0x88,0xcb,0xb4,0x19,0x34,0xc5,0x0d,0xac,0xa0,0x18,0x90,0x1b,0x00,0x07,0x0a,0x5f,0xf6,0xcc,0x25,0xa7,0xe9,0xe7,0x16,0xa9,0xd5,0xb9,0xe4,0xd6,0x64},
+        2,
+        { /* outputs to scan */
+            {0xf2,0x07,0x16,0x2b,0x1a,0x7a,0xbc,0x51,0xc4,0x20,0x17,0xbe,0xf0,0x55,0xe9,0xec,0x1e,0xfc,0x3d,0x35,0x67,0xcb,0x72,0x03,0x57,0xe2,0xb8,0x43,0x25,0xdb,0x33,0xac},
+            {0xbe,0x36,0x8e,0x28,0x97,0x9d,0x95,0x02,0x45,0xd7,0x42,0x89,0x1a,0xe6,0x06,0x40,0x20,0xba,0x54,0x8c,0x1e,0x2e,0x65,0xa6,0x39,0xa8,0xbb,0x06,0x75,0xd9,0x5c,0xff},
+            "",
+            ""
+        },
+        1, {0, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        1,
+        {
+            {0xbe,0x36,0x8e,0x28,0x97,0x9d,0x95,0x02,0x45,0xd7,0x42,0x89,0x1a,0xe6,0x06,0x40,0x20,0xba,0x54,0x8c,0x1e,0x2e,0x65,0xa6,0x39,0xa8,0xbb,0x06,0x75,0xd9,0x5c,0xff},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x80,0xcd,0x76,0x7e,0xd2,0x0b,0xd0,0xbb,0x7d,0x8e,0xa5,0xe8,0x03,0xf8,0xc3,0x81,0x29,0x3a,0x62,0xe8,0xa0,0x73,0xcf,0x46,0xfb,0x00,0x81,0xda,0x46,0xe6,0x4e,0x1f},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x7f,0xbd,0x50,0x74,0xcf,0x13,0x77,0x27,0x31,0x55,0xee,0xfa,0xfc,0x7c,0x33,0x0c,0xb6,0x1b,0x31,0xda,0x25,0x2f,0x22,0x20,0x6a,0xc2,0x75,0x30,0xd2,0xb2,0x56,0x70,0x40,0xd9,0xaf,0x78,0x08,0x34,0x2e,0xd4,0xa0,0x95,0x98,0xc2,0x6d,0x83,0x07,0x44,0x6e,0x4e,0xd7,0x70,0x79,0xe6,0xa2,0xe6,0x1f,0xea,0x73,0x6e,0x44,0xda,0x5f,0x5a},
+            "",
+            "",
+            ""
+        }
+    },
+
+    /* ----- Single receipient: taproot input with NUMS point (19) ----- */
+    {
+        0,
+        { /* input plain seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input plain pubkeys */
+            "",
+            "",
+            ""
+        },
+        1,
+        { /* input taproot seckeys */
+            {0xfc,0x87,0x16,0xa9,0x7a,0x48,0xba,0x9a,0x05,0xa9,0x8a,0xe4,0x7b,0x5c,0xd2,0x01,0xa2,0x5a,0x7f,0xd5,0xd8,0xb7,0x3c,0x20,0x3c,0x5f,0x7b,0x6b,0x6b,0x3b,0x6a,0xd7},
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            {0x78,0x2e,0xeb,0x91,0x34,0x31,0xca,0x6e,0x9b,0x8c,0x2f,0xd8,0x0a,0x5f,0x72,0xed,0x20,0x24,0xef,0x72,0xa3,0xc6,0xfb,0x10,0x26,0x3c,0x37,0x99,0x37,0x32,0x33,0x38},
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        1,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0x79,0xe7,0x98,0x97,0xc5,0x29,0x35,0xbf,0xd9,0x7f,0xc6,0xe0,0x76,0xa6,0x43,0x1a,0x0c,0x75,0x43,0xca,0x8c,0x31,0xe0,0xfc,0x3c,0xf7,0x19,0xbb,0x57,0x2c,0x84,0x2d},
+            "",
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        1,
+        { /* outputs to scan */
+            {0x79,0xe7,0x98,0x97,0xc5,0x29,0x35,0xbf,0xd9,0x7f,0xc6,0xe0,0x76,0xa6,0x43,0x1a,0x0c,0x75,0x43,0xca,0x8c,0x31,0xe0,0xfc,0x3c,0xf7,0x19,0xbb,0x57,0x2c,0x84,0x2d},
+            "",
+            "",
+            ""
+        },
+        0, {0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        1,
+        {
+            {0x79,0xe7,0x98,0x97,0xc5,0x29,0x35,0xbf,0xd9,0x7f,0xc6,0xe0,0x76,0xa6,0x43,0x1a,0x0c,0x75,0x43,0xca,0x8c,0x31,0xe0,0xfc,0x3c,0xf7,0x19,0xbb,0x57,0x2c,0x84,0x2d},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x3d,0xde,0xc3,0x23,0x26,0x09,0xd3,0x48,0xd6,0xb8,0xb5,0x31,0x23,0xb4,0xf4,0x0f,0x6d,0x4f,0x53,0x98,0xca,0x58,0x6f,0x08,0x7b,0x04,0x16,0xec,0x3b,0x85,0x14,0x96},
+            "",
+            "",
+            ""
+        },
+        {
+            {0xd7,0xd0,0x6e,0x3a,0xfb,0x68,0x36,0x30,0x31,0xe4,0xeb,0x18,0x03,0x5c,0x46,0xce,0xae,0x41,0xbd,0xbe,0xbe,0x78,0x88,0xa4,0x75,0x4b,0xc9,0x84,0x8c,0x59,0x64,0x36,0x86,0x9a,0xea,0xec,0xff,0x05,0x27,0x64,0x9a,0x1f,0x45,0x8b,0x71,0xc9,0xce,0xec,0xec,0x10,0xb5,0x35,0xc0,0x9d,0x01,0xd7,0x20,0x22,0x9a,0xa2,0x28,0x54,0x77,0x06},
+            "",
+            "",
+            ""
+        }
+    },
+
+    /* ----- Pubkey extraction from malleated p2pkh (20) ----- */
+    {
+        3,
+        { /* input plain seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            {0x03,0x78,0xe9,0x56,0x85,0xb7,0x45,0x65,0xfa,0x56,0x75,0x1b,0x84,0xa3,0x2d,0xfd,0x18,0x54,0x5d,0x10,0xd6,0x91,0x64,0x1b,0x83,0x72,0xe3,0x21,0x64,0xfa,0xd6,0x6a},
+            {0x72,0xb8,0xae,0x09,0x17,0x5c,0xa7,0x97,0x7f,0x04,0x99,0x3e,0x65,0x1d,0x88,0x68,0x1e,0xd9,0x32,0xdf,0xb9,0x2c,0x51,0x58,0xcd,0xf0,0x16,0x1d,0xd2,0x3f,0xda,0x6e}
+        },
+        { /* input plain pubkeys */
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            {0x03,0x78,0x2e,0xeb,0x91,0x34,0x31,0xca,0x6e,0x9b,0x8c,0x2f,0xd8,0x0a,0x5f,0x72,0xed,0x20,0x24,0xef,0x72,0xa3,0xc6,0xfb,0x10,0x26,0x3c,0x37,0x99,0x37,0x32,0x33,0x38},
+            {0x02,0xe0,0xec,0x4f,0x64,0xb3,0xfa,0x2e,0x46,0x3c,0xcf,0xcf,0x4e,0x85,0x6e,0x37,0xd5,0xe1,0xe2,0x02,0x75,0xbc,0x89,0xec,0x1d,0xef,0x9e,0xb0,0x98,0xef,0xf1,0xf8,0x5d}
+        },
+        0,
+        { /* input taproot seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            "",
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        1,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0x46,0x12,0xcd,0xbf,0x84,0x5c,0x66,0xc7,0x51,0x1d,0x70,0xaa,0xb4,0xd9,0xae,0xd1,0x1e,0x49,0xe4,0x8c,0xdb,0x8d,0x79,0x9d,0x78,0x71,0x01,0xcd,0xd0,0xd5,0x3e,0x4f},
+            "",
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        1,
+        { /* outputs to scan */
+            {0x46,0x12,0xcd,0xbf,0x84,0x5c,0x66,0xc7,0x51,0x1d,0x70,0xaa,0xb4,0xd9,0xae,0xd1,0x1e,0x49,0xe4,0x8c,0xdb,0x8d,0x79,0x9d,0x78,0x71,0x01,0xcd,0xd0,0xd5,0x3e,0x4f},
+            "",
+            "",
+            ""
+        },
+        0, {0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        1,
+        {
+            {0x46,0x12,0xcd,0xbf,0x84,0x5c,0x66,0xc7,0x51,0x1d,0x70,0xaa,0xb4,0xd9,0xae,0xd1,0x1e,0x49,0xe4,0x8c,0xdb,0x8d,0x79,0x9d,0x78,0x71,0x01,0xcd,0xd0,0xd5,0x3e,0x4f},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x10,0xbd,0xe9,0x78,0x1d,0xef,0x20,0xd7,0x70,0x1e,0x76,0x03,0xef,0x1b,0x1e,0x5e,0x71,0xc6,0x7b,0xae,0x71,0x54,0x81,0x88,0x14,0xe3,0xc8,0x1e,0xf5,0xb1,0xa3,0xd3},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x61,0x37,0x96,0x9f,0x81,0x0e,0x9e,0x8e,0xf6,0xc9,0x75,0x50,0x10,0xe8,0x08,0xf5,0xdd,0x1a,0xed,0x70,0x58,0x82,0xe4,0x4d,0x7f,0x0a,0xe6,0x4e,0xb0,0xc5,0x09,0xec,0x8b,0x62,0xa0,0x67,0x1b,0xee,0x0d,0x59,0x14,0xac,0x27,0xd2,0xc4,0x63,0x44,0x3e,0x28,0xe9,0x99,0xd8,0x2d,0xc3,0xd3,0xa4,0x91,0x9f,0x09,0x38,0x72,0xd9,0x47,0xbb},
+            "",
+            "",
+            ""
+        }
+    },
+
+    /* ----- P2PKH and P2WPKH Uncompressed Keys are skipped (21) ----- */
+    {
+        1,
+        { /* input plain seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            "",
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            "",
+            ""
+        },
+        0,
+        { /* input taproot seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            "",
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        1,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0x67,0xfe,0xe2,0x77,0xda,0x9e,0x85,0x42,0xb5,0xd2,0xe6,0xf3,0x2d,0x66,0x0a,0x9b,0xbd,0x3f,0x0e,0x10,0x7c,0x2d,0x53,0x63,0x8a,0xb1,0xd8,0x69,0x08,0x88,0x82,0xd6},
+            "",
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        1,
+        { /* outputs to scan */
+            {0x67,0xfe,0xe2,0x77,0xda,0x9e,0x85,0x42,0xb5,0xd2,0xe6,0xf3,0x2d,0x66,0x0a,0x9b,0xbd,0x3f,0x0e,0x10,0x7c,0x2d,0x53,0x63,0x8a,0xb1,0xd8,0x69,0x08,0x88,0x82,0xd6},
+            "",
+            "",
+            ""
+        },
+        0, {0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        1,
+        {
+            {0x67,0xfe,0xe2,0x77,0xda,0x9e,0x85,0x42,0xb5,0xd2,0xe6,0xf3,0x2d,0x66,0x0a,0x9b,0xbd,0x3f,0x0e,0x10,0x7c,0x2d,0x53,0x63,0x8a,0xb1,0xd8,0x69,0x08,0x88,0x82,0xd6},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x68,0x8f,0xa3,0xae,0xb9,0x7d,0x2a,0x46,0xae,0x87,0xb0,0x35,0x91,0x92,0x1c,0x2e,0xaf,0x4b,0x50,0x5e,0xb0,0xdd,0xca,0x27,0x33,0xc9,0x47,0x01,0xe0,0x10,0x60,0xcf},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x72,0xe7,0xad,0x57,0x3a,0xc2,0x32,0x55,0xd4,0x65,0x1d,0x5b,0x03,0x26,0xa2,0x00,0x49,0x65,0x88,0xac,0xb7,0xa4,0x89,0x4b,0x22,0x09,0x22,0x36,0xd5,0xed,0xa6,0xa0,0xa9,0xa4,0xd8,0x42,0x9b,0x02,0x2c,0x22,0x19,0x08,0x1f,0xef,0xce,0x5b,0x33,0x79,0x5c,0xae,0x48,0x8d,0x10,0xf5,0xea,0x94,0x38,0x84,0x9e,0xd8,0x35,0x36,0x24,0xf2},
+            "",
+            "",
+            ""
+        }
+    },
+
+    /* ----- Skip invalid P2SH inputs (22) ----- */
+    {
+        1,
+        { /* input plain seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            "",
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x02,0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            "",
+            ""
+        },
+        0,
+        { /* input taproot seckeys */
+            "",
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            "",
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        1,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0x67,0xfe,0xe2,0x77,0xda,0x9e,0x85,0x42,0xb5,0xd2,0xe6,0xf3,0x2d,0x66,0x0a,0x9b,0xbd,0x3f,0x0e,0x10,0x7c,0x2d,0x53,0x63,0x8a,0xb1,0xd8,0x69,0x08,0x88,0x82,0xd6},
+            "",
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        1,
+        { /* outputs to scan */
+            {0x67,0xfe,0xe2,0x77,0xda,0x9e,0x85,0x42,0xb5,0xd2,0xe6,0xf3,0x2d,0x66,0x0a,0x9b,0xbd,0x3f,0x0e,0x10,0x7c,0x2d,0x53,0x63,0x8a,0xb1,0xd8,0x69,0x08,0x88,0x82,0xd6},
+            "",
+            "",
+            ""
+        },
+        0, {0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        1,
+        {
+            {0x67,0xfe,0xe2,0x77,0xda,0x9e,0x85,0x42,0xb5,0xd2,0xe6,0xf3,0x2d,0x66,0x0a,0x9b,0xbd,0x3f,0x0e,0x10,0x7c,0x2d,0x53,0x63,0x8a,0xb1,0xd8,0x69,0x08,0x88,0x82,0xd6},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x68,0x8f,0xa3,0xae,0xb9,0x7d,0x2a,0x46,0xae,0x87,0xb0,0x35,0x91,0x92,0x1c,0x2e,0xaf,0x4b,0x50,0x5e,0xb0,0xdd,0xca,0x27,0x33,0xc9,0x47,0x01,0xe0,0x10,0x60,0xcf},
+            "",
+            "",
+            ""
+        },
+        {
+            {0x72,0xe7,0xad,0x57,0x3a,0xc2,0x32,0x55,0xd4,0x65,0x1d,0x5b,0x03,0x26,0xa2,0x00,0x49,0x65,0x88,0xac,0xb7,0xa4,0x89,0x4b,0x22,0x09,0x22,0x36,0xd5,0xed,0xa6,0xa0,0xa9,0xa4,0xd8,0x42,0x9b,0x02,0x2c,0x22,0x19,0x08,0x1f,0xef,0xce,0x5b,0x33,0x79,0x5c,0xae,0x48,0x8d,0x10,0xf5,0xea,0x94,0x38,0x84,0x9e,0xd8,0x35,0x36,0x24,0xf2},
+            "",
+            "",
+            ""
+        }
+    },
+
+    /* ----- Recipient ignores unrelated outputs (23) ----- */
+    {
+        1,
+        { /* input plain seckeys */
+            {0x03,0x78,0xe9,0x56,0x85,0xb7,0x45,0x65,0xfa,0x56,0x75,0x1b,0x84,0xa3,0x2d,0xfd,0x18,0x54,0x5d,0x10,0xd6,0x91,0x64,0x1b,0x83,0x72,0xe3,0x21,0x64,0xfa,0xd6,0x6a},
+            "",
+            ""
+        },
+        { /* input plain pubkeys */
+            {0x03,0x78,0x2e,0xeb,0x91,0x34,0x31,0xca,0x6e,0x9b,0x8c,0x2f,0xd8,0x0a,0x5f,0x72,0xed,0x20,0x24,0xef,0x72,0xa3,0xc6,0xfb,0x10,0x26,0x3c,0x37,0x99,0x37,0x32,0x33,0x38},
+            "",
+            ""
+        },
+        1,
+        { /* input taproot seckeys */
+            {0xea,0xdc,0x78,0x16,0x5f,0xf1,0xf8,0xea,0x94,0xad,0x7c,0xfd,0xc5,0x49,0x90,0x73,0x8a,0x4c,0x53,0xf6,0xe0,0x50,0x7b,0x42,0x15,0x42,0x01,0xb8,0xe5,0xdf,0xf3,0xb1},
+            "",
+            ""
+        },
+        { /* input x-only pubkeys */
+            {0x5a,0x1e,0x61,0xf8,0x98,0x17,0x30,0x40,0xe2,0x06,0x16,0xd4,0x3e,0x9f,0x49,0x6f,0xba,0x90,0x33,0x8a,0x39,0xfa,0xa1,0xed,0x98,0xfc,0xba,0xee,0xe4,0xdd,0x9b,0xe5},
+            "",
+            ""
+        },
+        /* smallest outpoint */
+        {0x16,0x9e,0x1e,0x83,0xe9,0x30,0x85,0x33,0x91,0xbc,0x6f,0x35,0xf6,0x05,0xc6,0x75,0x4c,0xfe,0xad,0x57,0xcf,0x83,0x87,0x63,0x9d,0x3b,0x40,0x96,0xc5,0x4f,0x18,0xf4,0x00,0x00,0x00,0x00},
+        1,
+        { /* recipient pubkeys (address data) */
+            {
+                {0x02,0x20,0xbc,0xfa,0xc5,0xb9,0x9e,0x04,0xad,0x1a,0x06,0xdd,0xfb,0x01,0x6e,0xe1,0x35,0x82,0x60,0x9d,0x60,0xb6,0x29,0x1e,0x98,0xd0,0x1a,0x9b,0xc9,0xa1,0x6c,0x96,0xd4},
+                {0x02,0x5c,0xc9,0x85,0x6d,0x6f,0x83,0x75,0x35,0x0e,0x12,0x39,0x78,0xda,0xac,0x20,0x0c,0x26,0x0c,0xb5,0xb5,0xae,0x83,0x10,0x6c,0xab,0x90,0x48,0x4d,0xcd,0x8f,0xcf,0x36}
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            },
+            {
+                "",
+                ""
+            }
+        },
+        { /* recipient outputs */
+            {0xf2,0x07,0x16,0x2b,0x1a,0x7a,0xbc,0x51,0xc4,0x20,0x17,0xbe,0xf0,0x55,0xe9,0xec,0x1e,0xfc,0x3d,0x35,0x67,0xcb,0x72,0x03,0x57,0xe2,0xb8,0x43,0x25,0xdb,0x33,0xac},
+            "",
+            "",
+            ""
+        },
+        /* receiver data (scan and spend seckeys) */
+        {0x0f,0x69,0x4e,0x06,0x80,0x28,0xa7,0x17,0xf8,0xaf,0x6b,0x94,0x11,0xf9,0xa1,0x33,0xdd,0x35,0x65,0x25,0x87,0x14,0xcc,0x22,0x65,0x94,0xb3,0x4d,0xb9,0x0c,0x1f,0x2c},
+        {0x9d,0x6a,0xd8,0x55,0xce,0x34,0x17,0xef,0x84,0xe8,0x36,0x89,0x2e,0x5a,0x56,0x39,0x2b,0xfb,0xa0,0x5f,0xa5,0xd9,0x7c,0xce,0xa3,0x0e,0x26,0x6f,0x54,0x0e,0x08,0xb3},
+        2,
+        { /* outputs to scan */
+            {0x78,0x2e,0xeb,0x91,0x34,0x31,0xca,0x6e,0x9b,0x8c,0x2f,0xd8,0x0a,0x5f,0x72,0xed,0x20,0x24,0xef,0x72,0xa3,0xc6,0xfb,0x10,0x26,0x3c,0x37,0x99,0x37,0x32,0x33,0x38},
+            {0x84,0x17,0x92,0xc3,0x3c,0x9d,0xc6,0x19,0x3e,0x76,0x74,0x41,0x34,0x12,0x5d,0x40,0xad,0xd8,0xf2,0xf4,0xa9,0x64,0x75,0xf2,0x8b,0xa1,0x50,0xbe,0x03,0x2d,0x64,0xe8},
+            "",
+            ""
+        },
+        0, {0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}, /* labels */
+        /* expected output data (pubkeys and seckey tweaks) */
+        0,
+        {
+            "",
+            "",
+            "",
+            ""
+        },
+        {
+            "",
+            "",
+            "",
+            ""
+        },
+        {
+            "",
+            "",
+            "",
+            ""
+        }
+    },
+
+};
diff --git a/src/tests.c b/src/tests.c
index e653aeea58..bb74513857 100644
--- a/src/tests.c
+++ b/src/tests.c
@@ -7285,6 +7285,10 @@ static void run_ecdsa_wycheproof(void) {
 # include "modules/ellswift/tests_impl.h"
 #endif
 
+#ifdef ENABLE_MODULE_SILENTPAYMENTS
+# include "modules/silentpayments/tests_impl.h"
+#endif
+
 static void run_secp256k1_memczero_test(void) {
     unsigned char buf1[6] = {1, 2, 3, 4, 5, 6};
     unsigned char buf2[sizeof(buf1)];
@@ -7633,6 +7637,10 @@ int main(int argc, char **argv) {
     run_ellswift_tests();
 #endif
 
+#ifdef ENABLE_MODULE_SILENTPAYMENTS
+    run_silentpayments_tests();
+#endif
+
     /* util tests */
     run_secp256k1_memczero_test();
     run_secp256k1_byteorder_tests();
diff --git a/tools/bech32m.py b/tools/bech32m.py
new file mode 100644
index 0000000000..795e153863
--- /dev/null
+++ b/tools/bech32m.py
@@ -0,0 +1,135 @@
+# Copyright (c) 2017, 2020 Pieter Wuille
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""Reference implementation for Bech32/Bech32m and segwit addresses."""
+
+
+from enum import Enum
+
+class Encoding(Enum):
+    """Enumeration type to list the various supported encodings."""
+    BECH32 = 1
+    BECH32M = 2
+
+CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
+BECH32M_CONST = 0x2bc830a3
+
+def bech32_polymod(values):
+    """Internal function that computes the Bech32 checksum."""
+    generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
+    chk = 1
+    for value in values:
+        top = chk >> 25
+        chk = (chk & 0x1ffffff) << 5 ^ value
+        for i in range(5):
+            chk ^= generator[i] if ((top >> i) & 1) else 0
+    return chk
+
+
+def bech32_hrp_expand(hrp):
+    """Expand the HRP into values for checksum computation."""
+    return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
+
+
+def bech32_verify_checksum(hrp, data):
+    """Verify a checksum given HRP and converted data characters."""
+    const = bech32_polymod(bech32_hrp_expand(hrp) + data)
+    if const == 1:
+        return Encoding.BECH32
+    if const == BECH32M_CONST:
+        return Encoding.BECH32M
+    return None
+
+def bech32_create_checksum(hrp, data, spec):
+    """Compute the checksum values given HRP and data."""
+    values = bech32_hrp_expand(hrp) + data
+    const = BECH32M_CONST if spec == Encoding.BECH32M else 1
+    polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const
+    return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
+
+
+def bech32_encode(hrp, data, spec):
+    """Compute a Bech32 string given HRP and data values."""
+    combined = data + bech32_create_checksum(hrp, data, spec)
+    return hrp + '1' + ''.join([CHARSET[d] for d in combined])
+
+def bech32_decode(bech):
+    """Validate a Bech32/Bech32m string, and determine HRP and data."""
+    if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
+            (bech.lower() != bech and bech.upper() != bech)):
+        return (None, None, None)
+    bech = bech.lower()
+    pos = bech.rfind('1')
+
+    # remove the requirement that bech32m be less than 90 chars
+    if pos < 1 or pos + 7 > len(bech):
+        return (None, None, None)
+    if not all(x in CHARSET for x in bech[pos+1:]):
+        return (None, None, None)
+    hrp = bech[:pos]
+    data = [CHARSET.find(x) for x in bech[pos+1:]]
+    spec = bech32_verify_checksum(hrp, data)
+    if spec is None:
+        return (None, None, None)
+    return (hrp, data[:-6], spec)
+
+def convertbits(data, frombits, tobits, pad=True):
+    """General power-of-2 base conversion."""
+    acc = 0
+    bits = 0
+    ret = []
+    maxv = (1 << tobits) - 1
+    max_acc = (1 << (frombits + tobits - 1)) - 1
+    for value in data:
+        if value < 0 or (value >> frombits):
+            return None
+        acc = ((acc << frombits) | value) & max_acc
+        bits += frombits
+        while bits >= tobits:
+            bits -= tobits
+            ret.append((acc >> bits) & maxv)
+    if pad:
+        if bits:
+            ret.append((acc << (tobits - bits)) & maxv)
+    elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
+        return None
+    return ret
+
+
+def decode(hrp, addr):
+    """Decode a segwit address."""
+    hrpgot, data, spec = bech32_decode(addr)
+    if hrpgot != hrp:
+        return (None, None)
+    decoded = convertbits(data[1:], 5, 8, False)
+    if decoded is None or len(decoded) < 2:
+        return (None, None)
+    if data[0] > 16:
+        return (None, None)
+    return (data[0], decoded)
+
+
+def encode(hrp, witver, witprog):
+    """Encode a segwit address."""
+    spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M
+    ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec)
+    if decode(hrp, ret) == (None, None):
+        return None
+    return ret
diff --git a/tools/ripemd160.py b/tools/ripemd160.py
new file mode 100644
index 0000000000..12801364b4
--- /dev/null
+++ b/tools/ripemd160.py
@@ -0,0 +1,130 @@
+# Copyright (c) 2021 Pieter Wuille
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""Test-only pure Python RIPEMD160 implementation."""
+
+import unittest
+
+# Message schedule indexes for the left path.
+ML = [
+    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+    7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8,
+    3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,
+    1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2,
+    4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13
+]
+
+# Message schedule indexes for the right path.
+MR = [
+    5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12,
+    6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2,
+    15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,
+    8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14,
+    12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11
+]
+
+# Rotation counts for the left path.
+RL = [
+    11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8,
+    7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12,
+    11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5,
+    11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,
+    9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6
+]
+
+# Rotation counts for the right path.
+RR = [
+    8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6,
+    9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11,
+    9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5,
+    15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8,
+    8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11
+]
+
+# K constants for the left path.
+KL = [0, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e]
+
+# K constants for the right path.
+KR = [0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0]
+
+
+def fi(x, y, z, i):
+    """The f1, f2, f3, f4, and f5 functions from the specification."""
+    if i == 0:
+        return x ^ y ^ z
+    elif i == 1:
+        return (x & y) | (~x & z)
+    elif i == 2:
+        return (x | ~y) ^ z
+    elif i == 3:
+        return (x & z) | (y & ~z)
+    elif i == 4:
+        return x ^ (y | ~z)
+    else:
+        assert False
+
+
+def rol(x, i):
+    """Rotate the bottom 32 bits of x left by i bits."""
+    return ((x << i) | ((x & 0xffffffff) >> (32 - i))) & 0xffffffff
+
+
+def compress(h0, h1, h2, h3, h4, block):
+    """Compress state (h0, h1, h2, h3, h4) with block."""
+    # Left path variables.
+    al, bl, cl, dl, el = h0, h1, h2, h3, h4
+    # Right path variables.
+    ar, br, cr, dr, er = h0, h1, h2, h3, h4
+    # Message variables.
+    x = [int.from_bytes(block[4*i:4*(i+1)], 'little') for i in range(16)]
+
+    # Iterate over the 80 rounds of the compression.
+    for j in range(80):
+        rnd = j >> 4
+        # Perform left side of the transformation.
+        al = rol(al + fi(bl, cl, dl, rnd) + x[ML[j]] + KL[rnd], RL[j]) + el
+        al, bl, cl, dl, el = el, al, bl, rol(cl, 10), dl
+        # Perform right side of the transformation.
+        ar = rol(ar + fi(br, cr, dr, 4 - rnd) + x[MR[j]] + KR[rnd], RR[j]) + er
+        ar, br, cr, dr, er = er, ar, br, rol(cr, 10), dr
+
+    # Compose old state, left transform, and right transform into new state.
+    return h1 + cl + dr, h2 + dl + er, h3 + el + ar, h4 + al + br, h0 + bl + cr
+
+
+def ripemd160(data):
+    """Compute the RIPEMD-160 hash of data."""
+    # Initialize state.
+    state = (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0)
+    # Process full 64-byte blocks in the input.
+    for b in range(len(data) >> 6):
+        state = compress(*state, data[64*b:64*(b+1)])
+    # Construct final blocks (with padding and size).
+    pad = b"\x80" + b"\x00" * ((119 - len(data)) & 63)
+    fin = data[len(data) & ~63:] + pad + (8 * len(data)).to_bytes(8, 'little')
+    # Process final blocks.
+    for b in range(len(fin) >> 6):
+        state = compress(*state, fin[64*b:64*(b+1)])
+    # Produce output.
+    return b"".join((h & 0xffffffff).to_bytes(4, 'little') for h in state)
+
+
+class TestFrameworkKey(unittest.TestCase):
+    def test_ripemd160(self):
+        """RIPEMD-160 test vectors."""
+        # See https://homes.esat.kuleuven.be/~bosselae/ripemd160.html
+        for msg, hexout in [
+            (b"", "9c1185a5c5e9fc54612808977ee8f548b2258d31"),
+            (b"a", "0bdc9d2d256b3ee9daae347be6f4dc835a467ffe"),
+            (b"abc", "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc"),
+            (b"message digest", "5d0689ef49d2fae572b881b123a85ffa21595f36"),
+            (b"abcdefghijklmnopqrstuvwxyz",
+                "f71c27109c692c1b56bbdceb5b9d2865b3708dbc"),
+            (b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+                "12a053384a9c0c88e405a06c27dcf49ada62eb2b"),
+            (b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
+                "b0e20b6e3116640286ed3a87a5713079b21f5189"),
+            (b"1234567890" * 8, "9b752e45573d4b39f4dbd3323cab82bf63326bfb"),
+            (b"a" * 1000000, "52783243c1697bdbe16d37f97f68f08325dc1528")
+        ]:
+            self.assertEqual(ripemd160(msg).hex(), hexout)
diff --git a/tools/tests_silentpayments_generate.py b/tools/tests_silentpayments_generate.py
new file mode 100755
index 0000000000..527189761a
--- /dev/null
+++ b/tools/tests_silentpayments_generate.py
@@ -0,0 +1,289 @@
+#!/usr/bin/env python3
+import hashlib
+import json
+import sys
+
+import bech32m
+import ripemd160
+
+NUMS_H = bytes.fromhex("50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0")
+
+def sha256(s):
+    return hashlib.sha256(s).digest()
+
+def hash160(s):
+    return ripemd160.ripemd160(sha256(s))
+
+def smallest_outpoint(outpoints):
+    serialized_outpoints = [bytes.fromhex(txid)[::-1] + n.to_bytes(4, 'little') for txid, n in outpoints]
+    return sorted(serialized_outpoints)[0]
+
+def decode_silent_payments_address(address):
+    _, data = bech32m.decode("sp", address)
+    data = bytes(data)  # convert from list to bytes
+    assert len(data) == 66
+    return data[:33], data[33:]
+
+def is_p2tr(s):  # OP_1 OP_PUSHBYTES_32 <32 bytes>
+    return (len(s) == 34) and (s[0] == 0x51) and (s[1] == 0x20)
+
+def is_p2wpkh(s):  # OP_0 OP_PUSHBYTES_20 <20 bytes>
+    return (len(s) == 22) and (s[0] == 0x00) and (s[1] == 0x14)
+
+def is_p2sh(s):  # OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUAL
+    return (len(s) == 23) and (s[0] == 0xA9) and (s[1] == 0x14) and (s[-1] == 0x87)
+
+def is_p2pkh(s):  # OP_DUP OP_HASH160 OP_PUSHBYTES_20 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
+    return (len(s) == 25) and (s[0] == 0x76) and (s[1] == 0xA9) and (s[2] == 0x14) and \
+        (s[-2] == 0x88) and (s[-1] == 0xAC)
+
+def get_pubkey_from_input(spk, script_sig, witness):
+    # build witness stack from raw witness data
+    witness_stack = []
+    no_witness_items = 0
+    if len(witness) > 0:
+        no_witness_items = witness[0]
+        witness = witness[1:]
+    for i in range(no_witness_items):
+        item_len = witness[0]
+        witness_stack.append(witness[1:item_len+1])
+        witness = witness[item_len+1:]
+
+    if is_p2pkh(spk):
+        spk_pkh = spk[3:3 + 20]
+        for i in range(len(script_sig), 0, -1):
+            if i - 33 >= 0:
+                pk = script_sig[i - 33:i]
+                if hash160(pk) == spk_pkh:
+                    return pk
+    elif is_p2sh(spk) and is_p2wpkh(script_sig[1:]):
+        pubkey = witness_stack[-1]
+        if len(pubkey) == 33:
+            return pubkey
+    elif is_p2wpkh(spk):
+        # the witness must contain two items and the second item is the pubkey
+        pubkey = witness_stack[-1]
+        if len(pubkey) == 33:
+            return pubkey
+    elif is_p2tr(spk):
+        if len(witness_stack) > 1 and witness_stack[-1][0] == 0x50:
+            witness_stack.pop()
+        if len(witness_stack) > 1:  # script-path spend?
+            control_block = witness_stack[-1]
+            internal_key = control_block[1:33]
+            if internal_key == NUMS_H:  # skip
+                return b''
+        return spk[2:]
+
+    return b''
+
+def to_c_array(x):
+    if x == "":
+        return ""
+    s = ',0x'.join(a+b for a,b in zip(x[::2], x[1::2]))
+    return "0x" + s
+
+def emit_key_material(comment, keys, include_count=False):
+    global out
+    if include_count:
+        out += f"        {len(keys)}," + "\n"
+    out += f"        {{ /* {comment} */" + "\n"
+    for i in range(3):
+        out += "            "
+        if i < len(keys):
+            out += "{"
+            out += to_c_array(keys[i])
+            out += "}"
+        else:
+            out += '""'
+        if i != 2:
+            out += ','
+        out += "\n"
+    out +=  "        },\n"
+
+def emit_receiver_addr_material(receiver_pubkeys):
+    global out
+    out += f"        {len(receiver_pubkeys)}," + "\n"
+    out +=  "        { /* recipient pubkeys (address data) */\n"
+    for i in range(4):
+        out += "            {\n"
+        if i < len(receiver_pubkeys):
+            out += "                {"
+            out += to_c_array(receiver_pubkeys[i][0])
+            out += "},\n"
+            out += "                {"
+            out += to_c_array(receiver_pubkeys[i][1])
+            out += "}\n"
+        else:
+            out += '                "",\n'
+            out += '                ""\n'
+        out += "            }"
+        if i != 3:
+            out += ','
+        out += "\n"
+    out += "        },\n"
+
+def emit_outputs(comment, outputs, include_count=False, last=False):
+    global out
+    if include_count:
+        out += f"        {len(outputs)}," + "\n"
+    if comment:
+        out += f"        {{ /* {comment} */" + "\n"
+    else:
+        out +=  "        {\n"
+    for i in range(4):
+        if i < len(outputs):
+            out += "            {"
+            out += to_c_array(outputs[i])
+            out += "}"
+        else:
+            out += '            ""'
+        if i != 3:
+            out += ','
+        out += "\n"
+    out += "        }"
+    if not last:
+        out += ","
+    out += "\n"
+
+filename_input = sys.argv[1]
+with open(filename_input, 'rb') as f:
+    hash_calculated = sha256(f.read()).hex()
+    if hash_calculated != "d69df3ff4e79afc6bbfc79ee1dc0415b7fa3455508ed39f2d1f2a6f316d4b4d8":
+        print("Error: input file doesn't match hash from BIP352", file=sys.stderr)
+        sys.exit(1)
+
+with open(filename_input) as f:
+    test_vectors = json.load(f)
+
+out = ""
+num_vectors = 0
+
+for test_nr, test_vector in enumerate(test_vectors):
+    # determine input private and public keys, grouped into plain and taproot/x-only
+    input_plain_seckeys = []
+    input_taproot_seckeys = []
+    input_plain_pubkeys = []
+    input_xonly_pubkeys = []
+    outpoints = []
+    for i in test_vector['sending'][0]['given']['vin']:
+        pub_key = get_pubkey_from_input(bytes.fromhex(i['prevout']['scriptPubKey']['hex']),
+            bytes.fromhex(i['scriptSig']), bytes.fromhex(i['txinwitness']))
+        if len(pub_key) == 33:  # regular input
+            input_plain_seckeys.append(i['private_key'])
+            input_plain_pubkeys.append(pub_key.hex())
+        elif len(pub_key) == 32:  # taproot input
+            input_taproot_seckeys.append(i['private_key'])
+            input_xonly_pubkeys.append(pub_key.hex())
+        outpoints.append((i['txid'], i['vout']))
+    if len(input_plain_pubkeys) == 0 and len(input_xonly_pubkeys) == 0:
+        continue
+
+    num_vectors += 1
+    out += f"    /* ----- {test_vector['comment']} ({num_vectors}) ----- */\n"
+    out +=  "    {\n"
+
+    outpoint_L = smallest_outpoint(outpoints).hex()
+    emit_key_material("input plain seckeys", input_plain_seckeys, include_count=True)
+    emit_key_material("input plain pubkeys", input_plain_pubkeys)
+    emit_key_material("input taproot seckeys", input_taproot_seckeys, include_count=True)
+    emit_key_material("input x-only pubkeys", input_xonly_pubkeys)
+    out += "        /* smallest outpoint */\n"
+    out += "        {"
+    out += to_c_array(outpoint_L)
+    out += "},\n"
+
+    # emit recipient pubkeys (address data)
+    recipient_pubkeys = []
+    for recipient_address, recipient_value in test_vector['sending'][0]['given']['recipients']:
+        recipient_B_scan, recipient_B_spend = decode_silent_payments_address(recipient_address)
+        recipient_pubkeys.append((recipient_B_scan.hex(), recipient_B_spend.hex()))
+    emit_receiver_addr_material(recipient_pubkeys)
+
+    # emit recipient outputs
+    emit_outputs("recipient outputs", [o[0] for o in test_vector['sending'][0]['expected']['outputs']])
+
+    # emit receiver scan/spend seckeys
+    recv_test_given = test_vector['receiving'][0]['given']
+    recv_test_expected = test_vector['receiving'][0]['expected']
+    out += "        /* receiver data (scan and spend seckeys) */\n"
+    out += "        {" + f"{to_c_array(recv_test_given['key_material']['scan_priv_key'])}" + "},\n"
+    out += "        {" + f"{to_c_array(recv_test_given['key_material']['spend_priv_key'])}" + "},\n"
+
+    # emit receiver to-scan outputs, labels and expected-found outputs
+    emit_outputs("outputs to scan", recv_test_given['outputs'], include_count=True)
+    labels = recv_test_given['labels']
+    out += f"        {len(labels)}, " + "{"
+    for i in range(4):
+        if i < len(labels):
+            out += f"{labels[i]}"
+        else:
+            out += "0xffffffff"
+        if i != 3:
+            out += ", "
+    out += "}, /* labels */\n"
+    expected_pubkeys = [o['pub_key'] for o in recv_test_expected['outputs']]
+    expected_tweaks = [o['priv_key_tweak'] for o in recv_test_expected['outputs']]
+    expected_signatures = [o['signature'] for o in recv_test_expected['outputs']]
+    out += "        /* expected output data (pubkeys and seckey tweaks) */\n"
+    emit_outputs("", expected_pubkeys, include_count=True)
+    emit_outputs("", expected_tweaks)
+    emit_outputs("", expected_signatures, last=True)
+
+    out += "    }"
+    if test_nr != len(test_vectors)-1:
+        out += ","
+    out += "\n\n"
+
+STRUCT_DEFINITIONS = """
+#define MAX_INPUTS_PER_TEST_CASE  3
+#define MAX_OUTPUTS_PER_TEST_CASE 4
+
+struct bip352_receiver_addressdata {
+    unsigned char scan_pubkey[33];
+    unsigned char spend_pubkey[33];
+};
+
+struct bip352_test_vector {
+    /* Inputs (private keys / public keys + smallest outpoint) */
+    size_t num_plain_inputs;
+    unsigned char plain_seckeys[MAX_INPUTS_PER_TEST_CASE][32];
+    unsigned char plain_pubkeys[MAX_INPUTS_PER_TEST_CASE][33];
+
+    size_t num_taproot_inputs;
+    unsigned char taproot_seckeys[MAX_INPUTS_PER_TEST_CASE][32];
+    unsigned char xonly_pubkeys[MAX_INPUTS_PER_TEST_CASE][32];
+
+    unsigned char outpoint_smallest[36];
+
+    /* Given sender data (pubkeys encoded per output address to send to) */
+    size_t num_recipient_outputs;
+    struct bip352_receiver_addressdata receiver_pubkeys[MAX_OUTPUTS_PER_TEST_CASE];
+
+    /* Expected sender data */
+    unsigned char recipient_outputs[MAX_OUTPUTS_PER_TEST_CASE][32];
+
+    /* Given receiver data */
+    unsigned char scan_seckey[32];
+    unsigned char spend_seckey[32];
+    size_t num_to_scan_outputs;
+    unsigned char to_scan_outputs[MAX_OUTPUTS_PER_TEST_CASE][32];
+    size_t num_labels;
+    unsigned int label_integers[MAX_OUTPUTS_PER_TEST_CASE];
+
+    /* Expected receiver data */
+    size_t num_found_outputs;
+    unsigned char found_output_pubkeys[MAX_OUTPUTS_PER_TEST_CASE][32];
+    unsigned char found_seckey_tweaks[MAX_OUTPUTS_PER_TEST_CASE][32];
+    unsigned char found_signatures[MAX_OUTPUTS_PER_TEST_CASE][64];
+};
+"""
+
+print("/* Note: this file was autogenerated using tests_silentpayments_generate.py. Do not edit. */")
+print(f"#define SECP256K1_SILENTPAYMENTS_NUMBER_TESTVECTORS ({num_vectors})")
+
+print(STRUCT_DEFINITIONS)
+
+print("static const struct bip352_test_vector bip352_test_vectors[SECP256K1_SILENTPAYMENTS_NUMBER_TESTVECTORS] = {")
+print(out, end='')
+print("};")

From e4ffa036d1d12b8581bac741caf90e58887ced7c Mon Sep 17 00:00:00 2001
From: Sebastian Falbesoner <sebastian.falbesoner@gmail.com>
Date: Fri, 23 Feb 2024 00:25:41 +0100
Subject: [PATCH 13/13] ci: enable silentpayments module

---
 .cirrus.yml              |  3 +++
 .github/workflows/ci.yml | 31 +++++++++++++++++++++----------
 ci/ci.sh                 |  3 ++-
 3 files changed, 26 insertions(+), 11 deletions(-)

diff --git a/.cirrus.yml b/.cirrus.yml
index 04aa8f2409..79c611ce4e 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -22,6 +22,7 @@ env:
   RECOVERY: no
   SCHNORRSIG: no
   ELLSWIFT: no
+  SILENTPAYMENTS: no
   ### test options
   SECP256K1_TEST_ITERS:
   BENCH: yes
@@ -68,6 +69,7 @@ task:
     RECOVERY: yes
     SCHNORRSIG: yes
     ELLSWIFT: yes
+    SILENTPAYMENTS: yes
   matrix:
      # Currently only gcc-snapshot, the other compilers are tested on GHA with QEMU
      - env: { CC: 'gcc-snapshot' }
@@ -84,6 +86,7 @@ task:
     RECOVERY: yes
     SCHNORRSIG: yes
     ELLSWIFT: yes
+    SILENTPAYMENTS: yes
     WRAPPER_CMD: 'valgrind --error-exitcode=42'
     SECP256K1_TEST_ITERS: 2
   matrix:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4ad905af52..a2dce4a7b0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -33,6 +33,7 @@ env:
   RECOVERY: 'no'
   SCHNORRSIG: 'no'
   ELLSWIFT: 'no'
+  SILENTPAYMENTS: 'no'
   ### test options
   SECP256K1_TEST_ITERS:
   BENCH: 'yes'
@@ -71,18 +72,18 @@ jobs:
       matrix:
         configuration:
           - env_vars: { WIDEMUL: 'int64',  RECOVERY: 'yes' }
-          - env_vars: { WIDEMUL: 'int64',                   ECDH: 'yes', SCHNORRSIG: 'yes', ELLSWIFT: 'yes' }
+          - env_vars: { WIDEMUL: 'int64',                   ECDH: 'yes', SCHNORRSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes' }
           - env_vars: { WIDEMUL: 'int128' }
           - env_vars: { WIDEMUL: 'int128_struct',                                           ELLSWIFT: 'yes' }
           - env_vars: { WIDEMUL: 'int128', RECOVERY: 'yes',              SCHNORRSIG: 'yes', ELLSWIFT: 'yes' }
-          - env_vars: { WIDEMUL: 'int128',                  ECDH: 'yes', SCHNORRSIG: 'yes' }
+          - env_vars: { WIDEMUL: 'int128',                  ECDH: 'yes', SCHNORRSIG: 'yes',                  SILENTPAYMENTS: 'yes' }
           - env_vars: { WIDEMUL: 'int128', ASM: 'x86_64',                                   ELLSWIFT: 'yes' }
           - env_vars: {                    RECOVERY: 'yes',              SCHNORRSIG: 'yes' }
-          - env_vars: { CTIMETESTS: 'no',  RECOVERY: 'yes', ECDH: 'yes', SCHNORRSIG: 'yes', CPPFLAGS: '-DVERIFY' }
+          - env_vars: { CTIMETESTS: 'no',  RECOVERY: 'yes', ECDH: 'yes', SCHNORRSIG: 'yes',                  SILENTPAYMENTS: 'yes', CPPFLAGS: '-DVERIFY' }
           - env_vars: { BUILD: 'distcheck', WITH_VALGRIND: 'no', CTIMETESTS: 'no', BENCH: 'no' }
           - env_vars: { CPPFLAGS: '-DDETERMINISTIC' }
           - env_vars: { CFLAGS: '-O0', CTIMETESTS: 'no' }
-          - env_vars: { CFLAGS: '-O1',     RECOVERY: 'yes', ECDH: 'yes', SCHNORRSIG: 'yes', ELLSWIFT: 'yes' }
+          - env_vars: { CFLAGS: '-O1',     RECOVERY: 'yes', ECDH: 'yes', SCHNORRSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes' }
           - env_vars: { ECMULTGENPRECISION: 2, ECMULTWINDOW: 2 }
           - env_vars: { ECMULTGENPRECISION: 8, ECMULTWINDOW: 4 }
         cc:
@@ -141,6 +142,7 @@ jobs:
       RECOVERY: 'yes'
       SCHNORRSIG: 'yes'
       ELLSWIFT: 'yes'
+      SILENTPAYMENTS: 'yes'
       CC: ${{ matrix.cc }}
 
     steps:
@@ -185,6 +187,7 @@ jobs:
       RECOVERY: 'yes'
       SCHNORRSIG: 'yes'
       ELLSWIFT: 'yes'
+      SILENTPAYMENTS: 'yes'
       CTIMETESTS: 'no'
 
     steps:
@@ -236,6 +239,7 @@ jobs:
       RECOVERY: 'yes'
       SCHNORRSIG: 'yes'
       ELLSWIFT: 'yes'
+      SILENTPAYMENTS: 'yes'
       CTIMETESTS: 'no'
 
     steps:
@@ -281,6 +285,7 @@ jobs:
       RECOVERY: 'yes'
       SCHNORRSIG: 'yes'
       ELLSWIFT: 'yes'
+      SILENTPAYMENTS: 'yes'
       CTIMETESTS: 'no'
 
     strategy:
@@ -336,6 +341,7 @@ jobs:
       RECOVERY: 'yes'
       SCHNORRSIG: 'yes'
       ELLSWIFT: 'yes'
+      SILENTPAYMENTS: 'yes'
       CTIMETESTS: 'no'
 
     steps:
@@ -388,6 +394,7 @@ jobs:
       RECOVERY: 'yes'
       SCHNORRSIG: 'yes'
       ELLSWIFT: 'yes'
+      SILENTPAYMENTS: 'yes'
       CTIMETESTS: 'no'
       SECP256K1_TEST_ITERS: 2
 
@@ -439,6 +446,7 @@ jobs:
       RECOVERY: 'yes'
       SCHNORRSIG: 'yes'
       ELLSWIFT: 'yes'
+      SILENTPAYMENTS: 'yes'
       CTIMETESTS: 'no'
       CFLAGS: '-fsanitize=undefined,address -g'
       UBSAN_OPTIONS: 'print_stacktrace=1:halt_on_error=1'
@@ -496,6 +504,7 @@ jobs:
       RECOVERY: 'yes'
       SCHNORRSIG: 'yes'
       ELLSWIFT: 'yes'
+      SILENTPAYMENTS: 'yes'
       CTIMETESTS: 'yes'
       CC: 'clang'
       SECP256K1_TEST_ITERS: 32
@@ -543,6 +552,7 @@ jobs:
       RECOVERY: 'yes'
       SCHNORRSIG: 'yes'
       ELLSWIFT: 'yes'
+      SILENTPAYMENTS: 'yes'
       CTIMETESTS: 'no'
 
     strategy:
@@ -599,14 +609,14 @@ jobs:
       fail-fast: false
       matrix:
         env_vars:
-          - { WIDEMUL: 'int64',  RECOVERY: 'yes', ECDH: 'yes', SCHNORRSIG: 'yes', ELLSWIFT: 'yes' }
+          - { WIDEMUL: 'int64',  RECOVERY: 'yes', ECDH: 'yes', SCHNORRSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes' }
           - { WIDEMUL: 'int128_struct', ECMULTGENPRECISION: 2, ECMULTWINDOW: 4 }
-          - { WIDEMUL: 'int128',                  ECDH: 'yes', SCHNORRSIG: 'yes', ELLSWIFT: 'yes' }
+          - { WIDEMUL: 'int128',                  ECDH: 'yes', SCHNORRSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes' }
           - { WIDEMUL: 'int128', RECOVERY: 'yes' }
-          - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', SCHNORRSIG: 'yes', ELLSWIFT: 'yes' }
-          - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', SCHNORRSIG: 'yes', ELLSWIFT: 'yes', CC: 'gcc' }
-          - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', SCHNORRSIG: 'yes', ELLSWIFT: 'yes',            WRAPPER_CMD: 'valgrind --error-exitcode=42', SECP256K1_TEST_ITERS: 2 }
-          - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', SCHNORRSIG: 'yes', ELLSWIFT: 'yes', CC: 'gcc', WRAPPER_CMD: 'valgrind --error-exitcode=42', SECP256K1_TEST_ITERS: 2 }
+          - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', SCHNORRSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes' }
+          - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', SCHNORRSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes', CC: 'gcc' }
+          - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', SCHNORRSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes',            WRAPPER_CMD: 'valgrind --error-exitcode=42', SECP256K1_TEST_ITERS: 2 }
+          - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', SCHNORRSIG: 'yes', ELLSWIFT: 'yes', SILENTPAYMENTS: 'yes', CC: 'gcc', WRAPPER_CMD: 'valgrind --error-exitcode=42', SECP256K1_TEST_ITERS: 2 }
           - { WIDEMUL: 'int128', RECOVERY: 'yes', ECDH: 'yes', SCHNORRSIG: 'yes', ELLSWIFT: 'yes', CPPFLAGS: '-DVERIFY', CTIMETESTS: 'no' }
           - BUILD: 'distcheck'
 
@@ -718,6 +728,7 @@ jobs:
       RECOVERY: 'yes'
       SCHNORRSIG: 'yes'
       ELLSWIFT: 'yes'
+      SILENTPAYMENTS: 'yes'
 
     steps:
       - name: Checkout
diff --git a/ci/ci.sh b/ci/ci.sh
index 9cc715955e..6486db65c8 100755
--- a/ci/ci.sh
+++ b/ci/ci.sh
@@ -13,7 +13,7 @@ print_environment() {
     # does not rely on bash.
     for var in WERROR_CFLAGS MAKEFLAGS BUILD \
             ECMULTWINDOW ECMULTGENPRECISION ASM WIDEMUL WITH_VALGRIND EXTRAFLAGS \
-            EXPERIMENTAL ECDH RECOVERY SCHNORRSIG ELLSWIFT \
+            EXPERIMENTAL ECDH RECOVERY SCHNORRSIG ELLSWIFT SILENTPAYMENTS \
             SECP256K1_TEST_ITERS BENCH SECP256K1_BENCH_ITERS CTIMETESTS\
             EXAMPLES \
             HOST WRAPPER_CMD \
@@ -76,6 +76,7 @@ esac
     --with-ecmult-gen-precision="$ECMULTGENPRECISION" \
     --enable-module-ecdh="$ECDH" --enable-module-recovery="$RECOVERY" \
     --enable-module-ellswift="$ELLSWIFT" \
+    --enable-module-silentpayments="$SILENTPAYMENTS" \
     --enable-module-schnorrsig="$SCHNORRSIG" \
     --enable-examples="$EXAMPLES" \
     --enable-ctime-tests="$CTIMETESTS" \