gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-exchange] branch master updated: test for link data


From: gnunet
Subject: [taler-exchange] branch master updated: test for link data
Date: Mon, 30 Jan 2023 14:45:57 +0100

This is an automated email from the git hooks/post-receive script.

joseph-xu pushed a commit to branch master
in repository exchange.

The following commit(s) were added to refs/heads/master by this push:
     new 4bb96abc test for link data
4bb96abc is described below

commit 4bb96abc975aa6753828731a015b8ea7c00684f1
Author: Joseph <Joseph.xu@efrei.net>
AuthorDate: Mon Jan 30 08:43:38 2023 -0500

    test for link data
---
 src/exchangedb/exchange_do_get_link_data.sql       |  59 +++++
 src/exchangedb/pg_get_link_data.c                  |  70 +++++-
 src/exchangedb/procedures.sql.in                   |   1 +
 .../test_exchangedb_populate_link_data.c           | 275 +++++++++++----------
 4 files changed, 259 insertions(+), 146 deletions(-)

diff --git a/src/exchangedb/exchange_do_get_link_data.sql 
b/src/exchangedb/exchange_do_get_link_data.sql
new file mode 100644
index 00000000..a76f4aaa
--- /dev/null
+++ b/src/exchangedb/exchange_do_get_link_data.sql
@@ -0,0 +1,59 @@
+--
+-- This file is part of TALER
+-- Copyright (C) 2014--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/>
+--
+/*DROP FUNCTION exchange_do_refund_by_coin(
+  IN in_coin_pub BYTEA,
+  IN in_merchant_pub BYTEA,
+  IN in_h_contract BYTEA
+);*/
+CREATE OR REPLACE FUNCTION exchange_do_get_link_data(
+  IN in_coin_pub BYTEA
+)
+RETURNS SETOF record
+LANGUAGE plpgsql
+AS $$
+DECLARE
+  curs CURSOR
+  FOR
+  SELECT
+   melt_serial_id
+  FROM refresh_commitments
+  WHERE old_coin_pub=in_coin_pub;
+
+DECLARE
+  i RECORD;
+BEGIN
+OPEN curs;
+LOOP
+    FETCH NEXT FROM curs INTO i;
+    EXIT WHEN NOT FOUND;
+    RETURN QUERY
+      SELECT
+       tp.transfer_pub
+      ,denoms.denom_pub
+      ,rrc.ev_sig
+      ,rrc.ewv
+      ,rrc.link_sig
+      ,rrc.freshcoin_index
+      ,rrc.coin_ev
+      FROM refresh_revealed_coins rrc
+       JOIN refresh_transfer_keys tp
+         ON (tp.melt_serial_id=rrc.melt_serial_id)
+       JOIN denominations denoms
+         ON (rrc.denominations_serial=denoms.denominations_serial)
+       WHERE rrc.melt_serial_id =i.melt_serial_id;
+END LOOP;
+CLOSE curs;
+END $$;
diff --git a/src/exchangedb/pg_get_link_data.c 
b/src/exchangedb/pg_get_link_data.c
index 9bc4c9b0..26225a13 100644
--- a/src/exchangedb/pg_get_link_data.c
+++ b/src/exchangedb/pg_get_link_data.c
@@ -177,11 +177,35 @@ TEH_PG_get_link_data (void *cls,
   };
   enum GNUNET_DB_QueryStatus qs;
   struct LinkDataContext ldctx;
+  static int percent_refund = -2;
+  const char *query;
 
