Skip to content

Commit

Permalink
Use Either for error handling
Browse files Browse the repository at this point in the history
We already export an `Either` type, which can be reused by clients of this library.
  • Loading branch information
sstone committed Jan 31, 2024
1 parent de51b68 commit 9aa4add
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 154 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ plugins {
val currentOs = org.gradle.internal.os.OperatingSystem.current()

group = "fr.acinq.bitcoin"
version = "0.17.0-SNAPSHOT"
version = "0.17.0-EITHER-SNAPSHOT"

repositories {
google()
Expand Down
150 changes: 51 additions & 99 deletions src/commonMain/kotlin/fr/acinq/bitcoin/Bitcoin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package fr.acinq.bitcoin

import fr.acinq.bitcoin.utils.Either
import kotlin.jvm.JvmStatic

public const val MaxBlockSize: Int = 1000000
Expand All @@ -26,63 +27,14 @@ public fun <T> List<T>.updated(i: Int, t: T): List<T> = when (i) {
else -> this.take(i) + t + this.drop(i + 1)
}

public sealed class AddressToPublicKeyScriptResult {

public abstract val result: List<ScriptElt>?

public fun isSuccess(): Boolean = result != null

public fun isFailure(): Boolean = !isSuccess()

public data class Success(val script: List<ScriptElt>) : AddressToPublicKeyScriptResult() {
override val result: List<ScriptElt> = script
}

public sealed class Failure : AddressToPublicKeyScriptResult() {
override val result: List<ScriptElt>? = null

public object ChainHashMismatch : Failure() {
override fun toString(): String = "chain hash mismatch"
}

public object InvalidAddress : Failure() {
override fun toString(): String = "invalid base58 or bech32 address "
}

public object InvalidBech32Address : Failure() {
override fun toString(): String = "invalid bech32 address"
}

public data class InvalidWitnessVersion(val version: Int) : Failure() {
override fun toString(): String = "invalid witness version $version"
}
}
}

public sealed class AddressFromPublicKeyScriptResult {
public abstract val result: String?
public fun isSuccess(): Boolean = result != null
public fun isFailure(): Boolean = !isSuccess()

public data class Success(val address: String) : AddressFromPublicKeyScriptResult() {
override val result: String = address
}

public sealed class Failure : AddressFromPublicKeyScriptResult() {
override val result: String? = null

public object InvalidChainHash : Failure() {
override fun toString(): String = "invalid chain hash"
}

public object InvalidScript : Failure() {
override fun toString(): String = "invalid script"
}

public data class GenericError(val t: Throwable) : Failure() {
override fun toString(): String = "generic failure: ${t.message}"
}
}
public sealed class BitcoinException(message: String, cause: Throwable?) : Exception(message, cause) {
public data object InvalidChainHash : BitcoinException("invalid chain hash", null)
public data object ChainHashMismatch : BitcoinException("chain hash mismatch", null)
public data object InvalidScript : BitcoinException("invalid script", null)
public data object InvalidAddress : BitcoinException("invalid address", null)
public data object InvalidBech32Address : BitcoinException("invalid address", null)
public data class InvalidWitnessVersion(val version: Int) : BitcoinException("invalid witness version $version", null)
public data class GenericError(override val message: String, override val cause: Throwable?) : BitcoinException(message, cause)
}

public object Bitcoin {
Expand Down Expand Up @@ -121,77 +73,77 @@ public object Bitcoin {
* @param pubkeyScript public key script
*/
@JvmStatic
public fun addressFromPublicKeyScript(chainHash: BlockHash, pubkeyScript: List<ScriptElt>): AddressFromPublicKeyScriptResult {
public fun addressFromPublicKeyScript(chainHash: BlockHash, pubkeyScript: List<ScriptElt>): Either<BitcoinException, String> {
try {
return when {
Script.isPay2pkh(pubkeyScript) -> {
val prefix = when (chainHash) {
Block.LivenetGenesisBlock.hash -> Base58.Prefix.PubkeyAddress
Block.TestnetGenesisBlock.hash, Block.RegtestGenesisBlock.hash, Block.SignetGenesisBlock.hash -> Base58.Prefix.PubkeyAddressTestnet
else -> return AddressFromPublicKeyScriptResult.Failure.InvalidChainHash
else -> return Either.Left(BitcoinException.InvalidChainHash)
}
AddressFromPublicKeyScriptResult.Success(Base58Check.encode(prefix, (pubkeyScript[2] as OP_PUSHDATA).data))
Either.Right(Base58Check.encode(prefix, (pubkeyScript[2] as OP_PUSHDATA).data))
}

Script.isPay2sh(pubkeyScript) -> {
val prefix = when (chainHash) {
Block.LivenetGenesisBlock.hash -> Base58.Prefix.ScriptAddress
Block.TestnetGenesisBlock.hash, Block.RegtestGenesisBlock.hash, Block.SignetGenesisBlock.hash -> Base58.Prefix.ScriptAddressTestnet
else -> return AddressFromPublicKeyScriptResult.Failure.InvalidChainHash
else -> return Either.Left(BitcoinException.InvalidChainHash)
}
AddressFromPublicKeyScriptResult.Success(Base58Check.encode(prefix, (pubkeyScript[1] as OP_PUSHDATA).data))
Either.Right(Base58Check.encode(prefix, (pubkeyScript[1] as OP_PUSHDATA).data))
}

Script.isNativeWitnessScript(pubkeyScript) -> {
val hrp = Bech32.hrp(chainHash)
val witnessScript = (pubkeyScript[1] as OP_PUSHDATA).data.toByteArray()
when (pubkeyScript[0]) {
is OP_0 -> when {
Script.isPay2wpkh(pubkeyScript) || Script.isPay2wsh(pubkeyScript) -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 0, witnessScript))
else -> AddressFromPublicKeyScriptResult.Failure.InvalidScript
Script.isPay2wpkh(pubkeyScript) || Script.isPay2wsh(pubkeyScript) -> Either.Right(Bech32.encodeWitnessAddress(hrp, 0, witnessScript))
else -> return Either.Left(BitcoinException.InvalidScript)
}

is OP_1 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 1, witnessScript))
is OP_2 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 2, witnessScript))
is OP_3 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 3, witnessScript))
is OP_4 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 4, witnessScript))
is OP_5 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 5, witnessScript))
is OP_6 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 6, witnessScript))
is OP_7 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 7, witnessScript))
is OP_8 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 8, witnessScript))
is OP_9 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 9, witnessScript))
is OP_10 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 10, witnessScript))
is OP_11 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 11, witnessScript))
is OP_12 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 12, witnessScript))
is OP_13 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 13, witnessScript))
is OP_14 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 14, witnessScript))
is OP_15 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 15, witnessScript))
is OP_16 -> AddressFromPublicKeyScriptResult.Success(Bech32.encodeWitnessAddress(hrp, 16, witnessScript))
else -> AddressFromPublicKeyScriptResult.Failure.InvalidScript
is OP_1 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 1, witnessScript))
is OP_2 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 2, witnessScript))
is OP_3 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 3, witnessScript))
is OP_4 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 4, witnessScript))
is OP_5 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 5, witnessScript))
is OP_6 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 6, witnessScript))
is OP_7 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 7, witnessScript))
is OP_8 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 8, witnessScript))
is OP_9 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 9, witnessScript))
is OP_10 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 10, witnessScript))
is OP_11 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 11, witnessScript))
is OP_12 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 12, witnessScript))
is OP_13 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 13, witnessScript))
is OP_14 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 14, witnessScript))
is OP_15 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 15, witnessScript))
is OP_16 -> Either.Right(Bech32.encodeWitnessAddress(hrp, 16, witnessScript))
else -> return Either.Left(BitcoinException.InvalidScript)
}
}

