summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTorsten Grote <t@grobox.de>2020-07-02 13:41:34 -0300
committerTorsten Grote <t@grobox.de>2020-07-02 13:41:34 -0300
commit4a712e1da5bb66d7a7e27271e65e09d4910e2339 (patch)
tree10221c1edda8ee2d788f66abeb1cc52ed74e5151
parent08f5f3618b5a9d9f38f7da0dbff39164c8b5f77b (diff)
downloadwallet-kotlin-4a712e1da5bb66d7a7e27271e65e09d4910e2339.tar.gz
wallet-kotlin-4a712e1da5bb66d7a7e27271e65e09d4910e2339.tar.bz2
wallet-kotlin-4a712e1da5bb66d7a7e27271e65e09d4910e2339.zip
Add refresh crypto with tests
-rw-r--r--build.gradle1
-rw-r--r--src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/RsaBlinding.kt6
-rw-r--r--src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshTest.kt481
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt2
-rw-r--r--src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Refresh.kt256
5 files changed, 743 insertions, 3 deletions
diff --git a/build.gradle b/build.gradle
index 8ae318e..2dce365 100644
--- a/build.gradle
+++ b/build.gradle
@@ -39,6 +39,7 @@ kotlin {
commonMain {
dependencies {
implementation kotlin('stdlib-common')
+ implementation "com.soywiz.korlibs.klock:klock:1.11.12"
}
}
commonTest {
diff --git a/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/RsaBlinding.kt b/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/RsaBlinding.kt
index 6158c52..715bf83 100644
--- a/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/RsaBlinding.kt
+++ b/src/androidMain/kotlin/net/taler/wallet/kotlin/crypto/RsaBlinding.kt
@@ -1,10 +1,10 @@
package net.taler.wallet.kotlin.crypto
import java.math.BigInteger
+import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.floor
-@OptIn(ExperimentalStdlibApi::class)
internal object RsaBlinding {
fun rsaBlind(hm: ByteArray, bks: ByteArray, rsaPubEnc: ByteArray): ByteArray {
@@ -40,8 +40,8 @@ internal object RsaBlinding {
}
private fun rsaPubDecode(publicKey: ByteArray): RsaPublicKey {
- val modulusLength = (publicKey[0].toInt() shl 8) or publicKey[1].toInt()
- val exponentLength = (publicKey[2].toInt() shl 8) or publicKey[3].toInt()
+ val modulusLength = abs((publicKey[0].toInt() shl 8) or publicKey[1].toInt())
+ val exponentLength = abs((publicKey[2].toInt() shl 8) or publicKey[3].toInt())
if (4 + exponentLength + modulusLength != publicKey.size) {
throw Error("invalid RSA public key (format wrong)")
}
diff --git a/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshTest.kt b/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshTest.kt
new file mode 100644
index 0000000..37cf10f
--- /dev/null
+++ b/src/androidTest/kotlin/net/taler/wallet/kotlin/crypto/RefreshTest.kt
@@ -0,0 +1,481 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2020 Taler Systems S.A.
+ *
+ * GNU 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.
+ *
+ * GNU 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
+ * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.CoinRecord
+import net.taler.wallet.kotlin.CoinSourceType.WITHDRAW
+import net.taler.wallet.kotlin.CoinStatus.DORMANT
+import net.taler.wallet.kotlin.DenominationRecord
+import net.taler.wallet.kotlin.DenominationStatus
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.crypto.Refresh.DenominationSelectionInfo
+import net.taler.wallet.kotlin.crypto.Refresh.RefreshPlanchetRecord
+import net.taler.wallet.kotlin.crypto.Refresh.RefreshSessionRecord
+import net.taler.wallet.kotlin.crypto.Refresh.SelectedDenomination
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+// TODO move to commonTest once RsaBlinding is implemented everywhere
+class RefreshTest {
+
+ private val crypto = CryptoFactory.getCrypto()
+ private val refresh = Refresh(crypto)
+
+ private class RefreshVector(
+ val kappa: Int,
+ val meltCoin: CoinRecord,
+ val newCoinDenominations: DenominationSelectionInfo,
+ val meltFee: Amount,
+ val refreshSessionRecord: RefreshSessionRecord,
+ val kappaKeys: List<EcdheKeyPair>
+ )
+
+ @Test
+ fun testCreateRefreshSession() {
+ val vectors = listOf(
+ RefreshVector(
+ kappa = 3,
+ meltCoin = CoinRecord(
+ blindingKey = "N2A8K8XVTMPKNRCEJ1B8GMWJM6TXWRGAN5HWPFGPXS3WY76EKB60",
+ coinPriv = "884XH9DBQT9MA34YKCR7WPEP2HH3J1R9C3G7MSB8C3NHB0XEVZW0",
+ coinPub = "3GSHK9JCTMEAZY7BMC1QQD28ABEJEJJJXHPVF2YXTET97WB81GNG",
+ currentAmount = Amount("TESTKUDOS", 1, 96000000),
+ denomPub = "020000YDCXEZRKA0RR18PBW1HGXAH9HM9PN5TPWSVB8TG9QP4KDNHDVS0BRM9ATZV5AW5238WSNJWXA7V6WHE5QGMKRTSZYZZ6DWNHMZGAKCBC3T1HWKVPBHTRJZK68CGRQQ7A6VBRPCRS26QP8Z89TFRVN9NAQC6ZR0CD6P269XNQXYQAARNVBB74QSHJJXPMB5HE6TQ1HHK6AZRNWG2001",
+ denomPubHash = "9ZSE6E0271SRAZTHS08BYF8TGAY2Y1Q8Y7TYWM3ZM25THE3ZJYNKDCXR72YKVQSES1GXWGQVSF13YZ0Z2RQE5VRBMF79KJQP85R71X8",
+ denomSig = "73XS5TEM2WNQWJJBBMYC77DCV98NKX949KAFNJX9KXW2H9QDE4FM7SEJ7RB6RHX99QJGVT6SD1N24GXQCJY1CZA57RKDP3DK2C9WJ8HZ09Y33MYV33TD7C325GQQJFSSZ1EC5JX06BYNGKB0VEHXAR7KHXVT0Y5MZXSD8X65GRNPFV8B3HKC6BBNGVVXXM28WA7VTA7EWJKM4",
+ exchangeBaseUrl = "example.org", status = DORMANT, coinSource = WITHDRAW, suspended = false
+ ),
+ newCoinDenominations = DenominationSelectionInfo(
+ totalCoinValue = Amount("TESTKUDOS", 1, 81000000),
+ totalWithdrawCost = Amount("TESTKUDOS", 1, 92000000),
+ selectedDenominations = listOf(
+ SelectedDenomination(
+ count = 1,
+ denominationRecord = DenominationRecord(
+ denomPub = "020000YDW0GZQGY9GSKTEESAD1ZS803D576X1HTVJM8CWEBPVGQ4GHMDD5SBQHJ462NPW9FJD437HYW69MJR5N4YABJWYZB6P7CZ7CZR0YD7KY1M7C291BQX4T18DGKCTDEDBMVH2CF4K4NWVA1FFX4AYDB5GRWJ0JBJRRAVQ1ZY8D9PX9TJT9HEVBXAMQQF2ESKG1JX5CXDX7AMY2TG2001",
+ denomPubHash = "ZVZMM6GEFETH1S71BRKFKB03BTSTA8JZH26Q1PZED78NZ50EVVT0DMDBPDN5EBY90SK4AWT0J8CNPD7NB2TSCFMSKAS5V6DK26GEKT0",
+ exchangeBaseUrl = "example.org",
+ feeDeposit = Amount("TESTKUDOS", fraction = 2000000, value = 0),
+ feeRefresh = Amount("TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount("TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount("TESTKUDOS", fraction = 2000000, value = 0),
+ isOffered = true,
+ isRevoked = false,
+ masterSig = "1XDNXJXA9CY30Q5BTJW5YN8XSYCCFKKSVGR9TW5CFPD4YSTYY52YBHDEPEAX3AY1HWR0XHYQHRJCX156NZMAPQ4BPZ00EXTW0MA1018",
+ stampExpireDeposit = Timestamp(1656768984000),
+ stampExpireLegal = Timestamp(1688304984000),
+ stampExpireWithdraw = Timestamp(1594301784000),
+ stampStart = Timestamp(1593696984000),
+ status = DenominationStatus.VerifiedGood,
+ value = Amount("TESTKUDOS", fraction = 0, value = 1)
+ )
+ ),
+ SelectedDenomination(
+ count = 8,
+ denominationRecord = DenominationRecord(
+ denomPub = "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ denomPubHash = "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ exchangeBaseUrl = "example.org",
+ feeDeposit = Amount("TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefresh = Amount("TESTKUDOS", fraction = 3000000, value = 0),
+ feeRefund = Amount("TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount("TESTKUDOS", fraction = 1000000, value = 0),
+ isOffered = true,
+ isRevoked = false,
+ masterSig = "X4WYKHZVAQFRXKCAQ0QCA8VFMC32ESJP8M3BTTYW20SBR46Q1HRF9Y309SQVVH8TY3R8PJ81A4QFAABYEQR2D9RXTSVCBMD667Q723R",
+ stampExpireDeposit = Timestamp(1656768984000),
+ stampExpireLegal = Timestamp(1688304984000),
+ stampExpireWithdraw = Timestamp(1594301784000),
+ stampStart = Timestamp(1593696984000),
+ status = DenominationStatus.VerifiedGood,
+ value = Amount("TESTKUDOS", fraction = 10000000, value = 0)
+ )
+ ),
+ SelectedDenomination(
+ count = 1,
+ denominationRecord = DenominationRecord(
+ denomPub = "020000Z46ESSPKMQ5875ZZMX9EZ4ZD7MQ3MPM4R2QJ7K7ADMCK0CJDM7W6GEZF0BQGWHVCMA4CY83NBYKBZYB9AYG293JD7VW7BSQTFH51HK02Q96JRTXBAY2DS8ZEXQFXRM6SD43C6BJCBDRCSXXEXA1WG2FFG5CX65TZW94274CMHJNVAY6VR8Y8XKV928P7AHBK106R6TCTS164CG2001",
+ denomPubHash = "0F44ZC9M6XB828XW09DY2YZNM07DA0CZV72D69F31RXH1CQHF3GDPVGZT5135BJEHVX0RGN44SCWS8CR8E39TDSRW10GZ6AEB2R40E8",
+ exchangeBaseUrl = "example.org",
+ feeDeposit = Amount("TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefresh = Amount("TESTKUDOS", fraction = 1000000, value = 0),
+ feeRefund = Amount("TESTKUDOS", fraction = 1000000, value = 0),
+ feeWithdraw = Amount("TESTKUDOS", fraction = 1000000, value = 0),
+ isOffered = true,
+ isRevoked = false,
+ masterSig = "8DGF50VWCB2JHM613NRJM0NQYAVWFXCSBAHKTG3ZHKQ7N59WXK7MA4V794A97YDG0AVP350B2NT1QQ66VS1H2QG3P29PM5QDE6ZH60R",
+ stampExpireDeposit = Timestamp(1656768984000),
+ stampExpireLegal = Timestamp(1688304984000),
+ stampExpireWithdraw = Timestamp(1594301784000),
+ stampStart = Timestamp(1593696984000),
+ status = DenominationStatus.VerifiedGood,
+ value = Amount("TESTKUDOS", fraction = 1000000, value = 0)
+ )
+ )
+ )
+ ),
+ meltFee = Amount("TESTKUDOS", fraction = 3000000, value = 0),
+ refreshSessionRecord = RefreshSessionRecord(
+ confirmSig = "97N1DQ1FGXT6SFR4J8H9WXCNG741BE986E3YW59T6R4DZ1FPC3XRFQ9D6BNRHGTAANZQXH4T1KXH61DP8BGHY6SYHZJWVGSQBV0A00G",
+ exchangeBaseUrl = "example.org",
+ hash = "XCSSAYNJ6964PVG393PDEGX0CGGZBNY6YMRDNZYBCTBDJF8ASA0BEA5TK435GG1DCFTTK26SS3V0EQ0DB9XKC9M9PWHTE4SZK9JZPVG",
+ meltCoinPub = "3GSHK9JCTMEAZY7BMC1QQD28ABEJEJJJXHPVF2YXTET97WB81GNG",
+ newDenominationHashes = listOf(
+ "ZVZMM6GEFETH1S71BRKFKB03BTSTA8JZH26Q1PZED78NZ50EVVT0DMDBPDN5EBY90SK4AWT0J8CNPD7NB2TSCFMSKAS5V6DK26GEKT0",
+ "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ "0F44ZC9M6XB828XW09DY2YZNM07DA0CZV72D69F31RXH1CQHF3GDPVGZT5135BJEHVX0RGN44SCWS8CR8E39TDSRW10GZ6AEB2R40E8"
+ ),
+ newDenominations = listOf(
+ "020000YDW0GZQGY9GSKTEESAD1ZS803D576X1HTVJM8CWEBPVGQ4GHMDD5SBQHJ462NPW9FJD437HYW69MJR5N4YABJWYZB6P7CZ7CZR0YD7KY1M7C291BQX4T18DGKCTDEDBMVH2CF4K4NWVA1FFX4AYDB5GRWJ0JBJRRAVQ1ZY8D9PX9TJT9HEVBXAMQQF2ESKG1JX5CXDX7AMY2TG2001",
+ "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ "020000WTGW5YBES6D2CDGZWESS40NQMSS19R07QGQZTARCZ108VQXQKWQW97C6HM2937V05P4NJ7PNYJJS8DARNGDD2ENQ6ANEHK1PMJEJA1R2FBQKQNA463VX53HSW94RRJP631M9PGE7P32ZNM9DA0F1DJFHSSPZ6GPY3K7BRKW0PP3AZKMX475WP00B02XMHNDWGXR0J2NSWV81KG2001",
+ "020000Z46ESSPKMQ5875ZZMX9EZ4ZD7MQ3MPM4R2QJ7K7ADMCK0CJDM7W6GEZF0BQGWHVCMA4CY83NBYKBZYB9AYG293JD7VW7BSQTFH51HK02Q96JRTXBAY2DS8ZEXQFXRM6SD43C6BJCBDRCSXXEXA1WG2FFG5CX65TZW94274CMHJNVAY6VR8Y8XKV928P7AHBK106R6TCTS164CG2001"
+ ),
+ noRevealIndex = null,
+ planchetsForGammas = listOf(
+ listOf(
+ RefreshPlanchetRecord(
+ blindingKey = "G17FNHNV8BA7BFZRG75PPKJSK7FC47GCXF8YKJZ491D115FQEGE0",
+ coinEv =
+ "85BB17VN2XV7TTNKV5MS64JVA7X39TMCSQQQKWFT30SZFJDA4A9F06SGP89GF7ZZBAVB8X3VTT94SKRQ192ENJS4K12715NKHVH11XWVKND063AK16ANGV2P3XCP32Y4GRJGZMZ068EQS623C4CW75VB2KPF45TKEJ3VX4M4GXWMB3H07F8NQDQGMPFCBDS8SG2HXESC8DXVE",
+ privateKey = "972E3RE0GFMMGXHZTYSFD0H3YDYGGBNH80F5GMHPCN7C8NQCH460",
+ publicKey = "BH1Y1VBKNH86RT9FD191MP82W7PAQCA0SN9GPBN23N02M9NBYS50"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "QR15AD5SFG20S9KW70P1MFAGPQC9WK7MSD0VFQXHKPDZTVZ6BZV0",
+ coinEv =
+ "DBKVPSHR1N8F04N1FQ04EPVBBWGETX5835M8Z9P7WF5X6BHBJW8KKNHZVR1JM9TRVZ9J0G5VDK7BQ9ESFKDCZD4PPB05K0MHW0HAJRKCCJ5PTNFXSRVB4V25D48XT031P0M53WDZRTPEK5G1XQ5KMQHZT06A70GE9N5C7SR4RDVA20AZDBBD1MKPR6NB2FMPAP2DR3NJENPWA",
+ privateKey = "AZXBY4QCFSH759DWTDCK1Q5D5JRNH3663JP2RTC3TSSTH3ARV0D0",
+ publicKey = "1C88APBTDQ8A2Q55N1CB3PD91PHYKJHY8QM2CYK0PGRJ0CTK4VPG"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "EP0XT6PDDJFKGFVG739QKNZCWRN05497TCBQERFRBJBB7W7Y2JV0",
+ coinEv =
+ "J8E5HCMEJZ78BP2VDCNA9FVFBDG9QT4A1FDBSAH6ZZJ2HNJNKYKS57ZVRPZ47B1B8J6WV77PXCDP7EZTX5YTJFPGGAT6W6QWYNGRTW1NPE454J0VPJ3DKT19EA10N27P6MTM35HZZ8GJ8CGW06HGRXSZ5HJA3SQFDRZS2XN68PYM28918ERVV9BDVKFBMRK9NDMWMM79EK198",
+ privateKey = "3V1SMYW447SGBHHZMZN8KDB21FYGNT0P2WK7A1XAR4AJYMXZNSGG",
+ publicKey = "SVM02H0CDHXQX2FPR2B31ZZKMT0SQWE2S3VEWSAZDEDVSDDBXW00"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "XSW3C40PHH03SX6G74QJ9CJCS6RFA2Q1V79G4ZS0J1F055ETW5TG",
+ coinEv =
+ "58FBPPVPMZEPMQB3RV4NJV7GA8GH8XYJ2NZ8N0NYG4M95434E1WT5GSGGM54X5X3FVTKKM5HRG28TCSD5ZCMC1V0KV6YTN1VEGQWVEN92M3TV761KWQDEJEZGHFKE7BMWP75KS37PV9SX4VABSEPS5VZCZB4C3RPG36QH2GH366G0AGQNX2F5T80SBS51Q3XA4FG44KJ14D32",
+ privateKey = "8SFAB3CA3JQ0KB18MREYVJYJYQ0BNYS1NCVPKHW7WS3YB4QN898G",
+ publicKey = "4QYR2AQT98J5QNMB44P3ZVFA0WVHJJTWG7WTXRKYJ11X1FYNYDGG"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "Y94ST132M2FTD579X8V9E3SK349CZQ5Z70RZ29VSP8S1PM08ZV30",
+ coinEv =
+ "HF7CMN9F3Q6EG85C10SQ313336MFQTWJ9ZQ85J4EKWX6Y2546ZNQCPMKDRM2E0KZWQFNR57SKE7EHJ4BWVT14582MT1YG0ZA7RC2DX5VGZSWVT3N568TNSN9TPKYD74KMAH2A0SSXY8QNXF9C3TQD0HPQ409FNV0QR4R65PJ0G4AJAJW3EH6W4CMEXRVXSD8DYT81H6KFPR12",
+ privateKey = "CTNN080C67WSNA96SY3ECCGYM017T3SYZXTJ3CTDEC23E1ZCXVSG",
+ publicKey = "R4G1DHWQCB8MS0E5ET8YTC9M9425R6Z146HKM1ADGQJ0Z2QA1YQ0"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "2P1C6Z5RJNWPP2YW4SXGED45AM9H4EM7M063V8HKVSRHA4HGC5A0",
+ coinEv =
+ "3N07NHNBBSZMQP04EPSN3SC8DZXW0DHWGZ6QD7A20EWD8DDJ00PB2CH11KZP404ZXN752X4TMA6D09C459WXYQDBYQJ70J6920MJ63P9XRMKYHEKWRSJEEC3FX487VFMJJ8ZYYBR3C4YRVPADRXFPZ8T7YJ4667TJKCSRQJGS7V2DS953HQ509M9ZHHWRH19XBFPTET3R71ZT",
+ privateKey = "DWNV6AD7WKZMMPG342D1KB9PMH6D8RW2ZE5T0MB07KNA88NR2RF0",
+ publicKey = "21STR11NETSM4VVBZY65NEDNJHXMV27GM1NP2P47DJN4C8AQS21G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "XTRQX3ZCR6MAKY2AJR2KF9C4GJZ0W5HKK71Y0EHG683WBG6VFHQ0",
+ coinEv =
+ "867475JHJNY8BWHQH23CTCF3GEZZ2R1JBFKER7FTFQFNA9RCG4JR8Y89165HCW6GZQFBCWN8DQ91V8G3JH9JG4RPNJ6YZ6K1R2JNN4H99AR6DR96SDQ5N8YZSKFBFKD2QE8E9KAD8K05AVXPPQ4R4NDN45R7YTA346S1PB24GR0SCG7TPS1W59596KN3V1F4G17RGAKQA66T0",
+ privateKey = "T90M7DVZ78WVVDHBGTHW0Y38M88AWM48Q8MEP6WXXK4WGQQBF0C0",
+ publicKey = "K1MHGCPZSQ8HY8T8WJSAXE5DXH5SMXAGTZ8YZYFWE1ZYBYRHPG6G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "BM2NA21HG2A50GFXZQCQKK5Z23J0Y63SNM966M2FHDPM2XBYJND0",
+ coinEv =
+ "GAZ5VA6AD060FE466W5DHJK8E5TBY4MY6TEFD0TZ9BX45CT4S9V5NGV47G26ESW0Q31787GX1QB756J55BNQJY1SASN2942EARZTYCWC9MEA0MAGDFR6S5N6QRNFEACCHE37S4DB2DN4HWKEJP8JM8RADC73KHCBKBVKAEZGYAFD4P99BSER0S9S2ZH7GAWQYZ45YZY7W6C64",
+ privateKey = "GY4ZPR1BJB5YY0273H46GJTS4HXTWEFPZA4KT5E3Y9H12R8TQCN0",
+ publicKey = "9ZXVR8Y0TDT2H57M4XZ0HGG1R78RBSN3KZK2WAH678G09XP2WM6G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "X24KD0GD1GC8JWDBB72R94XJMD8FKR7T2GK1MX8DFJN0PY6EKH20",
+ coinEv =
+ "5GTW52KR7ACEGQ96SB92KMYBCC8XNPCGV9Q8ZS7MT1N27BH1NYM3BRTR8XJWZZ5FQ5KXEMT3FA374BQTZVPS9GTYHX9J3NMRSBJ6XJA6AGZDMW5VDXS3QWK49271WH1CSHMG2ZJ7XM0NJF86EZN7MPXKTV3WPD25AZ0EA4P980XB7DZ8GA04CVET8K0VFFP4KB13N4088KEB6",
+ privateKey = "K1W5XB7SESBYTAPP4PCS9DNYY4Z3HQ0464BW4T15VDZ64GFCXBZG",
+ publicKey = "W88CW2E3Y4Z0VZC8KE9Z4G6H2B8E4BCRGNDPVVD6MF8PNGRHGMH0"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "CF2AE3C5HVQJSP0R7CBBTAYQK1NDTG5DTXGW6D6KKWG3053QMHX0",
+ coinEv =
+ "FWA6FJY0YM5HWA96QQNH2CGTTD0NR6DTN5YNRDZCBHDTE4A3ZJ5793175J6G0QRPTHNKEMPJFSQEVB4QH7MAHTPSXRJC28WEQ9Q9C7H3R5CDAE9SNMB61RXWV1D1JH195E13690RYZP62KC9349Q25JHADQ4Q4JQJE4MNJK19H3T1WHES285NHCP8BEE3MSE4TH567P1CBK58",
+ privateKey = "7ANNBTWEQPXP211W3TEHWTNRFG5ZFSCW393W0HG6N24PJN6XK6PG",
+ publicKey = "2DJ9HFE9HBQ0N8XNEH6BQD7R1H81BJD40Z94FVD016HEHT8JPCRG"
+ )
+ ),
+ listOf(
+ RefreshPlanchetRecord(
+ blindingKey = "J5979NKRJE2PS2NSVS4T4D6D1CSVJF7TBHKXA2FQYEZF6332PV2G",
+ coinEv =
+ "8S4CVYXJNHAQ93Z2ENGKJXNGYEXXMKE89KXH1TVRGVEY9PJ1NGFGENCKWZR1ZFC8W0TFXDGPYEAPHVPJAY0PT99G6GNJKDYV0YC89XGZK4F5HGN92BY7HT04AF2HY1RED8B0KKRKX9RV727XSV5QG9TZFBSAP4Y4HXZKEN1DEBRR2WJFYSZ2MYV50XPK5T7TPPMM30TX4PX88",
+ privateKey = "CJ6QMHX606JYZY9DS5VGPTFKDYYASAD60MFRBZTVEDQQRYX2KX2G",
+ publicKey = "WYYT5DM2QASGBCTSQKVXT5M7MREXV39MDF11HM5JX4NKMNXWY41G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "BJ9N8QGX1VYYRP1RR4WZGE2NMKY1EG3K6636HPY624MHT1FNK1T0",
+ coinEv =
+ "J5XPY46YX3S314FSBJD1MTZXCT87Y9PYKY17TJP5PCJNK2RF5JGMWBJVJQRXZCJCCFPY3FH4R7TD9A3FYB6JF4Y5QNM8Z46M90599H3HP2BSFZM4HW68R5DKPRE11GADR6BAW7ECFYAREDBZJNC5C18BMZWHP5XZ35CZZRWZ4GG1910KXX6A3789KW1028VBC46PHC3M61396",
+ privateKey = "FE0T9EZM72EZA5PX3TQPVNM9RM5NWJX4M9CEVFXW2BF2HF28AJ7G",
+ publicKey = "2Z07PKFFWXX53NXPX76HBF5RYRRM6E41A5KD5484VJ2DB7P64AA0"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "4B9D3MG4TEHDCETMKBAC3XGYW7A4FFWQ8MEHT7V5SXC0BY0AF3AG",
+ coinEv =
+ "CX96G0RTN9N3QFYNKFFWG165XDKYW7FJEMB5K9SKKYB6A9DCK33T34VDDAJBH9VGG6ADXAM2DBVXZYPVKST9VK2KN943A9MRK0HNP8JVEJK84H9CVA4D1Y9N1351V05H2NC8TEDG43CS6RHSD37HX08N3Q03EW887GF2HF97MW23WH9CH2HBBNS9683DH7K5PY6H3MSQZE0S4",
+ privateKey = "ZS5031P7N3T3B52GNYGF7AEQ44HVCV63Y5Z0Q8E9XME6JC4VGQVG",
+ publicKey = "QB9HPVS5HXP0TVCACT7YB8B2Z7T9NZD47NA0XR49ZBCKJ03M7NXG"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "VTT1W5TK7JTTC6NJZJBQQGD6D005JKXXRGCWRG7AQ6Y86D0JPJHG",
+ coinEv =
+ "AGZ8ZKH7HJ2E4QBQVKH0SH94SPZFSQYG1YG0NFSE7T8HB7P6JKPF66ERSFGDH1CYRBJXA0WA1GFPX7WG528NBWXVEAN9HERZZ45XV2F6H4GPTT59F4TK27VK01NRXR93G2NYSS2V3FVVTNNTVB1BE57S5J18J2TD67RMYWWFCG0S1JPXMN196QBKBMB2KQNFP05G9WBPGH4ZT",
+ privateKey = "MXXTAHN3VWEPD3327H55S8F4JMZHBFFDDV9FBK4SQH8SR0TRRWG0",
+ publicKey = "XZ4VZMRZMZ32Q3DPPSPFHST8D41MX5YFT6BSF51C0D7M30M16NW0"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "047V0KJ23HSGECQRR4MF8DRN3XT3HTHZ0QV585PBJ4HG75CYCPK0",
+ coinEv =
+ "DR1K7Y41WADGP405ZZTCPAWBS6QS9WZEW6VX3JGK1KHYNWZKREKA375TXZM73X558449PR805JCECYQNPCQHZ7HG7FNBT8DEDRRNP1JKJX0HPVTD155K1ZNVWWP2YY9THNG8729MKY3Q77CD7NN8MENJYH9E5RA4FXQYYEGQDDPG2YS0TM595HNA2KX1TD7KYYJ86AGT7FX7M",
+ privateKey = "C5M9VGNMQ2RM92DA92417HA5WGBTP2R2DPZWB6E5MA61S2TNX7W0",
+ publicKey = "JR43Q0EGAH85B2F1VQDGHZ33318FGB44158TC0ZB4YP8XF1N3KYG"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "B4DSQ4BZP3B1QPG21ME2R3BFE0DPX4GRSAVHF3S0V1YEG0JJX620",
+ coinEv =
+ "GAH89K67PPPNXZMC8Q7KEA9VHXDV1WB6WHSJXWB0852B4G5FECGAG9W36NV13AYMX7N37Y5X0NZ3WWKJHFKY8R328EEG528KPFV7E7NCJYZZB83PA6DDP5ZVAQF0N79WDANNEXRZEFT1H8CW0ZFK7533TR7FRTCDT9EWRVBDRJ23NX45C6CQVA6CK1JN9P0ZXP69FPJJAYHHE",
+ privateKey = "A0WS0Z3C9RGJNX9TX5QV4KYYVCTVNMW4N3FD10W97X6AF28KVB10",
+ publicKey = "B5MBRBBTF6NR53QPFJE5X8WX8GG9Z4TEDG2PCPPVNNB6XHHBNRF0"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "ZTQHYH1RPX4XEJV6HG50EQZF6H6M74AY75N3DN4G81ENRS38AM40",
+ coinEv =
+ "FKH1C38XNYGYXK0RMMCVJHJAM60JA0M7T0XBVNV0K580M4VEW6SCR528BV65C7J3XYEZHCGMC1822J4AQXT5KS492KNB8G6MCJYJ95FMRD6R47PGWXSBH35JS95WSKE72FSB5W3Y12D6K50D5H0B7SKDQWAK4VZXNHC1S1X01KHV6KVZCTJW763K68ZABAD9R2TQYB686GE8W",
+ privateKey = "AQZAJRZV4R2FK1ASYEXNJ9E1K3QV0PHSBZNNPZGK2TR2QSBW4XPG",
+ publicKey = "N53YEANM052FMJZQDJGWGWERVB9X67VDRMA67X31Q7HKXFMJTJVG"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "GHJ6XF1XWYY623QDYNDXKTZ0PRSHAN41H8SHA4RGD1HCTV9PV570",
+ coinEv =
+ "3ZP6QZ4X6RH1R7C60T51P2HQEYZ4RMFG45MN47J6NQ6YC1M9M2BAFAQBM87JD8T1M69002E5QD2QZDV8HDJ492CT107TWZCE4TQ367GDSTPT6AAFS0JRSDJPBNME5KD6ZH8Q42KNMQ4PCM7GWA2ZQK7V35Z7BDJGWRFF65QGEPBC3F8PKVC92KJ8AN89ANY9H4YM55JFDCRSE",
+ privateKey = "T7BME1SM7Q6YEKNS6CCEGA7Q54SB1D451H1TJYYNGV6393R2QKGG",
+ publicKey = "QBTQNQKCGS6CH2W6AQSDSA7M1EFF0DC5Q1AE4MK2BQM0EBG3RJQ0"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "1KFVREMW5KJZ3YS65MKCM40GNTCYBREPN7KM26YTVDW0GRPSDYTG",
+ coinEv =
+ "K08B7KDNK8C179M042GZ6BYGRNEK2M3YKTR25FZQ1H9AF9EG9GYFEP6Z2KZ3FH1SEPWYWHNQM1N3507PG99B8XWMR1ZG8BR1CFFM2EQFB5W8SVF145NS7SYXSDPMJ7A64NJZEQMZH0VS2XY262M3BDMVWP7FDEG3W1XN4V0S6GXA14GWRNKFG5MYX8RBPFC1ZHPNFHRGD1FT2",
+ privateKey = "0Q8W768BQYAPSQCD46R3GMH7QAVT6ZJ0T5YA2A3WXXGRGX7X2WGG",
+ publicKey = "AK1YSV2A9Y61E1XV313KXR1CJY81RZDKZW4TK9CZ82KK7YFJQDY0"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "M2FEDXEYNBXQMMWYR36QY9TSVFYW910947RKFBN4AQ3GNZW7565G",
+ coinEv =
+ "3R65GCTT7EB685EGVXCJE928KPX1HH8YNHKD3JYYRJ26PD066GG21ZEE5HXSFX9D22NH31KMK4SPKB9QKNAWK539BYSP58ZZ9W011FYTGMKMDB8CMA354JMH03B814Y4MM64EE5BA1BDJ5BDZ0X6CT5BZ51QMD8NBZTKR1SE6YF1YZ6RJDQAG12E312F2VRZNYKXHGXNDSR1J",
+ privateKey = "V6XNMA56RBR8VRK0GG6Z76STRA6QMNSJZYB15SFS7XX1QPFNN6KG",
+ publicKey = "KM59YNG5ZZH9RHCN2XF4MY9YEGXC4A7HHN907FGG9C4W6ACFNY20"
+ )
+ ),
+ listOf(
+ RefreshPlanchetRecord(
+ blindingKey = "X84BZ3AZ0KMG3BYVRT3YT04ESCB7R3YEWVBZN5ZDDZ6WN00AEPR0",
+ coinEv =
+ "3B5ZHJ3NESVQ31QEBJXAGF6B5SJBEMXX5Q2FDTHRW807FDVQ3Y7T7KVQ5JSMB1DA3EQCQDNJYVZDQQQDHHHDR5TDEZZDJHX9S94KEKTRMNJDPFXSYABCCMWR7R4H1R7W6TM5Q4B5H2FYZXAVCAJJMC13ZK53N83SRRF8EVAJ2ZB51D6X7WPWQ1MQX3DPTXVZV4EGGPMYDP720",
+ privateKey = "FFZ1E4KGF8Z6HHYGQC1QR6RJFDQ3QRPCDM58NMK6YTFTSK6GTA9G",
+ publicKey = "Y7F33NG04MEFAK0QCBAHG325705B1326HCJ9WFXQCZVKMJJ8ZE30"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "E3QTYEC6NNZBQ6QTSX7TJ50CQGM3QCBABVPDSZANHFJNX8CW2Q90",
+ coinEv =
+ "6NE9SY9SZXTCJP4SW01T5R3YYZFCEG7AR5VJ4JNVQ9WPB7A1DGVXDZQ68233KSG5GEBV4W4T74CK5AC2MDKST7RCJVFSVP1B4EETSDVNWSJ8VF2V7BY8W9S85Z06WJE223PW692CEE4JA0Y83Z40Z69P28NS430BX9VMWRXMZMCH9FKB92TCVZGTXM3CJ2ZG5RRPNZB60VKKT",
+ privateKey = "ABAF6S5WZMZZ1ZWXJG7JVEQTMJB5ZBRR4T554Q0SNDZDRFG9MNMG",
+ publicKey = "HFVN1JANX4A4DSB4K458255GC5S879QTSBF2DDEM2K0HPZQPHF0G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "VRWF9B40EHKB4NZ6FXQG1P26XESD7FZXSEA9X53DZ1WVXS22GZAG",
+ coinEv =
+ "EWSVHPMDEBPW256A7K20TSW2T9EBQNKCHQWRJ1HJ85NW9K058H9C5C8QZ1R2ZEKDRYSEMPM920HXSE863BG8VKJHDR1ADQFREWH6TA2QA2ZANSMNJDHCA1EMG78A9PSMZCYE8Y82SY70MJSKZG9ZF0F5EG3KJ14X21RC6HY41P79NVETG4R11GF7MADSTF11G5CNB8EX26RN8",
+ privateKey = "DKHBJ9SRVEGAD7Q6K7PMH8DMA8WSRCK31PEAD6NWA4T6QRCMBW60",
+ publicKey = "DMVWT6WPYWHHX4G0H124HDQ6QDA1T447WQQW9NVR1ABJZ31E3YA0"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "7HJN1EJK12M3JF7H6J6A82X9VB0VWCAC5MW4DA0W7524ZA620F3G",
+ coinEv =
+ "5YFPXNJ0SKNBJ6ATMD4JAPCMDFQD25RWYYQZXM7X6W8V9VBVBRT4M94MTMSE6W5H56XMCT5MF51MJVM9W090223R5VWTDQ9KEAHT45VPASHXNJ817R1A2S50F7TZTDHKE3ZKPJ1ZSTC9XDS57N0JGZ4AK3606PZ5211G8BDWSAGY8N0JYNASZRZ9JABN4HNTKRXRYZKN0V240",
+ privateKey = "AXZTP48HT58DB4BFKREZ34ARBVREC1BECK0KEYBWFWVP7K17ZQG0",
+ publicKey = "HP8PY0NPT0NV32RNMEYZ63YSDYB2T9J64KXAYV76M1N9Z4GPF8YG"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "KK0VT0E3KMHG39VT0B5NNTEXC9HMT9HJ6554THEVWHPHS96H4FS0",
+ coinEv =
+ "EMNAXG036TG4JNZ2TWBE688THQ1BZW8H5PCYMD4Z71K5FHSB6N8QE54V03RK0GJQVCDYPH699PPR5HDSWCBV13YG6WX9DX097VPMKTHSD7CYGH611RGDJ3KJXJQXQCDYXC869JZ9ZTBNGYGVZDVP16141Q32ARRF59T0MDKR6C4BJQ1V32Y2ZWHRD5QVRBAYS90KPDW6G00D6",
+ privateKey = "4NFWW24DP7V17ZN4D12NW6TENEMF54XZMNB4XVXM1Q31AMC8DKHG",
+ publicKey = "A7F9SY8413B3EQSVCES7AFDYRJH768J8AA8DVDR4BR06SHXT0W1G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "1KB9TCM2GDT61ARG99NA5TCG35H3P4K6ZGXEQA33P3XNQPSX33G0",
+ coinEv =
+ "1MRTJBESYQ1MSJC117JHRT90GS7WPCF5ACA198RGV4X0E464W2KMPFS4K43Z94NJ8QKP40YZCRX22ZR23MMCFWGNS5KYDV57JH4VZM6YSDVCRDGCBQDB87BN9TS2PXK8GXK3JA3G84X5HBGY7CX870B3MWBHW0V6FHMQNDXVFP1PCYZT0T0GAPPQ02MZ6Q8JGMSXT0QFXF9P4",
+ privateKey = "289K9AW8M3N45RZZNGEYBFHZEGCTKRY9TB15F5SNVAQ09BHKHEWG",
+ publicKey = "BZFET2N4MFYPQSMDASD7DJHA0EC8KGSB8RS3G34G8B5D6S9W1B90"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "B28XC87F006EE1B0GRMBEW3P88J324C7BYPS3SCVHAB3KRA7JBM0",
+ coinEv =
+ "8S771KDCPD9XFRXDT7938T8HX2XNGM7FH3WJGTYBD343GY5E427ZPD0QA4XAJYWN9038K0BH6Y0R2G42T8PCAM4W0YP31FPCABPZXPCXZP52ZDB6QR3ZG01KS9GBWAJ4PPKJG24MREBVTX37R90WA44G7Y8G5FCDBEQR1TZ3PZ0Q4C3K0Z3QP1HNNEH3WEP2QPFJD82YFF1G2",
+ privateKey = "592P3HWA2D245M1ZHA4AMSRK6Y5A9YFVP70VAKBAF6GSVVZE7MW0",
+ publicKey = "VS7N76T06X9GPEPGF1K7CD3ZTCJ4R8DJW715NRE30HT92JXQ076G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "9VQATS7SQ9QRM19602PN3QF16JEF9FM8GT1BZC8WRM9Y6HRV9HMG",
+ coinEv =
+ "CMZK656TCB6BRDRXVZ625SYZTGVKX56J77NE9B6T0EHRM8DRD7MR5WDRWQV8Z5WWDTAGKPCPA3XHCTGW5T156KK9RM340N53VPYJTDXH22CKY68PY8J4J40XG40YZW3F6TREE5TRFF51VJT7QV8CHW9PM67ZK2ZZWGVXV3KJF2HVDYFS2X8BHC19B266H5BK5EX6NJ65E9ADG",
+ privateKey = "4QVMP2V1FJQKTCTVXFBDCCR29T3BG40RCFZCJDPG6WPAS89EWWF0",
+ publicKey = "MV5RPV33Z2EFXSA6XPV0GVP2WBAP580JXAB2WD0G86FM29VM2X9G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "ZTKWFCA58WC7GPYDYBMAXCH12KPTRGTXR2WMYSX8HPSAZ4M47480",
+ coinEv =
+ "7Z9T151XYF58P4T8B7M3DS8VYFS7MXQPYG40V1JZ0SXXFNVQ1E6GX6C64F36FQADNPDB7S4YDNR80TGCACPXD5ZEP7JQ514Z1ZS43D739BBGPWH5QHGGDN91V2KDSDD8AS4V2M56CF26G0RBVKV8EZTM6ATQCQBVJE6Y4S98VP31WECYSW8RAYKHMHWKN5DHF1NMZMY86KA5E",
+ privateKey = "M87WJ5ECSYVVGK0PDTWPJCQS5ZE27D79RAZVGNDB8HWGC4R0X1G0",
+ publicKey = "7ANMTX5E74K8074Z50E36D8QX5J9TQ6B7WPN7XK3DPV4YFS1DX7G"
+ ),
+ RefreshPlanchetRecord(
+ blindingKey = "2522XN7JAFCVXSFMG031CSA20DFMY2MF5ZTZT6W3HP59RPW5S34G",
+ coinEv =
+ "5R0DF70BTV34K5X8DQG8Q1XY55RXCRQ8KF2WK12QP90DBY2QRWF5CJDE20X5BFWD5YKWVMKHV8QWE6SFGSHDBSJADYKA7WZXGBJ2EJPAYXEHMTTJ89KH60KZME1SDBBYZA239M98JV56ZAMZPNMG928Y4P27F5BPBSGQK3E3A700BDNW1TTJ5VYPXN8SNJANW74YFPJZN2HWY",
+ privateKey = "ZAX0KQPGATJT5EMWP7D6G4TB8GH6ZS6C17PJ91FS4F4VWX16H26G",
+ publicKey = "DNYGRGC1R41AH28FY063N71RDYPFYMDB20N491R8297R33G72FE0"
+ )
+ )
+ ),
+ transferPrivateKeys = listOf(
+ "KXR71TDT7A5GBZ6D92PWYC7PY9Q18KMVC4F4VRNYZ4NQ2V6DDY50",
+ "DWRWHG4NN81SWFYVJ1B1EKN9RSXRBJYV06Z8SQWWWTN6W6TRPCEG",
+ "3ZSZTTF3S1BG4MDW1GF8PE5EX2A2WD6WBTR8SYSD6GZBS4D4Z780"
+ ),
+ transferPublicKeys = listOf(
+ "2KQHHDF90PETJ852EX67W441PGZ5M47ZRD8XT0DY0HGY5Y1XWXSG",
+ "ANNVKXH143SB5EXDMS9XN2NXV0ETVA7KFJPS94Y6356H16K0JH5G",
+ "JB60PMEWBJ0Z51QGBHNHVQ51V8FT0C97QX64DFSK8SXEQNSYV1EG"
+ ),
+ amountRefreshOutput = Amount("TESTKUDOS", value = 1, fraction = 81000000),
+ amountRefreshInput = Amount("TESTKUDOS", value = 1, fraction = 95000000),
+ timestampCreated = Timestamp(1593697014952),
+ finishedTimestamp = null
+ ),
+ kappaKeys = listOf(
+ EcdheKeyPair(
+ publicKey = Base32Crockford.decode("2KQHHDF90PETJ852EX67W441PGZ5M47ZRD8XT0DY0HGY5Y1XWXSG"),
+ privateKey = Base32Crockford.decode("KXR71TDT7A5GBZ6D92PWYC7PY9Q18KMVC4F4VRNYZ4NQ2V6DDY50")
+ ),
+ EcdheKeyPair(
+ publicKey = Base32Crockford.decode("ANNVKXH143SB5EXDMS9XN2NXV0ETVA7KFJPS94Y6356H16K0JH5G"),
+ privateKey = Base32Crockford.decode("DWRWHG4NN81SWFYVJ1B1EKN9RSXRBJYV06Z8SQWWWTN6W6TRPCEG")
+ ),
+ EcdheKeyPair(
+ publicKey = Base32Crockford.decode("JB60PMEWBJ0Z51QGBHNHVQ51V8FT0C97QX64DFSK8SXEQNSYV1EG"),
+ privateKey = Base32Crockford.decode("3ZSZTTF3S1BG4MDW1GF8PE5EX2A2WD6WBTR8SYSD6GZBS4D4Z780")
+ )
+ )
+ )
+ )
+ for (v in vectors) testRefreshVector(v)
+ }
+
+ private fun testRefreshVector(v: RefreshVector) {
+ val record =
+ refresh.createRefreshSession("example.org", v.meltCoin, v.meltFee, v.newCoinDenominations, v.kappa) {
+ v.kappaKeys[it]
+ }
+ // use expected timestamp, so we don't need to mock system clock
+ assertTrue(v.refreshSessionRecord.timestampCreated.ms < record.timestampCreated.ms)
+ assertEquals(v.refreshSessionRecord, record.copy(timestampCreated = v.refreshSessionRecord.timestampCreated))
+ }
+
+ private class SignCoinLinkVector(
+ val oldCoinPrivateKey: String,
+ val newDenomHash: String,
+ val oldCoinPublicKey: String,
+ val transferPublicKey: String,
+ val coinEv: String,
+ val signature: String
+ )
+
+ @Test
+ fun testSignCoinLink() {
+ val vectors = listOf(
+ SignCoinLinkVector(
+ oldCoinPrivateKey = "884XH9DBQT9MA34YKCR7WPEP2HH3J1R9C3G7MSB8C3NHB0XEVZW0",
+ newDenomHash = "ZVZMM6GEFETH1S71BRKFKB03BTSTA8JZH26Q1PZED78NZ50EVVT0DMDBPDN5EBY90SK4AWT0J8CNPD7NB2TSCFMSKAS5V6DK26GEKT0",
+ oldCoinPublicKey = "3GSHK9JCTMEAZY7BMC1QQD28ABEJEJJJXHPVF2YXTET97WB81GNG",
+ transferPublicKey = "ANNVKXH143SB5EXDMS9XN2NXV0ETVA7KFJPS94Y6356H16K0JH5G",
+ coinEv = "8S4CVYXJNHAQ93Z2ENGKJXNGYEXXMKE89KXH1TVRGVEY9PJ1NGFGENCKWZR1ZFC8W0TFXDGPYEAPHVPJAY0PT99G6GNJKDYV0YC89XGZK4F5HGN92BY7HT04AF2HY1RED8B0KKRKX9RV727XSV5QG9TZFBSAP4Y4HXZKEN1DEBRR2WJFYSZ2MYV50XPK5T7TPPMM30TX4PX88",
+ signature = "H1V8D30JTVW8F7KERCWRDHPVVV6BZXNE0QG4V7ANEPTVFT2NKEWCCRM8NSG6D6MBA80ZSSYG1KYZXR8CWG0HV6N7QA48C4YEWK4M030"
+ ),
+ SignCoinLinkVector(
+ oldCoinPrivateKey = "884XH9DBQT9MA34YKCR7WPEP2HH3J1R9C3G7MSB8C3NHB0XEVZW0",
+ newDenomHash = "4VEW03Q1JRQTKDGDKV73DX4RYHPDMPYQ48BZWAFVFPB03XK9HNFKWM1KZCR0BHXGS0034W12CE1VG1J2YEN4G7C1400MGX35GDS263R",
+ oldCoinPublicKey = "3GSHK9JCTMEAZY7BMC1QQD28ABEJEJJJXHPVF2YXTET97WB81GNG",
+ transferPublicKey = "ANNVKXH143SB5EXDMS9XN2NXV0ETVA7KFJPS94Y6356H16K0JH5G",
+ coinEv = "CX96G0RTN9N3QFYNKFFWG165XDKYW7FJEMB5K9SKKYB6A9DCK33T34VDDAJBH9VGG6ADXAM2DBVXZYPVKST9VK2KN943A9MRK0HNP8JVEJK84H9CVA4D1Y9N1351V05H2NC8TEDG43CS6RHSD37HX08N3Q03EW887GF2HF97MW23WH9CH2HBBNS9683DH7K5PY6H3MSQZE0S4",
+ signature = "90NEPFDDK99524R9RZ3H4NEK6S5N71CQN6DAXJ9S87YASHB0JYG3VJKSV7BVJVCRCHWDYYS61CXQ4XWW9DMS6HAV0PAPWEY7FPKB430"
+ ),
+ SignCoinLinkVector(
+ oldCoinPrivateKey = "8G7F1X5EQV9P7JK1YVY5MFYX18Z85FRQFCKFQ9J5HX8B03QEAR80",
+ newDenomHash = "HDYHXFJ65YSW7BEVCP1G282E4QHQ9KSF8KBQNM4JN4T3KD30YVN2G6G1HFWEMSHJGAW1VM6N8HDEYRXVYJ3A16C58WM6QATMENGJ5CR",
+ oldCoinPublicKey = "4670V0PA3JG928CJFQHGJAC3DKCPJR3HMRRYHKAHTEEGQDK78HAG",
+ transferPublicKey = "BH4PSJF3T0S9QNFARDXTBWXWT46AX5CH9JENP5D1HQVTNK9YXN7G",
+ coinEv = "PDGXW6PDZTFEGMYFCVW972V4WR9WXTYW6JBDCW52DCEPCECBK5AX8R276Q5JZ38ABQ0X0E8NSSV28H56CX3MR3YTXFAJKHY9M93EECWDCK6AA4259M6DYSS1D4AMQ2PQJH3Q0BFFXDS9X5Z6VKFM0EH05KM91SBB6679NC2SF0B4A1G2N5KTQEXCQADNJKHGGZJ704PBPYCGR",
+ signature = "7STWC1GZ6WWYBYWBNA1BDKFYAX8TCRGFP36A3W0TH72K51R9DXMVRA2H6N21CSA40Z3SQJ0NCFBNTR0781PX3027QX7KZ9KC7Q3SW20"
+ ),
+ SignCoinLinkVector(
+ oldCoinPrivateKey = "8G7F1X5EQV9P7JK1YVY5MFYX18Z85FRQFCKFQ9J5HX8B03QEAR80",
+ newDenomHash = "Y7N6C15W0ADR06Q2QKK15VWKV09FMHC0BHT0ZZ95FP8T7G7T2GCYWVMYRE605QCP7NQ02QYH3Y9V834G37K8KT7V3AY75N715AYX170",
+ oldCoinPublicKey = "4670V0PA3JG928CJFQHGJAC3DKCPJR3HMRRYHKAHTEEGQDK78HAG",
+ transferPublicKey = "BH4PSJF3T0S9QNFARDXTBWXWT46AX5CH9JENP5D1HQVTNK9YXN7G",
+ coinEv = "63X0T7DWHJHMKR7GX4D9CTNHVMGRKBN4C2VHBSNY6W8JMJYJNDD40XZXG9RB7V4W7HHJ605XK6R9VSBMJ8WCF5AQ3A0631F6MFTSEKTKJDMMP8006DGZ0XMW43JD0PD2XXX42HPAD6DY0ST941NYKNMA050ZMTV1JKNYW9495RVXQCMKPJ759KCC4BA6066E17Q7AVNS6EJD8",
+ signature = "5NVY7F9ZBSHY0JP1Q8JFTJYCY63QRT2JNV3R25050451ACYDXZ4YT0CE3TQYAECMVZ3F092GN7TQFNJF3G24JZN6T7XHDDG4MT0M82R"
+ )
+ )
+ for (v in vectors) testSignCoinLinkVector(v)
+ }
+
+ private fun testSignCoinLinkVector(v: SignCoinLinkVector) {
+ val signature =
+ refresh.signCoinLink(v.oldCoinPrivateKey, v.newDenomHash, v.oldCoinPublicKey, v.transferPublicKey, v.coinEv)
+ assertEquals(v.signature, signature)
+ }
+
+}
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt b/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt
index fbd6c65..c0efbac 100644
--- a/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/Timestamp.kt
@@ -1,5 +1,6 @@
package net.taler.wallet.kotlin
+import com.soywiz.klock.DateTime
import net.taler.wallet.kotlin.crypto.CryptoImpl.Companion.toByteArray
@@ -10,6 +11,7 @@ class Timestamp(
companion object {
const val NEVER: Long = -1
+ fun now(): Timestamp = Timestamp(DateTime.now().unixMillisLong)
}
/**
diff --git a/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Refresh.kt b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Refresh.kt
new file mode 100644
index 0000000..4ffb3b9
--- /dev/null
+++ b/src/commonMain/kotlin/net/taler/wallet/kotlin/crypto/Refresh.kt
@@ -0,0 +1,256 @@
+package net.taler.wallet.kotlin.crypto
+
+import net.taler.wallet.kotlin.Amount
+import net.taler.wallet.kotlin.Base32Crockford
+import net.taler.wallet.kotlin.CoinRecord
+import net.taler.wallet.kotlin.DenominationRecord
+import net.taler.wallet.kotlin.Timestamp
+import net.taler.wallet.kotlin.crypto.Signature.Companion.WALLET_COIN_LINK
+import net.taler.wallet.kotlin.crypto.Signature.Companion.WALLET_COIN_MELT
+import net.taler.wallet.kotlin.crypto.Signature.PurposeBuilder
+
+internal class Refresh(private val crypto: Crypto) {
+
+ data class RefreshSessionRecord(
+
+ /**
+ * Public key that's being melted in this session.
+ */
+ val meltCoinPub: String,
+
+ /**
+ * How much of the coin's value is melted away with this refresh session?
+ */
+ val amountRefreshInput: Amount,
+
+ /**
+ * Sum of the value of denominations we want to withdraw in this session, without fees.
+ */
+ val amountRefreshOutput: Amount,
+
+ /**
+ * Signature to confirm the melting.
+ */
+ val confirmSig: String,
+
+ /**
+ * Hashed denominations of the newly requested coins.
+ */
+ val newDenominationHashes: List<String>,
+
+ /**
+ * Denominations of the newly requested coins.
+ */
+ val newDenominations: List<String>,
+
+ /**
+ * Planchets for each cut-and-choose instance.
+ */
+ val planchetsForGammas: List<List<RefreshPlanchetRecord>>,
+
+ /**
+ * The transfer public keys, kappa of them.
+ */
+ val transferPublicKeys: List<String>,
+
+ /**
+ * Private keys for the transfer public keys.
+ */
+ val transferPrivateKeys: List<String>,
+
+ /**
+ * The no-reveal-index after we've done the melting.
+ */
+ val noRevealIndex: Int?,
+
+ /**
+ * Hash of the session.
+ */
+ val hash: String,
+
+ /**
+ * Timestamp when the refresh session finished.
+ */
+ val finishedTimestamp: Timestamp?,
+
+ /**
+ * When was this refresh session created?
+ */
+ val timestampCreated: Timestamp,
+
+ /**
+ * Base URL for the exchange we're doing the refresh with.
+ */
+ val exchangeBaseUrl: String
+ )
+
+ data class RefreshPlanchetRecord(
+ /**
+ * Public key for the coin.
+ */
+ val publicKey: String,
+ /**
+ * Private key for the coin.
+ */
+ val privateKey: String,
+ /**
+ * Blinded public key.
+ */
+ val coinEv: String,
+ /**
+ * Blinding key used.
+ */
+ val blindingKey: String
+ )
+
+ data class DenominationSelectionInfo(
+ val totalCoinValue: Amount,
+ val totalWithdrawCost: Amount,
+ val selectedDenominations: List<SelectedDenomination>
+ )
+
+ data class SelectedDenomination(val count: Int, val denominationRecord: DenominationRecord)
+
+ /**
+ * Create a new refresh session.
+ */
+ fun createRefreshSession(
+ exchangeBaseUrl: String,
+ meltCoin: CoinRecord,
+ meltFee: Amount,
+ newCoinDenominations: DenominationSelectionInfo,
+ kappa: Int = newCoinDenominations.selectedDenominations.size
+ ) : RefreshSessionRecord {
+ return createRefreshSession(exchangeBaseUrl, meltCoin, meltFee, newCoinDenominations, kappa) {
+ crypto.createEcdheKeyPair()
+ }
+ }
+
+ /**
+ * Create a new refresh session and allow to provide transfer key pairs for testing.
+ */
+ fun createRefreshSession(
+ exchangeBaseUrl: String,
+ meltCoin: CoinRecord,
+ meltFee: Amount,
+ newCoinDenominations: DenominationSelectionInfo,
+ kappa: Int = newCoinDenominations.selectedDenominations.size,
+ kappaKeys: (Int) -> EcdheKeyPair
+ ): RefreshSessionRecord {
+ val sessionHashState = crypto.getHashSha512State()
+
+ // create fresh transfer keys, one pair for each selected denomination (kappa-many)
+ val transferPublicKeys = ArrayList<String>()
+ val transferPrivateKeys = ArrayList<String>()
+ for (i in 0 until kappa) {
+ val transferKeyPair = kappaKeys(i)
+ sessionHashState.update(transferKeyPair.publicKey)
+ transferPrivateKeys.add(Base32Crockford.encode(transferKeyPair.privateKey))
+ transferPublicKeys.add(Base32Crockford.encode(transferKeyPair.publicKey))
+ }
+
+ // add denomination public keys to session hash
+ val newDenominations = ArrayList<String>()
+ val newDenominationHashes = ArrayList<String>()
+ for (selectedDenomination in newCoinDenominations.selectedDenominations) {
+ for (i in 0 until selectedDenomination.count) {
+ newDenominations.add(selectedDenomination.denominationRecord.denomPub)
+ newDenominationHashes.add(selectedDenomination.denominationRecord.denomPubHash)
+ sessionHashState.update(Base32Crockford.decode(selectedDenomination.denominationRecord.denomPub))
+ }
+ }
+
+ // add public key of melted coin to session hash
+ sessionHashState.update(Base32Crockford.decode(meltCoin.coinPub))
+
+ // calculate total value with all fees and add to session hash
+ val (totalOutput, withdrawFee) = calculateOutputAndWithdrawFee(newCoinDenominations.selectedDenominations)
+ val valueWithFee = totalOutput + withdrawFee + meltFee
+ sessionHashState.update(valueWithFee.toByteArray())
+
+ val planchetsForGammas = ArrayList<ArrayList<RefreshPlanchetRecord>>()
+ for (i in 0 until kappa) {
+ val planchets = ArrayList<RefreshPlanchetRecord>()
+ for (selectedDenomination in newCoinDenominations.selectedDenominations) {
+ for (k in 0 until selectedDenomination.count) {
+ val coinNumber = planchets.size
+ val transferPrivateKey = Base32Crockford.decode(transferPrivateKeys[i])
+ val oldCoinPub = Base32Crockford.decode(meltCoin.coinPub)
+ val transferSecret = crypto.keyExchangeEcdheEddsa(transferPrivateKey, oldCoinPub)
+ val fresh = crypto.setupRefreshPlanchet(transferSecret, coinNumber)
+ val publicKeyHash = crypto.sha512(fresh.coinPublicKey)
+ val denominationPub = Base32Crockford.decode(selectedDenomination.denominationRecord.denomPub)
+ val ev = crypto.rsaBlind(publicKeyHash, fresh.bks, denominationPub)
+ val planchet = RefreshPlanchetRecord(
+ blindingKey = Base32Crockford.encode(fresh.bks),
+ coinEv = Base32Crockford.encode(ev),
+ privateKey = Base32Crockford.encode(fresh.coinPrivateKey),
+ publicKey = Base32Crockford.encode(fresh.coinPublicKey)
+ )
+ planchets.add(planchet)
+ sessionHashState.update(ev)
+ }
+ }
+ planchetsForGammas.add(planchets)
+ }
+
+ val sessionHash = sessionHashState.final()
+
+ // make a signature over sessionHash, value (again?), meltFee and meltCoin public key with meltCoin private key
+ val confirmData = PurposeBuilder(WALLET_COIN_MELT)
+ .put(sessionHash)
+ .put(valueWithFee.toByteArray())
+ .put(meltFee.toByteArray())
+ .put(Base32Crockford.decode(meltCoin.coinPub))
+ .build()
+ val confirmSignature = crypto.eddsaSign(confirmData, Base32Crockford.decode(meltCoin.coinPriv))
+
+ return RefreshSessionRecord(
+ confirmSig = Base32Crockford.encode(confirmSignature),
+ exchangeBaseUrl = exchangeBaseUrl,
+ hash = Base32Crockford.encode(sessionHash),
+ meltCoinPub = meltCoin.coinPub,
+ newDenominationHashes = newDenominationHashes,
+ newDenominations = newDenominations,
+ noRevealIndex = null,
+ planchetsForGammas = planchetsForGammas,
+ transferPrivateKeys = transferPrivateKeys,
+ transferPublicKeys = transferPublicKeys,
+ amountRefreshOutput = totalOutput,
+ amountRefreshInput = valueWithFee,
+ timestampCreated = Timestamp.now(),
+ finishedTimestamp = null
+ )
+ }
+
+ private fun calculateOutputAndWithdrawFee(selectedDenomination: List<SelectedDenomination>): Pair<Amount, Amount> {
+ val currency = selectedDenomination[0].denominationRecord.value.currency
+ var total = Amount.zero(currency)
+ var fee = Amount.zero(currency)
+ for (ncd in selectedDenomination) {
+ total += ncd.denominationRecord.value * ncd.count
+ fee += ncd.denominationRecord.feeWithdraw * ncd.count
+ }
+ return Pair(total, fee)
+ }
+
+ fun signCoinLink(
+ oldCoinPrivateKey: String,
+ newDenominationHash: String,
+ oldCoinPublicKey: String,
+ transferPublicKey: String,
+ coinEv: String
+ ): String {
+ val coinEvHash = crypto.sha512(Base32Crockford.decode(coinEv))
+ val coinLink = PurposeBuilder(WALLET_COIN_LINK)
+ .put(Base32Crockford.decode(newDenominationHash))
+ .put(Base32Crockford.decode(oldCoinPublicKey))
+ .put(Base32Crockford.decode(transferPublicKey))
+ .put(coinEvHash)
+ .build()
+ val coinPrivateKey = Base32Crockford.decode(oldCoinPrivateKey)
+ val sig = crypto.eddsaSign(coinLink, coinPrivateKey)
+ return Base32Crockford.encode(sig)
+ }
+
+}