-  if (NULL == getenv ("NEW_LOGIC"))
+  if (-2 == percent_refund)
   {
+    const char *mode = getenv ("NEW_LOGIC");
+    char dummy;
+
+    if ( (NULL==mode) ||
+         (1 != sscanf (mode,
+                       "%d%c",
+                       &percent_refund,
+                       &dummy)) )
+      {
+        if (NULL != mode)
+          GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+                      "Bad mode `%s' specified\n",
+                      mode);
+      }
+      if (NULL==mode)
+        percent_refund=0;
+  }
+
+  switch (percent_refund)
+  {
+  case 0:
+    query="get_link";
     PREPARE (pg,
-             "get_link",
+             query,
              "SELECT "
              " tp.transfer_pub"
              ",denoms.denom_pub"
@@ -199,15 +223,15 @@ TEH_PG_get_link_data (void *cls,
              "       ON (rrc.denominations_serial = 
denoms.denominations_serial)"
              " WHERE old_coin_pub=$1"
              " ORDER BY tp.transfer_pub, rrc.freshcoin_index ASC");
-  }
-
-  else
-  {
+    break;
+  case 1:
+    query="get_link_v1";
     PREPARE (pg,
-             "get_link",
+             query,
              "WITH rc AS MATERIALIZED ("
              "SELECT"
-             "* FROM refresh_commitments"
+             " melt_serial_id"
+             " FROM refresh_commitments"
              " WHERE old_coin_pub=$1"
              ")"
              "SELECT "
@@ -217,13 +241,37 @@ TEH_PG_get_link_data (void *cls,
              ",rrc.ewv"
              ",rrc.link_sig"
              ",rrc.freshcoin_index"
-             ",rrc.coin_ev"
-             " FROM refresh_revealed_coins rrc"
+             ",rrc.coin_ev "
+             "FROM "
+             "refresh_revealed_coins rrc"
              "  JOIN refresh_transfer_keys tp"
              "   USING (melt_serial_id)"
              "  JOIN denominations denoms"
              "   USING (denominations_serial)"
+             " WHERE rrc.melt_serial_id = (SELECT melt_serial_id FROM rc)"
              " ORDER BY tp.transfer_pub, rrc.freshcoin_index ASC");
+    break;
+  case 2:
+    query="get_link_v2";
+    PREPARE (pg,
+             query,
+             "SELECT"
+             " *"
+             " FROM"
+             " exchange_do_get_link_data"
+             " ($1) "
+             " AS "
+             " (transfer_pub BYTEA"
+             " ,denom_pub BYTEA"
+             " ,ev_sig BYTEA"
+             " ,ewv BYTEA"
+             " ,link_sig BYTEA"
+             " ,freshcoin_index INT4"
+             " ,coin_ev BYTEA);");
+    break;
+  default:
+    GNUNET_break (0);
+    return GNUNET_DB_STATUS_HARD_ERROR;
   }
 
   ldctx.ldc = ldc;
@@ -231,7 +279,7 @@ TEH_PG_get_link_data (void *cls,
   ldctx.last = NULL;
   ldctx.status = GNUNET_OK;
   qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn,
-                                             "get_link",
+                                             query,
                                              params,
                                              &add_ldl,
                                              &ldctx);
diff --git a/src/exchangedb/procedures.sql.in b/src/exchangedb/procedures.sql.in
index ef6341a1..a53396c3 100644
--- a/src/exchangedb/procedures.sql.in
+++ b/src/exchangedb/procedures.sql.in
@@ -46,5 +46,6 @@ SET search_path TO exchange;
 #include "exchange_do_batch8_reserves_in_insert.sql"
 #include "exchange_do_refund_by_coin.sql"
 #include "exchange_do_get_ready_deposit.sql"
+#include "exchange_do_get_link_data.sql"
 
 COMMIT;
diff --git a/src/exchangedb/test_exchangedb_populate_link_data.c 
b/src/exchangedb/test_exchangedb_populate_link_data.c
index 1323f3b3..a4ddce1a 100644
--- a/src/exchangedb/test_exchangedb_populate_link_data.c
+++ b/src/exchangedb/test_exchangedb_populate_link_data.c
@@ -49,9 +49,10 @@
 
 #define CURRENCY "EUR"
 #define RSA_KEY_SIZE 1024
-#define ROUNDS 10
-#define NUM_ROWS 1000
+#define ROUNDS 2
+#define NUM_ROWS 10
 #define MELT_NEW_COINS 5
+#define DENOMINATIONS 5
 #define MELT_NOREVEAL_INDEX 1
 /**
  * Database plugin under test.
@@ -63,7 +64,7 @@ static struct TALER_DenomFeeSet fees;
  */
 static struct DenomKeyPair **new_dkp;
 static int result;
-static struct TALER_EXCHANGEDB_RefreshRevealedCoin *revealed_coins;
+
 static struct TALER_TransferPrivateKeyP tprivs[TALER_CNC_KAPPA];
 static struct TALER_TransferPublicKeyP tpub;
 struct DenomKeyPair
@@ -165,7 +166,6 @@ create_denom_key_pair (unsigned int size,
   }
   return dkp;
 }
