From 9d245c62a8dab8c47fd4999f9ed997a846bf574c Mon Sep 17 00:00:00 2001 From: Evgenii Kuzovkin Date: Fri, 11 Oct 2024 06:18:17 +0300 Subject: [PATCH] AND-8609: Add CasperAddressService --- .../casper/CasperAddressService.kt | 33 +++++++++++ .../blockchains/casper/CasperConstants.kt | 26 +++++++++ .../casper/cashaddr/CasperAddressUtils.kt | 55 +++++++++++++++++++ .../tangem/blockchain/common/Blockchain.kt | 3 +- .../tangem/blockchain/extensions/String.kt | 2 + .../blockchains/casper/CasperAddressTest.kt | 41 ++++++++++++++ 6 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 blockchain/src/main/java/com/tangem/blockchain/blockchains/casper/CasperAddressService.kt create mode 100644 blockchain/src/main/java/com/tangem/blockchain/blockchains/casper/CasperConstants.kt create mode 100644 blockchain/src/main/java/com/tangem/blockchain/blockchains/casper/cashaddr/CasperAddressUtils.kt create mode 100644 blockchain/src/test/java/com/tangem/blockchain/blockchains/casper/CasperAddressTest.kt diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/casper/CasperAddressService.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/casper/CasperAddressService.kt new file mode 100644 index 000000000..94ad5aa71 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/casper/CasperAddressService.kt @@ -0,0 +1,33 @@ +package com.tangem.blockchain.blockchains.casper + +import com.tangem.blockchain.blockchains.casper.cashaddr.CasperAddressUtils.checksum +import com.tangem.blockchain.common.address.AddressService +import com.tangem.blockchain.extensions.isSameCase +import com.tangem.common.card.EllipticCurve +import com.tangem.common.extensions.hexToBytes + +internal class CasperAddressService : AddressService() { + override fun makeAddress(walletPublicKey: ByteArray, curve: EllipticCurve?): String { + val prefix = CasperConstants.getAddressPrefix(curve!!) + val bytes = prefix.hexToBytes() + walletPublicKey + return bytes.checksum() + } + + override fun validate(address: String): Boolean { + val isCorrectEd25519Address = address.length == CasperConstants.ED25519_LENGTH && + address.startsWith(CasperConstants.ED25519_PREFIX) + val isCorrectSecp256k1Address = address.length == CasperConstants.SECP256K1_LENGTH && + address.startsWith(CasperConstants.SECP256K1_PREFIX) + + if (!isCorrectEd25519Address && !isCorrectSecp256k1Address) { + return false + } + + // don't check checksum if it's not mixed case + if (address.isSameCase()) { + return true + } + + return address == address.hexToBytes().checksum() + } +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/casper/CasperConstants.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/casper/CasperConstants.kt new file mode 100644 index 000000000..1d7e09171 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/casper/CasperConstants.kt @@ -0,0 +1,26 @@ +package com.tangem.blockchain.blockchains.casper + +import com.tangem.common.card.EllipticCurve + +internal object CasperConstants { + const val ED25519_PREFIX = "01" + const val ED25519_LENGTH = 66 + + const val SECP256K1_PREFIX = "02" + const val SECP256K1_LENGTH = 68 + + fun getAddressPrefix(curve: EllipticCurve) = when (curve) { + EllipticCurve.Ed25519, + EllipticCurve.Ed25519Slip0010, + -> ED25519_PREFIX + EllipticCurve.Secp256k1, + -> SECP256K1_PREFIX + // added as unsupported for now, need to research + EllipticCurve.Secp256r1, + EllipticCurve.Bls12381G2, + EllipticCurve.Bls12381G2Aug, + EllipticCurve.Bls12381G2Pop, + EllipticCurve.Bip0340, + -> error("${curve.curve} is not supported") + } +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/casper/cashaddr/CasperAddressUtils.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/casper/cashaddr/CasperAddressUtils.kt new file mode 100644 index 000000000..ad68b401f --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/casper/cashaddr/CasperAddressUtils.kt @@ -0,0 +1,55 @@ +package com.tangem.blockchain.blockchains.casper.cashaddr + +import org.bouncycastle.jcajce.provider.digest.Blake2b + +/** + * @see Source + */ +object CasperAddressUtils { + // Ed25519: encode([0x01]) + encode() + // or + // Secp256k1: encode([0x02]) + encode() + fun ByteArray.checksum(): String = encode(byteArrayOf(first())) + encode(drop(1).toByteArray()) + + // Separate bytes inside ByteArray to nibbles + // E.g. [0x01, 0x55, 0xFF, ...] -> [0x00, 0x01, 0x50, 0x05, 0xF0, 0x0F, ...] + @Suppress("MagicNumber") + private fun bytesToNibbles(bytes: ByteArray): ByteArray = bytes + .flatMap { byte -> + listOf( + (byte.toInt() and 0xff shr 4).toByte(), + (byte.toInt() and 0x0f).toByte(), + ) + }.toByteArray() + + // Separate bytes inside ByteArray to bits array + // E.g. [0x01, ...] -> [false, false, false, false, false, false, false, true, ...] + // E.g. [0xAA, ...] -> [true, false, true, false, true, false, true, false, ...] + @Suppress("MagicNumber") + private fun ByteArray.toBitArray(): BooleanArray = this + .flatMap { byte -> + List(8) { i -> + byte.toInt() shr i and 0x01 == 0x01 + } + }.toBooleanArray() + + private fun byteHash(bytes: ByteArray): ByteArray = Blake2b.Blake2b256().digest(bytes) + + private fun encode(input: ByteArray): String { + val inputNibbles = bytesToNibbles(input) + val hash = byteHash(input) + val hashBits = hash.toBitArray() + val hashBitsValues = hashBits.iterator() + val hexOutputString = inputNibbles.fold(StringBuilder()) { accum, nibble -> + val c = "%x".format(nibble) + + if (Regex("^[a-zA-Z()]+\$").matches(c) && hashBitsValues.hasNext() && hashBitsValues.next()) { + accum.append(c.uppercase()) + } else { + accum.append(c.lowercase()) + } + accum + }.toString() + return hexOutputString + } +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt b/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt index fd1560f1b..8f5496558 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.blockchains.binance.BinanceAddressService import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressService import com.tangem.blockchain.blockchains.bitcoincash.BitcoinCashAddressService import com.tangem.blockchain.blockchains.cardano.CardanoAddressServiceFacade +import com.tangem.blockchain.blockchains.casper.CasperAddressService import com.tangem.blockchain.blockchains.chia.ChiaAddressService import com.tangem.blockchain.blockchains.decimal.DecimalAddressService import com.tangem.blockchain.blockchains.ethereum.Chain @@ -377,7 +378,7 @@ enum class Blockchain( Nexa, NexaTestnet -> NexaAddressService(this.isTestnet()) Koinos, KoinosTestnet -> KoinosAddressService() Radiant -> RadiantAddressService() - Casper, CasperTestnet -> TODO() // AND-8609 + Casper, CasperTestnet -> CasperAddressService() Unknown -> error("unsupported blockchain") } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/extensions/String.kt b/blockchain/src/main/java/com/tangem/blockchain/extensions/String.kt index d9d6eb758..95c5a7cb4 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/extensions/String.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/extensions/String.kt @@ -38,6 +38,8 @@ fun String?.toBigDecimalOrDefault(default: BigDecimal = BigDecimal.ZERO): BigDec fun String.isValidHex(): Boolean = this.all { it.isAscii() } +fun String.isSameCase(): Boolean = this.lowercase() == this || this.uppercase() == this + inline fun String?.letNotBlank(block: (String) -> R): R? { if (isNullOrBlank()) return null diff --git a/blockchain/src/test/java/com/tangem/blockchain/blockchains/casper/CasperAddressTest.kt b/blockchain/src/test/java/com/tangem/blockchain/blockchains/casper/CasperAddressTest.kt new file mode 100644 index 000000000..8f0c162f5 --- /dev/null +++ b/blockchain/src/test/java/com/tangem/blockchain/blockchains/casper/CasperAddressTest.kt @@ -0,0 +1,41 @@ +package com.tangem.blockchain.blockchains.casper + +import com.google.common.truth.Truth +import com.tangem.common.card.EllipticCurve +import com.tangem.common.extensions.hexToBytes +import org.junit.Test + +class CasperAddressTest { + + private val addressService = CasperAddressService() + + @Test + fun makeAddressFromCorrectEd25519PublicKey() { + val walletPublicKey = "98C07D7E72D89A681D7227A7AF8A6FD5F22FE0105C8741D55A95DF415454B82E".hexToBytes() + val expected = "0198c07D7e72D89A681d7227a7Af8A6fd5F22fe0105c8741d55A95dF415454b82E" + + Truth.assertThat(addressService.makeAddress(walletPublicKey, EllipticCurve.Ed25519)).isEqualTo(expected) + } + + @Test + fun validateCorrectEd25519Address() { + val address = "0198c07D7e72D89A681d7227a7Af8A6fd5F22fe0105c8741d55A95dF415454b82E" + + Truth.assertThat(addressService.validate(address)).isTrue() + } + + @Test + fun makeAddressFromCorrectSecp256k1PublicKey() { + val walletPublicKey = "021F997DFBBFD32817C0E110EAEE26BCBD2BB70B4640C515D9721C9664312EACD8".hexToBytes() + val expected = "02021f997DfbbFd32817C0E110EAeE26BCbD2BB70b4640C515D9721c9664312eaCd8" + + Truth.assertThat(addressService.makeAddress(walletPublicKey, EllipticCurve.Secp256k1)).isEqualTo(expected) + } + + @Test + fun validateCorrectSecp256k1Address() { + val address = "02021f997DfbbFd32817C0E110EAeE26BCbD2BB70b4640C515D9721c9664312eaCd8" + + Truth.assertThat(addressService.validate(address)).isTrue() + } +}