Skip to content

Commit

Permalink
Fix keyset ID derivation and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
thunderbiscuit committed Dec 11, 2023
1 parent ebf171d commit 501d8cd
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 24 deletions.
17 changes: 12 additions & 5 deletions lib/src/main/kotlin/me/tb/cashuclient/Wallet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import me.tb.cashuclient.mint.MintRequest
import me.tb.cashuclient.mint.MintResponse
import me.tb.cashuclient.swap.PreSwapBundle
import me.tb.cashuclient.swap.SwapResponse
import me.tb.cashuclient.types.ActiveKeysetsResponse
import me.tb.cashuclient.types.BlindedSignaturesResponse
import me.tb.cashuclient.types.EcashUnit
import me.tb.cashuclient.types.Keyset
Expand Down Expand Up @@ -91,9 +92,12 @@ public class Wallet(
public fun getActiveKeyset(): Unit = runBlocking(Dispatchers.IO) {
val keyset = async {
val client = HttpClient(OkHttp)
val keysetJson = client.get("$mintUrl$ACTIVE_KEYSET_PATH").bodyAsText()
val response = client.get("$mintUrl$ACTIVE_KEYSET_PATH").bodyAsText()
client.close()
Keyset.fromJson(keysetJson)
val mintResponse = Json.decodeFromString(ActiveKeysetsResponse.serializer(), response)

// TODO: I'm not sure why there can be multiple active keysets at the same time. Open issue on specs repo.
mintResponse.keysets.first().toKeyset()
}
addKeyset(keyset.await())
}
Expand All @@ -103,12 +107,15 @@ public class Wallet(
* Query the mint for the [Keyset] associated with a given [KeysetId].
*/
public fun getSpecificKeyset(keysetId: KeysetId): Keyset = runBlocking(Dispatchers.IO) {
val keysetId: String = keysetId.value
val keysetIdHex: String = keysetId.value
val specificKeyset = async {
val client = HttpClient(OkHttp)
val keysetJson = client.get("$mintUrl$SPECIFIC_KEYSET_PATH$keysetId").bodyAsText()
val response = client.get("$mintUrl$SPECIFIC_KEYSET_PATH$keysetIdHex").bodyAsText()
client.close()
Keyset.fromJson(keysetJson)
val mintResponse = Json.decodeFromString(ActiveKeysetsResponse.serializer(), response)

// TODO: There should only be one keyset in the response it feels odd that the spec requires an array
mintResponse.keysets.first().toKeyset()
}
specificKeyset.await()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package me.tb.cashuclient.types

import kotlinx.serialization.Serializable

/**
* The data structure the mint returns when we ask for the active keysets.
*
* @param keysets A list of serializable [KeysetJson] objects.
*/
@Serializable
public data class ActiveKeysetsResponse(
public val keysets: List<KeysetJson>
)
2 changes: 1 addition & 1 deletion lib/src/main/kotlin/me/tb/cashuclient/types/EcashUnit.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package me.tb.cashuclient.types

public enum class EcashUnit {
Satoshi,
SATOSHI,
}
51 changes: 42 additions & 9 deletions lib/src/main/kotlin/me/tb/cashuclient/types/Keyset.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import fr.acinq.bitcoin.PublicKey
import fr.acinq.bitcoin.crypto.Digest
import kotlinx.serialization.Serializable
import java.util.SortedMap

/**
Expand All @@ -17,32 +18,33 @@ import java.util.SortedMap
*
* @param keyset A map of token values to public keys.
*/
public class Keyset(keyset: Map<ULong, PublicKey>) {
public class Keyset(
keyset: Map<ULong, PublicKey>,
) {
init {
keyset.forEach { (value, publicKey) ->
require(publicKey.isValid()) { "Invalid public key $publicKey (hex: ${publicKey.toHex()}) for value $value." }
}
}

public val sortedKeyset: SortedMap<ULong, PublicKey> = keyset.toSortedMap()
public val keysetId: KeysetId by lazy {
deriveKeysetId()
}
public val keysetId: KeysetId by lazy { deriveKeysetId() }
public val unit: EcashUnit = EcashUnit.SATOSHI

/**
* Derive the [KeysetId] for a given [Keyset]. Currently only derives 0x00 version keyset ids.
*/
@OptIn(ExperimentalStdlibApi::class)
private fun deriveKeysetId(): KeysetId {
val allKeysConcatenated: String = buildString {
sortedKeyset.values.forEach { publicKey ->
append(publicKey)
}
// Add all compressed public keys together
val allKeysConcatenated = sortedKeyset.values.fold(ByteArray(0)) { acc, publicKey ->
acc + publicKey.value.toByteArray()
}

val versionPrefix = byteArrayOf(0x00)
val bytes: ByteArray = Digest
.sha256()
.hash(allKeysConcatenated.toByteArray(Charsets.UTF_8))
.hash(allKeysConcatenated)
.sliceArray(0..<7)
val keysetId = versionPrefix + bytes

Expand All @@ -60,6 +62,16 @@ public class Keyset(keyset: Map<ULong, PublicKey>) {
return sortedKeyset[tokenValue] ?: throw Exception("No key found in keyset for token value $tokenValue")
}

public fun toKeysetJson(): KeysetJson {
val keys: Map<ULong, String> = sortedKeyset.map { (tokenValue, publicKey) ->
tokenValue to publicKey.toHex()
}.toMap()
val unit: String = "sat"
val id: String = keysetId.value

return KeysetJson(id, unit, keys)
}

/**
* This override allows us to compare two [Keyset]s for equality (not implementing this means leaving the default
* implementation, which is to compare references).
Expand Down Expand Up @@ -97,6 +109,27 @@ public class Keyset(keyset: Map<ULong, PublicKey>) {
}
}

// TODO: This class should be removed in favour of a proper serialization of the Keyset type.
@Serializable
public data class KeysetJson(
public val id: String,
public val unit: String = "sat",
public val keys: Map<ULong, String>,
) {
public fun toKeyset(): Keyset {
val keysetOnly: Map<ULong, PublicKey> = keys.map { (tokenValue, publicKeyHex) ->
tokenValue to PublicKey.fromHex(publicKeyHex)
}.toMap()
// println("keysetOnly: $keysetOnly")
println("Newly created keyset id should be: $id")
val keyset: Keyset = Keyset(keysetOnly)

require(keyset.keysetId.value == id) { "Keyset id mismatch: ${keyset.keysetId.value} != $id" }

return keyset
}
}

// TODO: Write tests for this type
/**
* A [KeysetId] is a unique identifier for a [Keyset].
Expand Down
2 changes: 1 addition & 1 deletion lib/src/test/kotlin/me/tb/cashuclient/KeysetTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class KeysetTest {
@Test
fun `Derive keyset id`() {
val keyset: Keyset = Keyset.fromJson(TEST_KEYSET)
assertEquals<String>(expected = "00236c8dfa24587e", actual = keyset.keysetId.value)
assertEquals<String>(expected = "000f01df73ea149a", actual = keyset.keysetId.value)
}

@Test
Expand Down
26 changes: 18 additions & 8 deletions lib/src/test/kotlin/me/tb/cashuclient/WalletTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
/*
* Copyright 2023 thunderbiscuit and contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the ./LICENSE file.
*/

package me.tb.cashuclient

import me.tb.cashuclient.types.EcashUnit
import me.tb.cashuclient.types.Keyset
import me.tb.cashuclient.types.KeysetId
import kotlin.test.Test
Expand Down Expand Up @@ -92,9 +98,7 @@ class WalletTest {
fun `Wallet successfully updates active keyset from mint after requesting it`() {
val jsonString = """{"1":"03142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9"}"""
val smallKeyset = Keyset.fromJson(jsonString)
val wallet = Wallet(activeKeyset = smallKeyset, mintUrl = "https://testnut.cashu.space")
// val wallet = Wallet(activeKeyset = smallKeyset, mintUrl = "https://mutinynet-cashu.thesimplekid.space")
// val wallet = Wallet(activeKeyset = smallKeyset, mintUrl = "https://8333.space:3338")
val wallet = Wallet(activeKeyset = smallKeyset, mintUrl = "https://testnut.cashu.space", unit = EcashUnit.SATOSHI)
println("The current wallet keyset is ${wallet.activeKeyset}")

// At this point the wallet keyset should not be the same as the smallKeyset
Expand All @@ -105,7 +109,6 @@ class WalletTest {

// Now we request the active keyset from the mint
wallet.getActiveKeyset()
println("The new wallet keyset is ${wallet.activeKeyset}")

// At this point the wallet keyset should not be the same anymore
assertEquals<Boolean>(
Expand All @@ -118,11 +121,10 @@ class WalletTest {
fun `Wallet adds old keyset to list of inactive keysets`() {
val jsonString = """{"1":"03142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9"}"""
val smallKeyset = Keyset.fromJson(jsonString)
val wallet = Wallet(activeKeyset = smallKeyset, mintUrl = "https://testnut.cashu.space")
val wallet = Wallet(activeKeyset = smallKeyset, mintUrl = "https://testnut.cashu.space", unit = EcashUnit.SATOSHI)

// Now we request the active keyset from the mint
wallet.getActiveKeyset()
println("The new wallet keyset ID is ${wallet.activeKeyset!!.keysetId}")

assertEquals<Keyset>(
expected = smallKeyset,
Expand All @@ -134,10 +136,18 @@ class WalletTest {
fun `Wallet can request specific keyset from mint`() {
val jsonString = """{"1":"03142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9"}"""
val smallKeyset = Keyset.fromJson(jsonString)
val wallet = Wallet(activeKeyset = smallKeyset, mintUrl = "https://testnut.cashu.space")
val wallet = Wallet(
activeKeyset = smallKeyset,
mintUrl = "https://testnut.cashu.space",
unit = EcashUnit.SATOSHI
)

val specificKeyset = wallet.getSpecificKeyset(KeysetId("009a1f293253e41e"))
println("The specific keyset 009a1f293253e41e is ${specificKeyset.sortedKeyset}")

assertEquals<String>(
expected = "009a1f293253e41e",
actual = specificKeyset.keysetId.value
)
}

// TODO: Fix this test: from what I understand the test completes before the BD has finished its work,
Expand Down

0 comments on commit 501d8cd

Please sign in to comment.