-
 /**
  * Function called with the session hashes and transfer secret
  * information for a given coin.
@@ -181,30 +181,10 @@ handle_link_data_cb (void *cls,
 {
   (void) cls;
   (void) transfer_pub;
-  for (const struct TALER_EXCHANGEDB_LinkList *ldlp = ldl;
-       NULL != ldlp;
-       ldlp = ldlp->next)
-  {
-    bool found;
-
-    found = false;
-    for (unsigned int cnt = 0; cnt < MELT_NEW_COINS; cnt++)
-    {
-      if ( (0 ==
-            TALER_denom_pub_cmp (&ldlp->denom_pub,
-                                 &new_dkp[cnt]->pub)) &&
-           (0 ==
-            TALER_blinded_denom_sig_cmp (&ldlp->ev_sig,
-                                         &revealed_coins[cnt].coin_sig)) )
-      {
-        found = true;
-        break;
-      }
-    }
-    GNUNET_assert (GNUNET_NO != found);
-  }
+  (void) ldl;
 }
 
+
 /**
  * Main function that will be run by the scheduler.
  *
@@ -221,13 +201,11 @@ run (void *cls)
   struct DenomKeyPair *dkp = NULL;
   struct TALER_EXCHANGEDB_Deposit *depos=NULL;
   struct TALER_Amount value;
-  struct TALER_DenominationPublicKey *new_denom_pubs = NULL;
   struct GNUNET_TIME_Relative times = GNUNET_TIME_UNIT_ZERO;
   unsigned long long sqrs=0;
   struct TALER_EXCHANGEDB_Refund *ref=NULL;
   unsigned int *perm;
   unsigned long long duration_sq;
-  struct TALER_EXCHANGEDB_RefreshRevealedCoin *ccoin;
   struct TALER_ExchangeWithdrawValues alg_values = {
     .cipher = TALER_DENOMINATION_RSA
     };
@@ -237,7 +215,7 @@ run (void *cls)
   depos = GNUNET_new_array (ROUNDS +1,
                             struct TALER_EXCHANGEDB_Deposit);
   refresh = GNUNET_new_array (ROUNDS +1,
-                            struct TALER_EXCHANGEDB_Refresh);
+                              struct TALER_EXCHANGEDB_Refresh);
 
   if (NULL ==
       (plugin = TALER_EXCHANGEDB_plugin_load (cfg)))
@@ -282,51 +260,18 @@ run (void *cls)
   {
     //PAIR KEY LIST
     new_dkp = GNUNET_new_array (MELT_NEW_COINS,
-                              struct DenomKeyPair *);
-    //PUBLIC KEY LIST
-    new_denom_pubs = GNUNET_new_array (MELT_NEW_COINS,
-                                       struct TALER_DenominationPublicKey);
-    //REFRESH REVEAL COIN LIST
-    revealed_coins
-      = GNUNET_new_array (MELT_NEW_COINS,
-                          struct TALER_EXCHANGEDB_RefreshRevealedCoin);
+                                struct DenomKeyPair *);
+
     for (unsigned int cnt = 0; cnt < MELT_NEW_COINS; cnt++)
-      {
-        struct GNUNET_TIME_Timestamp now;
-        struct TALER_BlindedRsaPlanchet *rp;
-        struct TALER_BlindedPlanchet *bp;
-
-        now = GNUNET_TIME_timestamp_get ();
-        //5 KEY PAIR
-        new_dkp[cnt] = create_denom_key_pair (RSA_KEY_SIZE,
-                                              now,
-                                              &value,
-                                              &fees);
-        GNUNET_assert (NULL != new_dkp[cnt]);
-        new_denom_pubs[cnt] = new_dkp[cnt]->pub;
-        ccoin = &revealed_coins[cnt];
-        bp = &ccoin->blinded_planchet;
-        bp->cipher = TALER_DENOMINATION_RSA;
-        rp = &bp->details.rsa_blinded_planchet;
-        rp->blinded_msg_size = 1 + (size_t) GNUNET_CRYPTO_random_u64 (
-                                                                      
GNUNET_CRYPTO_QUALITY_WEAK,
-                                                                      
(RSA_KEY_SIZE / 8) - 1);
-        rp->blinded_msg = GNUNET_malloc (rp->blinded_msg_size);
-        GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK,
-                                    rp->blinded_msg,
-                                    rp->blinded_msg_size);
-        TALER_denom_pub_hash (&new_dkp[cnt]->pub,
-                              &ccoin->h_denom_pub);
-        ccoin->exchange_vals = alg_values;
-        TALER_coin_ev_hash (bp,
-                            &ccoin->h_denom_pub,
-                            &ccoin->coin_envelope_hash);
-        GNUNET_assert (GNUNET_OK ==
-                     TALER_denom_sign_blinded (&ccoin->coin_sig,
-                                               &new_dkp[cnt]->priv,
-                                               true,
-                                               bp));
-      }
+    {
+      struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
+      
+      new_dkp[cnt] = create_denom_key_pair (RSA_KEY_SIZE,
+                                            now,
+                                            &value,
+                                            &fees);
+      GNUNET_assert (NULL != new_dkp[cnt]);
+    }
   }
   perm = GNUNET_CRYPTO_random_permute (GNUNET_CRYPTO_QUALITY_NONCE,
                                        NUM_ROWS);
@@ -335,60 +280,111 @@ run (void *cls)
           plugin->start (plugin->cls,
                          "Transaction"));
   for (unsigned int j = 0; j < NUM_ROWS; j++)
+  {
+    union TALER_DenominationBlindingKeyP bks;
+    struct TALER_CoinPubHashP c_hash;
+    unsigned int i = perm[j];
+    uint64_t known_coin_id;
+    struct TALER_EXCHANGEDB_CollectableBlindcoin cbc;
+    if (i >= ROUNDS)
+      i = ROUNDS; /* throw-away slot, do not keep around */
+    RND_BLK (&depos[i].coin.coin_pub);
+    ZR_BLK (&cbc);
+    TALER_denom_pub_hash (&new_dkp[(unsigned int)rand()%MELT_NEW_COINS]->pub,
+                          &depos[i].coin.denom_pub_hash);
+
+
+    
     {
-      union TALER_DenominationBlindingKeyP bks;
-      struct TALER_CoinSpendPublicKeyP coin_pub;
-      struct TALER_CoinPubHashP c_hash;
-      unsigned int k = (unsigned int)rand()%5;
-      unsigned int i = perm[j];
-      if (i >= ROUNDS)
-        i = ROUNDS; /* throw-away slot, do not keep around */
-      RND_BLK (&coin_pub);
-      RND_BLK (&c_hash);
-
-      RND_BLK (&depos[i].coin.coin_pub);
-      TALER_denom_pub_hash (&new_dkp[k]->pub,
-                            &depos[i].coin.denom_pub_hash);
-      GNUNET_assert (GNUNET_OK ==
-                     TALER_denom_sig_unblind (&depos[i].coin.denom_sig,
-                                              &ccoin->coin_sig,
-                                              &bks,
-                                              &c_hash,
-                                              &alg_values,
-                                              &new_dkp[k]->pub));
+      struct TALER_EXCHANGEDB_RefreshRevealedCoin 
revealed_coins[MELT_NEW_COINS];
+      
+      for (unsigned int p=0;p<MELT_NEW_COINS;p++)
+        {
+          struct TALER_EXCHANGEDB_RefreshRevealedCoin *revealed_coin = 
&revealed_coins[p];
+          struct TALER_BlindedPlanchet *bp = &revealed_coin->blinded_planchet;
+          struct TALER_BlindedRsaPlanchet *rp = 
&bp->details.rsa_blinded_planchet;
+          
+          /* h_coin_ev must be unique, but we only have MELT_NEW_COINS created
+             above for NUM_ROWS iterations; instead of making "all new" coins,
+             we simply randomize the hash here as nobody is checking for 
consistency
+             anyway ;-) */
+          bp->cipher = TALER_DENOMINATION_RSA;
+          rp->blinded_msg_size = 1 + (size_t) GNUNET_CRYPTO_random_u64 (
+                                                                        
GNUNET_CRYPTO_QUALITY_WEAK,
+                                                                        
(RSA_KEY_SIZE / 8) - 1);
+          rp->blinded_msg = GNUNET_malloc (rp->blinded_msg_size);
+          GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK,
+                                      rp->blinded_msg,
+                                      rp->blinded_msg_size);
+          TALER_denom_pub_hash (&new_dkp[(unsigned 
int)rand()%MELT_NEW_COINS]->pub,
+                                &revealed_coin->h_denom_pub);
+          revealed_coin->exchange_vals = alg_values;
+          TALER_coin_ev_hash (bp,
+                              &revealed_coin->h_denom_pub,
+                              &revealed_coin->coin_envelope_hash); 
+          GNUNET_assert (GNUNET_OK ==
+                         TALER_denom_sign_blinded (&revealed_coin->coin_sig,
+                                                   &new_dkp[(unsigned 
int)rand()%MELT_NEW_COINS]->priv,
+                                                   true,
+                                                   bp));
+          GNUNET_assert (
+                         GNUNET_OK ==
+                         TALER_denom_sign_blinded (
+                                                   &cbc.sig,
+                                                   &new_dkp[(unsigned 
int)rand()%MELT_NEW_COINS]->priv,
+                                                   false,
+                                                   bp));
+        }
+    GNUNET_assert (GNUNET_OK ==
+                   TALER_denom_sig_unblind (&depos[i].coin.denom_sig,
+                                            &cbc.sig,
+                                            &bks,
+                                            &c_hash,
+                                            &alg_values,
+                                            &new_dkp[(unsigned 
int)rand()%MELT_NEW_COINS]->pub));
+    {
+      /* ENSURE_COIN_KNOWN */
+      struct TALER_DenominationHashP dph;
+      struct TALER_AgeCommitmentHash agh;
+      bool zombie_required = false;
+      bool balance_ok;
+
+      FAILIF (TALER_EXCHANGEDB_CKS_ADDED !=
+              plugin->ensure_coin_known (plugin->cls,
+                                         &depos[i].coin,
+                                         &known_coin_id,
+                                         &dph,
+                                         &agh));
+      /**** INSERT REFRESH COMMITMENTS ****/
+      refresh[i].coin = depos[i].coin;
+      RND_BLK (&refresh[i].coin_sig);
+      RND_BLK (&refresh[i].rc);
+      refresh[i].amount_with_fee = value;
+      refresh[i].noreveal_index = MELT_NOREVEAL_INDEX;
+      FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+              plugin->do_melt (plugin->cls,
+                               NULL,
+                               &refresh[i],
+                               known_coin_id,
+                               &zombie_required,
+                               &balance_ok));
+    }
+      /****GET melt_serial_id generated by default****/
       {
-        /* ENSURE_COIN_KNOWN */
-        uint64_t known_coin_id;
-        struct TALER_DenominationHashP dph;
-        struct TALER_AgeCommitmentHash agh;
-        bool zombie_required = false;
-        bool balance_ok;
-        FAILIF (TALER_EXCHANGEDB_CKS_ADDED !=
-                plugin->ensure_coin_known (plugin->cls,
-                                           &depos[i].coin,
-                                           &known_coin_id,
-                                           &dph,
-                                           &agh));
-        /**** INSERT REFRESH COMMITMENTS ****/
-        refresh[i].coin = depos[i].coin;
-        RND_BLK (&refresh[i].coin_sig);
-        RND_BLK (&refresh[i].rc);
-        refresh[i].amount_with_fee = value;
-        refresh[i].noreveal_index = MELT_NOREVEAL_INDEX;
+        struct TALER_EXCHANGEDB_Melt ret_refresh_session;
+        
         FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
-                plugin->do_melt (plugin->cls,
-                                 NULL,
-                                 &refresh[i],
-                                 known_coin_id,
-                                 &zombie_required,
-                                 &balance_ok));
-        FAILIF (! balance_ok);
-        FAILIF (zombie_required);
+                plugin->get_melt (plugin->cls,
+                                  &refresh[i].rc,
+                                  &ret_refresh_session,
+                                  &melt_serial_id));
       }
       /**** INSERT REFRESH_REVEAL + TRANSFER_KEYS *****/
