commit 5b6579b711971cdc04e8435c65e1f041136d7ef5
parent ce42b6945114cd4a2518fdd01997aa76e06c98b5
Author: Özgür Kesim <oec@codeblau.de>
Date: Tue, 15 Sep 2026 10:06:19 +0200
pq: array query parameter for unblinded denomination signatures
TALER_PQ_query_param_array_denom_sig() encodes each element exactly
like GNUNET_PQ_query_param_unblinded_sig(), so the elements can be read
back with GNUNET_PQ_result_spec_unblinded_sig(). test_pq inserts a CS
and an RSA signature and reads them back element-wise.
Diffstat:
6 files changed, 259 insertions(+), 265 deletions(-)
diff --git a/src/exchangedb/do_insert_known_coin.c b/src/exchangedb/do_insert_known_coin.c
@@ -1,166 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2022 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-/**
- * @file exchangedb/do_insert_known_coin.c
- * @brief Implementation of the do_insert_known_coin function for Postgres
- * @author Christian Grothoff
- */
-#include "exchangedb_lib.h"
-#include "taler/taler_pq_lib.h"
-#include "exchange-database/do_insert_known_coin.h"
-#include "helper.h"
-
-
-enum TALER_EXCHANGEDB_CoinKnownStatus
-TALER_EXCHANGEDB_do_insert_known_coin (
- struct TALER_EXCHANGEDB_PostgresContext *pg,
- const struct TALER_CoinPublicInfo *coin,
- uint64_t *known_coin_id,
- struct TALER_DenominationHashP *denom_hash,
- struct TALER_AgeCommitmentHashP *h_age_commitment)
-{
- enum GNUNET_DB_QueryStatus qs;
- bool existed;
- bool no_denom_pub_hash;
- bool no_age_commitment_hash;
- struct GNUNET_PQ_QueryParam params[] = {
- GNUNET_PQ_query_param_auto_from_type (&coin->coin_pub),
- GNUNET_PQ_query_param_auto_from_type (&coin->denom_pub_hash),
- coin->no_age_commitment
- ? GNUNET_PQ_query_param_null ()
- : GNUNET_PQ_query_param_auto_from_type (&coin->h_age_commitment),
- TALER_PQ_query_param_denom_sig (&coin->denom_sig),
- GNUNET_PQ_query_param_end
- };
- struct GNUNET_PQ_ResultSpec rs[] = {
- GNUNET_PQ_result_spec_bool ("existed",
- &existed),
- GNUNET_PQ_result_spec_uint64 ("known_coin_id",
- known_coin_id),
- GNUNET_PQ_result_spec_allow_null (
- GNUNET_PQ_result_spec_auto_from_type ("denom_pub_hash",
- denom_hash),
- &no_denom_pub_hash),
- GNUNET_PQ_result_spec_allow_null (
- GNUNET_PQ_result_spec_auto_from_type ("age_commitment_hash",
- h_age_commitment),
- &no_age_commitment_hash),
- GNUNET_PQ_result_spec_end
- };
-
- /*
- See also:
- https://stackoverflow.com/questions/34708509/how-to-use-returning-with-on-conflict-in-postgresql/37543015#37543015
- */
- PREPARE (pg,
- "do_insert_known_coin",
- "WITH dd"
- " (denominations_serial"
- " ,coin"
- " ) AS ("
- " SELECT "
- " denominations_serial"
- " ,coin"
- " FROM denominations"
- " WHERE denom_pub_hash=$2"
- " ), input_rows"
- " (coin_pub) AS ("
- " VALUES ($1::BYTEA)"
- " ), ins AS ("
- " INSERT INTO known_coins "
- " (coin_pub"
- " ,denominations_serial"
- " ,age_commitment_hash"
- " ,denom_sig"
- " ,remaining"
- " ) SELECT "
- " $1"
- " ,denominations_serial"
- " ,$3"
- " ,$4"
- " ,coin"
- " FROM dd"
- " ON CONFLICT DO NOTHING" /* CONFLICT on (coin_pub) */
- " RETURNING "
- " known_coin_id"
- " ) "
- "SELECT "
- " FALSE AS existed"
- " ,known_coin_id"
- " ,NULL AS denom_pub_hash"
- " ,NULL AS age_commitment_hash"
- " FROM ins "
- "UNION ALL "
- "SELECT "
- " TRUE AS existed"
- " ,known_coin_id"
- " ,denom_pub_hash"
- " ,kc.age_commitment_hash"
- " FROM input_rows"
- " JOIN known_coins kc USING (coin_pub)"
- " JOIN denominations USING (denominations_serial)"
- " LIMIT 1");
- qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
- "do_insert_known_coin",
- params,
- rs);
- switch (qs)
- {
- case GNUNET_DB_STATUS_HARD_ERROR:
- GNUNET_break (0);
- return TALER_EXCHANGEDB_CKS_HARD_FAIL;
- case GNUNET_DB_STATUS_SOFT_ERROR:
- return TALER_EXCHANGEDB_CKS_SOFT_FAIL;
- case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
- GNUNET_break (0); /* should be impossible */
- return TALER_EXCHANGEDB_CKS_HARD_FAIL;
- case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
- if (! existed)
- return TALER_EXCHANGEDB_CKS_ADDED;
- break; /* continued below */
- }
-
- if ( (! no_denom_pub_hash) &&
- (0 != GNUNET_memcmp (denom_hash,
- &coin->denom_pub_hash)) )
- {
- GNUNET_break_op (0);
- return TALER_EXCHANGEDB_CKS_DENOM_CONFLICT;
- }
-
- if (no_age_commitment_hash != coin->no_age_commitment)
- {
- if (no_age_commitment_hash)
- {
- GNUNET_break_op (0);
- return TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NULL;
- }
- else
- {
- GNUNET_break_op (0);
- return TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NON_NULL;
- }
- }
- else if ( (! no_age_commitment_hash) &&
- (0 != GNUNET_memcmp (h_age_commitment,
- &coin->h_age_commitment)) )
- {
- GNUNET_break_op (0);
- return TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS;
- }
-
- return TALER_EXCHANGEDB_CKS_PRESENT;
-}
diff --git a/src/include/exchange-database/do_insert_known_coin.h b/src/include/exchange-database/do_insert_known_coin.h
@@ -1,98 +0,0 @@
-/*
- This file is part of TALER
- Copyright (C) 2022 Taler Systems SA
-
- TALER is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-/**
- * @file src/include/exchange-database/do_insert_known_coin.h
- * @brief implementation of the do_insert_known_coin function for Postgres
- * @author Christian Grothoff
- */
-#ifndef EXCHANGE_DATABASE_DO_INSERT_KNOWN_COIN_H
-#define EXCHANGE_DATABASE_DO_INSERT_KNOWN_COIN_H
-
-#include "exchangedb_lib.h"
-
-/**
- * Possible status codes from making sure a coin is known.
- */
-enum TALER_EXCHANGEDB_CoinKnownStatus
-{
- /**
- * The coin was successfully added.
- */
- TALER_EXCHANGEDB_CKS_ADDED = 1,
-
- /**
- * The coin was already present.
- */
- TALER_EXCHANGEDB_CKS_PRESENT = 0,
-
- /**
- * Serialization failure.
- */
- TALER_EXCHANGEDB_CKS_SOFT_FAIL = -1,
-
- /**
- * Hard database failure.
- */
- TALER_EXCHANGEDB_CKS_HARD_FAIL = -2,
-
- /**
- * Conflicting coin (different denomination key) already in database.
- */
- TALER_EXCHANGEDB_CKS_DENOM_CONFLICT = -3,
-
- /**
- * Conflicting coin (expected NULL age hash) already in database.
- */
- TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NULL = -4,
-
- /**
- * Conflicting coin (unexpected NULL age hash) already in database.
- */
- TALER_EXCHANGEDB_CKS_AGE_CONFLICT_EXPECTED_NON_NULL = -5,
-
- /**
- * Conflicting coin (different age hash) already in database.
- */
- TALER_EXCHANGEDB_CKS_AGE_CONFLICT_VALUE_DIFFERS = -6,
-
-};
-
-
-/**
- * Make sure the given @a coin is known to the database.
- *
- * Primary test table: `known_coins` (see test_known_coins.c).
- *
- * @param pg the database context
- * @param coin the coin that must be made known
- * @param[out] known_coin_id set to the unique row of the coin
- * @param[out] denom_hash set to the denomination hash of the existing
- * coin (for conflict error reporting)
- * @param[out] h_age_commitment set to the conflicting age commitment hash on conflict
- * @return database transaction status, non-negative on success
- */
-enum TALER_EXCHANGEDB_CoinKnownStatus
-TALER_EXCHANGEDB_do_insert_known_coin (struct TALER_EXCHANGEDB_PostgresContext *
- pg,
- const struct TALER_CoinPublicInfo *coin,
- uint64_t *known_coin_id,
- struct TALER_DenominationHashP *
- denom_hash,
- struct TALER_AgeCommitmentHashP *
- h_age_commitment)
-;
-
-#endif
diff --git a/src/include/taler/taler_pq_lib.h b/src/include/taler/taler_pq_lib.h
@@ -154,6 +154,24 @@ TALER_PQ_query_param_array_blinded_denom_sig (
/**
+ * Generate query parameter for an array of (unblinded) denomination
+ * signatures. Each element is encoded exactly like
+ * #GNUNET_PQ_query_param_unblinded_sig() encodes a single signature, so
+ * the elements can be read back with #GNUNET_PQ_result_spec_unblinded_sig().
+ *
+ * @param num number of elements in @a denom_sigs
+ * @param denom_sigs continuous array of denomination signatures
+ * @param db database context, needed for OID lookups
+ * @return query parameter to use
+ */
+struct GNUNET_PQ_QueryParam
+TALER_PQ_query_param_array_denom_sig (
+ size_t num,
+ const struct TALER_DenominationSignature *denom_sigs,
+ struct GNUNET_PQ_Context *db);
+
+
+/**
* Generate query parameter for an array of blinded hashes of coin envelopes
*
* @param num number of elements in @e denom_sigs
diff --git a/src/pq/pq_common.h b/src/pq/pq_common.h
@@ -42,6 +42,10 @@
enum TALER_PQ_ArrayType
{
TALER_PQ_array_of_blinded_denom_sig,
+ /**
+ * Unblinded denomination signatures (`struct TALER_DenominationSignature`).
+ */
+ TALER_PQ_array_of_denom_sig,
TALER_PQ_array_of_blinded_coin_hash,
TALER_PQ_array_of_denom_hash,
TALER_PQ_array_of_hash_code,
diff --git a/src/pq/pq_query_helper.c b/src/pq/pq_query_helper.c
@@ -785,6 +785,45 @@ qconv_array (
sizes = buffer_lengths;
break;
}
+ case TALER_PQ_array_of_denom_sig:
+ {
+ const struct TALER_DenominationSignature *denom_sigs = data;
+ size_t len;
+
+ buffers = GNUNET_new_array (num, void *);
+ buffer_lengths = GNUNET_new_array (num, size_t);
+
+ for (size_t i = 0; i<num; i++)
+ {
+ const struct GNUNET_CRYPTO_UnblindedSignature *ubs =
+ denom_sigs[i].unblinded_sig;
+
+ switch (ubs->cipher)
+ {
+ case GNUNET_CRYPTO_BSA_RSA:
+ len = GNUNET_CRYPTO_rsa_signature_encode (
+ ubs->details.rsa_signature,
+ &buffers[i]);
+ RETURN_UNLESS (len != 0);
+ break;
+ case GNUNET_CRYPTO_BSA_CS:
+ len = sizeof (ubs->details.cs_signature);
+ break;
+ default:
+ GNUNET_assert (0);
+ }
+
+ /* for the cipher and marker */
+ len += 2 * sizeof(uint32_t);
+ buffer_lengths[i] = len;
+
+ y = total_size;
+ total_size += len;
+ RETURN_UNLESS (total_size >= y);
+ }
+ sizes = buffer_lengths;
+ break;
+ }
default:
GNUNET_assert (0);
}
@@ -925,6 +964,41 @@ qconv_array (
}
break;
}
+ case TALER_PQ_array_of_denom_sig:
+ {
+ const struct TALER_DenominationSignature *denom_sigs = data;
+ const struct GNUNET_CRYPTO_UnblindedSignature *ubs =
+ denom_sigs[i].unblinded_sig;
+ uint32_t be[2];
+
+ /* Same encoding as GNUNET_PQ_query_param_unblinded_sig() */
+ be[0] = htonl ((uint32_t) ubs->cipher);
+ be[1] = htonl (0x00); /* magic marker: unblinded */
+ GNUNET_memcpy (out,
+ &be,
+ sizeof(be));
+ out += sizeof(be);
+ sz -= sizeof(be);
+
+ switch (ubs->cipher)
+ {
+ case GNUNET_CRYPTO_BSA_RSA:
+ /* For RSA, 'same_sized' must have been false */
+ GNUNET_assert (NULL != buffers);
+ GNUNET_memcpy (out,
+ buffers[i],
+ sz);
+ break;
+ case GNUNET_CRYPTO_BSA_CS:
+ GNUNET_memcpy (out,
+ &ubs->details.cs_signature,
+ sz);
+ break;
+ default:
+ GNUNET_assert (0);
+ }
+ break;
+ }
case TALER_PQ_array_of_blinded_coin_hash:
{
const struct TALER_BlindedCoinHashP *coin_hs = data;
@@ -1062,6 +1136,29 @@ TALER_PQ_query_param_array_blinded_denom_sig (
struct GNUNET_PQ_QueryParam
+TALER_PQ_query_param_array_denom_sig (
+ size_t num,
+ const struct TALER_DenominationSignature *denom_sigs,
+ struct GNUNET_PQ_Context *db)
+{
+ Oid oid;
+
+ GNUNET_assert (GNUNET_OK ==
+ GNUNET_PQ_get_oid_by_name (db,
+ "bytea",
+ &oid));
+ return query_param_array_generic (num,
+ true,
+ denom_sigs,
+ NULL,
+ 0,
+ TALER_PQ_array_of_denom_sig,
+ oid,
+ NULL);
+}
+
+
+struct GNUNET_PQ_QueryParam
TALER_PQ_query_param_array_blinded_coin_hash (
size_t num,
const struct TALER_BlindedCoinHashP *coin_hs,
diff --git a/src/pq/test_pq.c b/src/pq/test_pq.c
@@ -44,8 +44,17 @@ postgres_prepare (struct GNUNET_PQ_Context *db)
",hash"
",hashes"
",cs_r_pubs"
+ ",denom_sigs"
") VALUES "
- "($1, $2, $3, $4, $5, $6, $7, $8);"),
+ "($1, $2, $3, $4, $5, $6, $7, $8, $9);"),
+ GNUNET_PQ_make_prepare ("test_select_denom_sigs",
+ "SELECT"
+ " ds.denom_sig"
+ " FROM test_pq"
+ " CROSS JOIN LATERAL"
+ " UNNEST (denom_sigs) WITH ORDINALITY"
+ " AS ds (denom_sig, idx)"
+ " ORDER BY ds.idx;"),
GNUNET_PQ_make_prepare ("test_select",
"SELECT"
" tamount"
@@ -68,6 +77,79 @@ postgres_prepare (struct GNUNET_PQ_Context *db)
/**
+ * Closure for #denom_sig_cb().
+ */
+struct DenomSigCheck
+{
+ /**
+ * Signatures we inserted, in order.
+ */
+ const struct TALER_DenominationSignature *expected;
+
+ /**
+ * Number of entries in @e expected.
+ */
+ unsigned int num;
+
+ /**
+ * Rows seen so far.
+ */
+ unsigned int seen;
+
+ /**
+ * Set to true if a row did not match.
+ */
+ bool mismatch;
+};
+
+
+/**
+ * Compare each unblinded signature read back from the array
+ * with the one we inserted at the same position.
+ *
+ * @param cls a `struct DenomSigCheck *`
+ * @param result the result
+ * @param num_results number of rows
+ */
+static void
+denom_sig_cb (void *cls,
+ PGresult *result,
+ unsigned int num_results)
+{
+ struct DenomSigCheck *dsc = cls;
+
+ for (unsigned int i = 0; i < num_results; i++)
+ {
+ struct TALER_DenominationSignature got = { 0 };
+ struct GNUNET_PQ_ResultSpec rs[] = {
+ GNUNET_PQ_result_spec_unblinded_sig ("denom_sig",
+ &got.unblinded_sig),
+ GNUNET_PQ_result_spec_end
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_PQ_extract_result (result,
+ rs,
+ i))
+ {
+ GNUNET_break (0);
+ dsc->mismatch = true;
+ return;
+ }
+ if ( (dsc->seen >= dsc->num) ||
+ (0 != TALER_denom_sig_cmp (&got,
+ &dsc->expected[dsc->seen])) )
+ {
+ GNUNET_break (0);
+ dsc->mismatch = true;
+ }
+ dsc->seen++;
+ GNUNET_PQ_cleanup_result (rs);
+ }
+}
+
+
+/**
* Run actual test queries.
*
* @return 0 on success
@@ -75,6 +157,7 @@ postgres_prepare (struct GNUNET_PQ_Context *db)
static int
run_queries (struct GNUNET_PQ_Context *conn)
{
+ struct TALER_DenominationSignature denom_sigs[2];
struct TALER_Amount tamount;
struct TALER_Amount aamount[3];
struct TALER_Amount aamountc[2];
@@ -125,6 +208,31 @@ run_queries (struct GNUNET_PQ_Context *conn)
json_integer (42)));
GNUNET_CRYPTO_random_block (in_cs_r_pubs,
sizeof(struct GNUNET_CRYPTO_CSPublicRPairP) * 5);
+ /* one CS and one RSA signature: the two encodings differ in size */
+ {
+ struct GNUNET_CRYPTO_UnblindedSignature *ubs;
+ struct GNUNET_CRYPTO_RsaPrivateKey *rsa_priv;
+
+ ubs = GNUNET_new (struct GNUNET_CRYPTO_UnblindedSignature);
+ ubs->cipher = GNUNET_CRYPTO_BSA_CS;
+ ubs->rc = 1;
+ GNUNET_CRYPTO_random_block (&ubs->details.cs_signature,
+ sizeof (ubs->details.cs_signature));
+ denom_sigs[0].unblinded_sig = ubs;
+
+ rsa_priv = GNUNET_CRYPTO_rsa_private_key_create (1024);
+ GNUNET_assert (NULL != rsa_priv);
+ ubs = GNUNET_new (struct GNUNET_CRYPTO_UnblindedSignature);
+ ubs->cipher = GNUNET_CRYPTO_BSA_RSA;
+ ubs->rc = 1;
+ ubs->details.rsa_signature
+ = GNUNET_CRYPTO_rsa_sign_fdh (rsa_priv,
+ &hc,
+ sizeof (hc));
+ GNUNET_assert (NULL != ubs->details.rsa_signature);
+ GNUNET_CRYPTO_rsa_private_key_free (rsa_priv);
+ denom_sigs[1].unblinded_sig = ubs;
+ }
{
struct GNUNET_PQ_QueryParam params_insert[] = {
TALER_PQ_query_param_amount (conn,
@@ -147,6 +255,9 @@ run_queries (struct GNUNET_PQ_Context *conn)
TALER_PQ_query_param_array_cs_r_pub (5,
in_cs_r_pubs,
conn),
+ TALER_PQ_query_param_array_denom_sig (2,
+ denom_sigs,
+ conn),
GNUNET_PQ_query_param_end
};
PGresult *result;
@@ -288,6 +399,33 @@ run_queries (struct GNUNET_PQ_Context *conn)
}
GNUNET_PQ_cleanup_result (results_select);
}
+ {
+ struct DenomSigCheck dsc = {
+ .expected = denom_sigs,
+ .num = 2
+ };
+ struct GNUNET_PQ_QueryParam params_select[] = {
+ GNUNET_PQ_query_param_end
+ };
+
+ if (2 !=
+ GNUNET_PQ_eval_prepared_multi_select (conn,
+ "test_select_denom_sigs",
+ params_select,
+ &denom_sig_cb,
+ &dsc))
+ {
+ GNUNET_break (0);
+ return 1;
+ }
+ GNUNET_break (2 == dsc.seen);
+ GNUNET_break (! dsc.mismatch);
+ if ( (2 != dsc.seen) ||
+ (dsc.mismatch) )
+ return 1;
+ }
+ TALER_denom_sig_free (&denom_sigs[0]);
+ TALER_denom_sig_free (&denom_sigs[1]);
return 0;
}
@@ -337,6 +475,7 @@ main (int argc,
",hash gnunet_hashcode"
",hashes gnunet_hashcode[]"
",cs_r_pubs BYTEA[]"
+ ",denom_sigs BYTEA[]"
")"),
GNUNET_PQ_EXECUTE_STATEMENT_END
};