bench.kt (18690B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2024, 2025, 2026 Taler Systems S.A. 4 5 * LibEuFin is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation; either version 3, or 8 * (at your option) any later version. 9 10 * LibEuFin is distributed in the hope that it will be useful, but 11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General 13 * Public License for more details. 14 15 * You should have received a copy of the GNU Affero General Public 16 * License along with LibEuFin; see the file COPYING. If not, see 17 * <http://www.gnu.org/licenses/> 18 */ 19 20 import io.ktor.client.request.* 21 import io.ktor.http.* 22 import org.junit.Test 23 import org.postgresql.jdbc.PgConnection 24 import tech.libeufin.bank.* 25 import tech.libeufin.common.* 26 import tech.libeufin.common.crypto.PwCrypto 27 import tech.libeufin.common.crypto.CryptoUtil 28 import tech.libeufin.common.test.* 29 import java.time.Instant 30 import java.time.LocalDateTime 31 import java.time.ZoneId 32 import java.util.* 33 import kotlin.math.max 34 35 class Bench { 36 37 /** Generate [amount] rows to fill the database */ 38 fun genData(conn: PgConnection, amount: Int) { 39 val amount = max(amount, 10) 40 41 // Skip 4 accounts created by bankSetup 42 val skipAccount = 4 43 // Customer account will be used in tests so we want to generate more data for him 44 val customerAccount = 3 45 val exchangeAccount = 2 46 // In general half of the data is for generated account and half is for customer 47 val mid = amount / 2 48 49 val password = PwCrypto.Bcrypt(cost = 4).hashpw("password") 50 51 val token16 = ByteArray(16) 52 val token32 = ByteArray(32) 53 val token64 = ByteArray(64) 54 55 val accountPubs = List(amount*2) { EddsaPublicKey.randEdsaKey() } 56 57 conn.genData(amount, sequenceOf( 58 "customers(username, name, password_hash, cashout_payto)" to { 59 "account_$it\t$password\tMr n°$it\t$unknownPayto\n" 60 }, 61 "conversion_rate_classes(name)" to { 62 "Class n0$it\n" 63 }, 64 "bank_accounts(internal_payto, owning_customer_id, is_public,conversion_rate_class_id)" to { 65 val conversionId = when (it%5) { 66 0 -> "\\N" 67 1, 2 -> "1" 68 3 -> "2" 69 else -> it%10 70 } 71 "payto://x-taler-bank/localhost/account_$it\t${it+skipAccount}\t${it%3==0}\t$conversionId\n" 72 }, 73 "bearer_tokens(content, creation_time, expiration_time, scope, is_refreshable, bank_customer, description, last_access)" to { 74 val account = if (it > mid) customerAccount else it+4 75 val hex = token32.rand().encodeHex() 76 "\\\\x$hex\t0\t0\treadonly\tfalse\t$account\t\\N\t0\n" 77 }, 78 "bank_account_transactions(creditor_payto, creditor_name, debtor_payto, debtor_name, subject, amount, transaction_date, direction, bank_account_id)" to { 79 val account = if (it > mid) customerAccount else it+4 80 "$unknownPayto\tcreditor_name\t$unknownPayto\tdebtor_name\tsubject\t(42,0)\t0\tcredit\t$exchangeAccount\n" + 81 "$unknownPayto\tcreditor_name\t$unknownPayto\tdebtor_name\tsubject\t(42,0)\t0\tdebit\t$account\n" 82 }, 83 "bank_transaction_operations" to { 84 val hex = token32.rand().encodeHex() 85 "\\\\x$hex\t$it\n" 86 }, 87 "tan_challenges(uuid, hbody, salt, op, code, creation_date, expiration_date, retry_counter, customer, tan_channel, tan_info)" to { 88 val account = if (it > mid) customerAccount else it+4 89 val uuid = UUID.randomUUID() 90 val hex16 = token16.rand().encodeHex() 91 val hex64 = token64.rand().encodeHex() 92 "$uuid\t\\\\x$hex64\t\\\\x$hex16\taccount_reconfig\tcode\t0\t0\t0\t$account\tsms\tinfo\n" 93 }, 94 "taler_withdrawal_operations(withdrawal_uuid, wallet_bank_account, reserve_pub, creation_date)" to { 95 val account = if (it > mid) customerAccount else it+4 96 val hex = token32.rand().encodeHex() 97 val uuid = UUID.randomUUID() 98 "$uuid\t$account\t\\\\x$hex\t0\n" 99 }, 100 "prepared_transfers(type, account_pub, authorization_pub, authorization_sig, recurrent, registered_at, bank_transaction_id)" to { 101 val type = if (it % 2 == 0) "reserve" else "kyc" 102 val recurrent = if (it % 3 == 0) "true" else "false" 103 val incoming_transaction_id = if (it % 5 == 0) "\\N" else "${it*2}" 104 val hex = accountPubs[it].raw.encodeHex() 105 val hex64 = token64.rand().encodeHex() 106 "$type\t\\\\x$hex\t\\\\x$hex\t\\\\x$hex64\t$recurrent\t0\t$incoming_transaction_id\n" 107 }, 108 "pending_recurrent_incoming_transactions(bank_transaction_id, debtor_account_id, authorization_pub)" to { 109 val hex = accountPubs[it].raw.encodeHex() 110 "${it*2}\t${it}\t\\\\x$hex\n" 111 }, 112 "taler_exchange_outgoing(bank_transaction)" to { 113 "${it*2-1}\n" 114 }, 115 "transfer_operations(wtid, request_uid, amount, exchange_base_url, exchange_outgoing_id, exchange_id, transfer_date, creditor_payto, status, status_msg)" to { 116 val hex32 = token32.rand().encodeHex() 117 val hex64 = token64.rand().encodeHex() 118 if (it % 2 == 0) { 119 "\\\\x$hex32\t\\\\x$hex64\t(42, 0)\turl\t$it\t$it\t0\tpayto://x-taler-bank/localhost/10\tsuccess\t\\N\n" 120 } else { 121 "\\\\x$hex32\t\\\\x$hex64\t(42, 0)\turl\t\\N\t$it\t0\tpayto://x-taler-bank/localhost/10\tpermanent_failure\tfailure\n" 122 } 123 }, 124 "taler_exchange_incoming(type, metadata, bank_transaction)" to { 125 val hex = token32.rand().encodeHex() 126 if (it % 2 == 0) { 127 "reserve\t\\\\x$hex\t${it*2}\n" 128 } else { 129 "kyc\t\\\\x$hex\t${it*2}\n" 130 } 131 }, 132 "bank_stats(timeframe, start_time)" to { 133 val instant = Instant.ofEpochSecond(it.toLong()) 134 val date = LocalDateTime.ofInstant(instant, ZoneId.of("UTC")) 135 "day\t$date\n" 136 }, 137 "cashout_operations(request_uid,amount_debit,amount_credit,subject,creation_time,bank_account,local_transaction)" to { 138 val account = if (it > mid) customerAccount else it+4 139 val hex = token32.rand().encodeHex() 140 "\\\\x$hex\t(0,0)\t(0,0)\tsubject\t0\t$account\t$it\n" 141 } 142 )) 143 } 144 145 @Test 146 fun benchDb() = bench { AMOUNT -> bankSetup { db -> 147 // Prepare custoemr accounts 148 fillCashoutInfo("customer") 149 setMaxDebt("customer", "KUDOS:1000000") 150 151 // Generate data 152 db.conn { genData(it, AMOUNT) } 153 154 val accountPubs = List(AMOUNT) { EddsaPublicKey.randEdsaKeyPair() } 155 156 // Warm HTTP client 157 client.get("/config").assertOk() 158 159 // Accounts 160 measureAction("account_create") { 161 client.post("/accounts") { 162 json { 163 "username" to "account_bench_$it" 164 "password" to "account_bench_$it-password" 165 "name" to "Bench Account $it" 166 } 167 }.assertOkJson<RegisterAccountResponse>().internal_payto_uri 168 } 169 measureAction("account_reconfig") { 170 client.patchA("/accounts/account_bench_$it") { 171 json { 172 "name" to "New Bench Account $it" 173 } 174 }.assertNoContent() 175 } 176 measureAction("account_reconfig_auth") { 177 client.patchA("/accounts/account_bench_$it/auth") { 178 json { 179 "old_password" to "account_bench_$it-password" 180 "new_password" to "account_bench_$it-password" 181 } 182 }.assertNoContent() 183 } 184 measureAction("account_list") { 185 client.getAdmin("/accounts").assertOk() 186 } 187 measureAction("account_list_class") { 188 client.getAdmin("/accounts?conversion_rate_class=${it%10}").assertOk() 189 } 190 measureAction("account_list_name") { 191 client.getAdmin("/accounts?name=Mr").assertOk() 192 } 193 measureAction("account_list_public") { 194 client.get("/public-accounts").assertOk() 195 } 196 measureAction("account_get") { 197 client.getA("/accounts/account_bench_$it").assertOk() 198 } 199 200 // Tokens 201 val tokens = measureAction("token_create") { 202 client.postPw("/accounts/customer/token") { 203 json { 204 "scope" to "readonly" 205 "refreshable" to true 206 } 207 }.assertOkJson<TokenSuccessResponse>().access_token 208 } 209 measureAction("token_refresh") { 210 client.post("/accounts/customer/token") { 211 headers[HttpHeaders.Authorization] = "Bearer ${tokens[it]}" 212 json { "scope" to "readonly" } 213 }.assertOk() 214 } 215 measureAction("token_list") { 216 client.getA("/accounts/customer/tokens").assertOk() 217 } 218 measureAction("token_delete") { 219 client.delete("/accounts/customer/token") { 220 headers[HttpHeaders.Authorization] = "Bearer ${tokens[it]}" 221 }.assertNoContent() 222 } 223 224 // Conversion rate classes 225 val classes = measureAction("class_create") { 226 client.postAdmin("/conversion-rate-classes") { 227 json { 228 "name" to "Gen class $it" 229 } 230 }.assertOkJson<ConversionRateClassResponse>().conversion_rate_class_id 231 } 232 measureAction("class_patch") { 233 client.patchAdmin("/conversion-rate-classes/${classes[it]}") { 234 json { 235 "name" to "Gen class $it" 236 "description" to "test $it" 237 } 238 }.assertNoContent() 239 } 240 measureAction("class_get") { 241 client.getAdmin("/conversion-rate-classes/${classes[it]}").assertOk() 242 } 243 measureAction("class_list") { 244 client.getAdmin("/conversion-rate-classes").assertOk() 245 } 246 measureAction("class_delete") { 247 client.deleteAdmin("/conversion-rate-classes/${classes[it]}").assertNoContent() 248 } 249 250 // Transaction 251 val transactions = measureAction("transaction_create") { 252 client.postA("/accounts/customer/transactions") { 253 json { 254 "payto_uri" to "$merchantPayto?receiver-name=Test&message=payout" 255 "amount" to "KUDOS:0.0001" 256 } 257 }.assertOkJson<TransactionCreateResponse>().row_id 258 } 259 measureAction("transaction_get") { 260 client.getA("/accounts/customer/transactions/${transactions[it]}").assertOk() 261 } 262 measureAction("transaction_history") { 263 client.getA("/accounts/customer/transactions").assertOk() 264 } 265 measureAction("transaction_revenue") { 266 client.getA("/accounts/merchant/taler-revenue/history").assertOk() 267 } 268 269 // Withdrawal 270 val withdrawals = measureAction("withdrawal_create") { 271 client.postA("/accounts/customer/withdrawals") { 272 json { 273 "amount" to "KUDOS:0.0001" 274 } 275 }.assertOkJson<BankAccountCreateWithdrawalResponse>().withdrawal_id 276 } 277 measureAction("withdrawal_get") { 278 client.get("/withdrawals/${withdrawals[it]}").assertOk() 279 } 280 measureAction("withdrawal_status") { 281 client.get("/taler-integration/withdrawal-operation/${withdrawals[it]}").assertOk() 282 } 283 measureAction("withdrawal_select") { 284 client.post("/taler-integration/withdrawal-operation/${withdrawals[it]}") { 285 json { 286 "reserve_pub" to EddsaPublicKey.randEdsaKey() 287 "selected_exchange" to exchangePayto 288 } 289 }.assertOk() 290 } 291 measureAction("withdrawal_confirm") { 292 client.postA("/accounts/customer/withdrawals/${withdrawals[it]}/confirm") 293 .assertNoContent() 294 } 295 measureAction("withdrawal_abort") { 296 val uuid = client.postA("/accounts/customer/withdrawals") { 297 json { 298 "amount" to "KUDOS:0.0001" 299 } 300 }.assertOkJson<BankAccountCreateWithdrawalResponse>().withdrawal_id 301 client.postA("/accounts/customer/withdrawals/$uuid/abort") 302 .assertNoContent() 303 } 304 305 // Cashout 306 convert("KUDOS:0.1") 307 val cashouts = measureAction("cashout_create") { 308 client.postA("/accounts/customer/cashouts") { 309 json { 310 "request_uid" to ShortHashCode.rand() 311 "amount_debit" to "KUDOS:0.1" 312 "amount_credit" to convert("KUDOS:0.1") 313 } 314 }.assertOkJson<CashoutResponse>().cashout_id 315 } 316 measureAction("cashout_get") { 317 client.getA("/accounts/customer/cashouts/${cashouts[it]}").assertOk() 318 } 319 measureAction("cashout_history") { 320 client.getA("/accounts/customer/cashouts").assertOk() 321 } 322 measureAction("cashout_history_admin") { 323 client.getAdmin("/cashouts").assertOk() 324 } 325 326 // Wire gateway 327 val transfers = measureAction("wg_transfer") { 328 client.postA("/accounts/exchange/taler-wire-gateway/transfer") { 329 json { 330 "request_uid" to HashCode.rand() 331 "amount" to "KUDOS:0.0001" 332 "exchange_base_url" to "http://exchange.example.com/" 333 "wtid" to ShortHashCode.rand() 334 "credit_account" to customerPayto.canonical 335 } 336 }.assertOkJson<TransferResponse>().row_id 337 } 338 measureAction("wg_transfer_get") { 339 client.getA("/accounts/exchange/taler-wire-gateway/transfers/${transfers[it]}").assertOk() 340 } 341 measureAction("wg_transfer_page") { 342 client.getA("/accounts/exchange/taler-wire-gateway/transfers").assertOk() 343 } 344 measureAction("wg_transfer_page_filter") { 345 client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=success").assertOk() 346 } 347 measureAction("wg_add") { 348 client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { 349 json { 350 "amount" to "KUDOS:0.0001" 351 "reserve_pub" to EddsaPublicKey.randEdsaKey() 352 "debit_account" to customerPayto.canonical 353 } 354 }.assertOk() 355 } 356 measureAction("wg_incoming") { 357 client.getA("/accounts/exchange/taler-wire-gateway/history/incoming") 358 .assertOk() 359 } 360 measureAction("wg_outgoing") { 361 client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing") 362 .assertOk() 363 } 364 365 // TAN challenges 366 val challenges = measureAction("tan_send") { 367 val res = client.patchA("/accounts/account_bench_$it") { 368 json { 369 "contact_data" to obj { 370 "phone" to "+99" 371 "email" to "email@example.com" 372 } 373 "tan_channel" to "sms" 374 } 375 }.assertAcceptedJson<ChallengeResponse>() 376 val challenge = res.challenges[0] 377 client.postA("/accounts/account_bench_$it/challenge/${challenge.challenge_id}").assertOk() 378 val code = tanCode(challenge.tan_info) 379 Pair(challenge.challenge_id, code) 380 } 381 measureAction("tan_confirm") { 382 val (id, code) = challenges[it] 383 client.postA("/accounts/account_bench_$it/challenge/$id/confirm") { 384 json { "tan" to code } 385 }.assertNoContent() 386 } 387 388 // Wire transfer 389 measureAction("wt_register") { 390 val (priv, pub) = accountPubs[it] 391 val valid_req = obj { 392 "credit_amount" to "KUDOS:55" 393 "type" to "reserve" 394 "alg" to "ECDSA" 395 "account_pub" to pub 396 "authorization_pub" to pub 397 "authorization_sig" to CryptoUtil.eddsaSign(pub.raw, priv) 398 "recurrent" to false 399 } 400 client.post("/accounts/exchange/taler-wire-transfer-gateway/registration") { 401 json(valid_req) 402 }.assertOkJson<SubjectResult>() 403 client.post("/accounts/exchange/taler-wire-transfer-gateway/registration") { 404 json(valid_req) 405 }.assertOkJson<SubjectResult>() 406 } 407 measureAction("wt_unregister") { 408 val (priv, pub) = accountPubs[it] 409 val now = Instant.now().toString() 410 val valid_req = obj { 411 "timestamp" to now 412 "authorization_pub" to pub 413 "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) 414 } 415 client.delete("/accounts/exchange/taler-wire-transfer-gateway/registration") { 416 json(valid_req) 417 }.assertNoContent() 418 client.delete("/accounts/exchange/taler-wire-transfer-gateway/registration") { 419 json(valid_req) 420 }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) 421 } 422 423 // Delete accounts 424 425 measureAction("account_delete") { 426 client.deleteA("/accounts/account_bench_$it").assertNoContent() 427 } 428 429 // Other 430 measureAction("monitor") { 431 client.getAdmin("/monitor").assertOk() 432 } 433 db.gc.collect(Instant.now(), java.time.Duration.ZERO, java.time.Duration.ZERO, java.time.Duration.ZERO) 434 measureAction("gc") { 435 db.gc.collect(Instant.now(), java.time.Duration.ZERO, java.time.Duration.ZERO, java.time.Duration.ZERO) 436 } 437 } } 438 }