+      {
+        static unsigned int cnt;
+        
         RND_BLK (&tprivs);
         RND_BLK (&tpub);
-        RND_BLK(&melt_serial_id);
         FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
                 plugin->insert_refresh_reveal (plugin->cls,
                                                melt_serial_id,
@@ -397,9 +393,29 @@ run (void *cls)
                                                TALER_CNC_KAPPA - 1,
                                                tprivs,
                                                &tpub));
-        if (ROUNDS == i)
-          TALER_denom_sig_free (&depos[i].coin.denom_sig);
+        cnt++;
+        //        fprintf (stderr, "CNT: %u - %llu\n", cnt, (unsigned long 
long) melt_serial_id);
+      }
+      for (unsigned int cnt = 0; cnt < MELT_NEW_COINS; cnt++)
+      {
+          TALER_blinded_denom_sig_free (&revealed_coins[cnt].coin_sig);
+          TALER_blinded_planchet_free (&revealed_coins[cnt].blinded_planchet);
+      }
+    
+      {
+        struct TALER_CoinSpendPublicKeyP ocp;
+        uint64_t rrc_serial;
+        
+        FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
+                plugin->get_old_coin_by_h_blind (plugin->cls,
+                                                 
&revealed_coins[0].coin_envelope_hash,
+                                                 &ocp,
+                                                 &rrc_serial));
+      }
     }