else -> AddressFromPublicKeyScriptResult.Failure.InvalidScript
else -> return Either.Left(BitcoinException.InvalidScript)
}
} catch (t: Throwable) {
return AddressFromPublicKeyScriptResult.Failure.GenericError(t)
return Either.Left(BitcoinException.GenericError("", t))
}
}

@JvmStatic
public fun addressFromPublicKeyScript(chainHash: BlockHash, pubkeyScript: ByteArray): AddressFromPublicKeyScriptResult {
public fun addressFromPublicKeyScript(chainHash: BlockHash, pubkeyScript: ByteArray): Either<BitcoinException, String> {
return runCatching { Script.parse(pubkeyScript) }.fold(
onSuccess = {
addressFromPublicKeyScript(chainHash, it)
},
onFailure = {
AddressFromPublicKeyScriptResult.Failure.InvalidScript
Either.Left(BitcoinException.InvalidScript)
}
)
}

@JvmStatic
public fun addressToPublicKeyScript(chainHash: BlockHash, address: String): AddressToPublicKeyScriptResult {
public fun addressToPublicKeyScript(chainHash: BlockHash, address: String): Either<BitcoinException, List<ScriptElt>> {
val witnessVersions = mapOf(
0.toByte() to OP_0,
1.toByte() to OP_1,
Expand All @@ -216,36 +168,36 @@ public object Bitcoin {
onSuccess = {
when {
it.first == Base58.Prefix.PubkeyAddressTestnet && (chainHash == Block.TestnetGenesisBlock.hash || chainHash == Block.RegtestGenesisBlock.hash || chainHash == Block.SignetGenesisBlock.hash) ->
AddressToPublicKeyScriptResult.Success(Script.pay2pkh(it.second))
Either.Right(Script.pay2pkh(it.second))

it.first == Base58.Prefix.PubkeyAddress && chainHash == Block.LivenetGenesisBlock.hash ->
AddressToPublicKeyScriptResult.Success(Script.pay2pkh(it.second))
Either.Right(Script.pay2pkh(it.second))

it.first == Base58.Prefix.ScriptAddressTestnet && (chainHash == Block.TestnetGenesisBlock.hash || chainHash == Block.RegtestGenesisBlock.hash || chainHash == Block.SignetGenesisBlock.hash) ->
AddressToPublicKeyScriptResult.Success(listOf(OP_HASH160, OP_PUSHDATA(it.second), OP_EQUAL))
Either.Right(listOf(OP_HASH160, OP_PUSHDATA(it.second), OP_EQUAL))

it.first == Base58.Prefix.ScriptAddress && chainHash == Block.LivenetGenesisBlock.hash ->
AddressToPublicKeyScriptResult.Success(listOf(OP_HASH160, OP_PUSHDATA(it.second), OP_EQUAL))
Either.Right(listOf(OP_HASH160, OP_PUSHDATA(it.second), OP_EQUAL))

else -> AddressToPublicKeyScriptResult.Failure.ChainHashMismatch
else -> Either.Left(BitcoinException.ChainHashMismatch)
}
},
onFailure = { _ ->
runCatching { Bech32.decodeWitnessAddress(address) }.fold(
onSuccess = {
val witnessVersion = witnessVersions[it.second]
when {
witnessVersion == null -> AddressToPublicKeyScriptResult.Failure.InvalidWitnessVersion(it.second.toInt())
it.third.size != 20 && it.third.size != 32 -> AddressToPublicKeyScriptResult.Failure.InvalidBech32Address
it.first == "bc" && chainHash == Block.LivenetGenesisBlock.hash -> AddressToPublicKeyScriptResult.Success(listOf(witnessVersion, OP_PUSHDATA(it.third)))
it.first == "tb" && chainHash == Block.TestnetGenesisBlock.hash -> AddressToPublicKeyScriptResult.Success(listOf(witnessVersion, OP_PUSHDATA(it.third)))
it.first == "tb" && chainHash == Block.SignetGenesisBlock.hash -> AddressToPublicKeyScriptResult.Success(listOf(witnessVersion, OP_PUSHDATA(it.third)))
it.first == "bcrt" && chainHash == Block.RegtestGenesisBlock.hash -> AddressToPublicKeyScriptResult.Success(listOf(witnessVersion, OP_PUSHDATA(it.third)))
else -> AddressToPublicKeyScriptResult.Failure.ChainHashMismatch
witnessVersion == null -> Either.Left(BitcoinException.InvalidWitnessVersion(it.second.toInt()))
it.third.size != 20 && it.third.size != 32 -> Either.Left(BitcoinException.InvalidBech32Address)
it.first == "bc" && chainHash == Block.LivenetGenesisBlock.hash -> Either.Right(listOf(witnessVersion, OP_PUSHDATA(it.third)))
it.first == "tb" && chainHash == Block.TestnetGenesisBlock.hash -> Either.Right(listOf(witnessVersion, OP_PUSHDATA(it.third)))
it.first == "tb" && chainHash == Block.SignetGenesisBlock.hash -> Either.Right(listOf(witnessVersion, OP_PUSHDATA(it.third)))
it.first == "bcrt" && chainHash == Block.RegtestGenesisBlock.hash -> Either.Right(listOf(witnessVersion, OP_PUSHDATA(it.third)))
else -> Either.Left(BitcoinException.ChainHashMismatch)
}
},
onFailure = {
AddressToPublicKeyScriptResult.Failure.InvalidAddress
Either.Left(BitcoinException.InvalidAddress)
}
)
}
Expand Down
22 changes: 16 additions & 6 deletions src/commonMain/kotlin/fr/acinq/bitcoin/utils/Either.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,23 @@ public sealed class Either<out L, out R> {

public inline fun <X> map(f: (R) -> X): Either<L, X> = transform({ it }, f)

public data class Left<out L, Nothing>(val value: L) : Either<L, Nothing>() {
public data class Left<out L>(val value: L) : Either<L, Nothing>() {
override val isLeft: Boolean = true
override val isRight: Boolean = false
override val left: L? = value
override val left: L = value
override val right: Nothing? = null
}

public data class Right<Nothing, out R>(val value: R) : Either<Nothing, R>() {
public data class Right<out R>(val value: R) : Either<Nothing, R>() {
override val isLeft: Boolean = false
override val isRight: Boolean = true
override val left: Nothing? = null
override val right: R? = value
override val right: R = value
}
}

@Suppress("UNCHECKED_CAST")
public inline fun <L, R, X> Either<L, R>.flatMap(f: (R) -> Either<L, X>): Either<L, X> = when (this) {
is Either.Left -> this as Either<L, X>
is Either.Left -> this
is Either.Right -> f(this.value)
}

Expand All @@ -64,3 +63,14 @@ public fun <L, R> Either<L, R>.getOrDefault(defaultValue: R): R = when (this) {
is Either.Left -> defaultValue
is Either.Right -> this.value
}

public fun <R> Result<R>.toEither(): Either<Throwable, R> = try {
Either.Right(getOrThrow())
} catch (t: Throwable) {
Either.Left(t)
}

public fun <L : Throwable, R> Either<L, R>.toResult(): Result<R> = when (this) {
is Either.Left -> Result.failure(this.value)
is Either.Right -> Result.success(this.value)
}
6 changes: 3 additions & 3 deletions src/commonTest/kotlin/fr/acinq/bitcoin/BIP86TestsCommon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class BIP86TestsCommon {
assertEquals(outputKey.value, ByteVector32("a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c"))
val script = listOf(OP_1, OP_PUSHDATA(outputKey.value))
assertEquals(Script.write(script).byteVector(), ByteVector("5120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c"))
assertEquals(Bitcoin.addressFromPublicKeyScript(Block.LivenetGenesisBlock.hash, script), AddressFromPublicKeyScriptResult.Success("bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"))
assertEquals(Bitcoin.addressFromPublicKeyScript(Block.LivenetGenesisBlock.hash, script).right, "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr")

val key1 = DeterministicWallet.derivePrivateKey(accountKey, listOf(0L, 1L))
assertEquals(key1.secretkeybytes, DeterministicWallet.derivePrivateKey(master, KeyPath("m/86'/0'/0'/0/1")).secretkeybytes)
Expand All @@ -33,7 +33,7 @@ class BIP86TestsCommon {
assertEquals(outputKey1.value, ByteVector32("a82f29944d65b86ae6b5e5cc75e294ead6c59391a1edc5e016e3498c67fc7bbb"))
val script1 = listOf(OP_1, OP_PUSHDATA(outputKey1.value))
assertEquals(Script.write(script1).byteVector(), ByteVector("5120a82f29944d65b86ae6b5e5cc75e294ead6c59391a1edc5e016e3498c67fc7bbb"))
assertEquals(Bitcoin.addressFromPublicKeyScript(Block.LivenetGenesisBlock.hash, script1), AddressFromPublicKeyScriptResult.Success("bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh"))
assertEquals(Bitcoin.addressFromPublicKeyScript(Block.LivenetGenesisBlock.hash, script1).right, "bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh")

val key2 = DeterministicWallet.derivePrivateKey(accountKey, listOf(1L, 0L))
assertEquals(key2.secretkeybytes, DeterministicWallet.derivePrivateKey(master, KeyPath("m/86'/0'/0'/1/0")).secretkeybytes)
Expand All @@ -43,7 +43,7 @@ class BIP86TestsCommon {
assertEquals(outputKey2.value, ByteVector32("882d74e5d0572d5a816cef0041a96b6c1de832f6f9676d9605c44d5e9a97d3dc"))
val script2 = listOf(OP_1, OP_PUSHDATA(outputKey2.value))
assertEquals(Script.write(script2).byteVector(), ByteVector("5120882d74e5d0572d5a816cef0041a96b6c1de832f6f9676d9605c44d5e9a97d3dc"))
assertEquals(Bitcoin.addressFromPublicKeyScript(Block.LivenetGenesisBlock.hash, script2), AddressFromPublicKeyScriptResult.Success("bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7"))
assertEquals(Bitcoin.addressFromPublicKeyScript(Block.LivenetGenesisBlock.hash, script2).right, "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7")
}

@Test
Expand Down
Loading

0 comments on commit 9aa4add

Please sign in to comment.