+    if (ROUNDS == i)
+      TALER_denom_sig_free (&depos[i].coin.denom_sig);
+  }
   /* End of benchmark setup */
   GNUNET_free(perm);
   // commit
@@ -412,6 +428,7 @@ run (void *cls)
     struct GNUNET_TIME_Relative duration;
     enum GNUNET_DB_QueryStatus qs;
     time = GNUNET_TIME_absolute_get();
+
     qs = plugin->get_link_data (plugin->cls,
                                 &refresh[r].coin.coin_pub,
                                 &handle_link_data_cb,
@@ -444,22 +461,10 @@ run (void *cls)
   }
   result = 0;
 drop:
-  GNUNET_break (GNUNET_OK ==
-  plugin->drop_tables (plugin->cls));
+  // GNUNET_break (GNUNET_OK == plugin->drop_tables (plugin->cls));
 cleanup:
   if (NULL != dkp)
     destroy_denom_key_pair (dkp);
-  if (NULL != revealed_coins)
-  {
-    for (unsigned int cnt = 0; cnt < MELT_NEW_COINS; cnt++)
-    {
-      TALER_blinded_denom_sig_free (&revealed_coins[cnt].coin_sig);
-      TALER_blinded_planchet_free (&revealed_coins[cnt].blinded_planchet);
-    }
-    GNUNET_free (revealed_coins);
-    revealed_coins = NULL;
-  }
-  GNUNET_free (new_denom_pubs);
   for (unsigned int cnt = 0;
        (NULL != new_dkp) && (cnt < MELT_NEW_COINS) && (NULL != new_dkp[cnt]);
        cnt++)

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]