Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 10 additions & 17 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@
<groupId>fr.acinq</groupId>
<artifactId>bitcoin-lib_2.13</artifactId>
<packaging>jar</packaging>
<version>0.45.2-SNAPSHOT</version>
<version>0.46</version>
<description>Simple Scala Bitcoin library</description>
<url>https://github.com/ACINQ/bitcoin-lib</url>
<name>bitcoin-lib</name>

<distributionManagement>
<snapshotRepository>
<id>central-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots</url>
</snapshotRepository>
</distributionManagement>

<properties>
<project.build.outputTimestamp>2020-01-01T00:00:00Z</project.build.outputTimestamp>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand Down Expand Up @@ -40,20 +47,6 @@
</developer>
</developers>

<repositories>
<repository>
<id>central-snapshots</id>
<name>Sonatype Central Portal (snapshots)</name>
<url>https://central.sonatype.com/repository/maven-snapshots</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>

<build>
<plugins>
<plugin>
Expand Down Expand Up @@ -153,12 +146,12 @@
<dependency>
<groupId>fr.acinq.bitcoin</groupId>
<artifactId>bitcoin-kmp-jvm</artifactId>
<version>0.28.1</version>
<version>0.29.0</version>
</dependency>
<dependency>
<groupId>fr.acinq.secp256k1</groupId>
<artifactId>secp256k1-kmp-jni-jvm</artifactId>
<version>0.21.0</version>
<version>0.22.0</version>
</dependency>
<dependency>
<groupId>org.scodec</groupId>
Expand Down
25 changes: 9 additions & 16 deletions src/main/scala/fr/acinq/bitcoin/scalacompat/Crypto.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,14 @@ import scodec.bits.ByteVector
object Crypto {

// @formatter:off
/** Specify how private keys are tweaked when creating Schnorr signatures. */
sealed trait SchnorrTweak
object SchnorrTweak {
/** The private key is directly used, without any tweaks. */
case object NoTweak extends SchnorrTweak
}

sealed trait TaprootTweak extends SchnorrTweak
sealed trait TaprootTweak
object TaprootTweak {
/** The private key is tweaked with H_TapTweak(public key) (this is used for key path spending when there is no script tree). */
case object NoScriptTweak extends TaprootTweak
case object KeyPathTweak extends TaprootTweak
/** The private key is tweaked with H_TapTweak(public key || merkle_root) (this is used for key path spending when a script tree exists). */
case class ScriptTweak(merkleRoot: ByteVector32) extends TaprootTweak
object ScriptTweak {
def apply(scriptTree: ScriptTree): ScriptTweak = ScriptTweak(scriptTree.hash())
case class ScriptPathTweak(merkleRoot: ByteVector32) extends TaprootTweak
object ScriptPathTweak {
def apply(scriptTree: ScriptTree): ScriptPathTweak = ScriptPathTweak(scriptTree.hash())
}
}
// @formatter:on
Expand Down Expand Up @@ -159,10 +152,10 @@ object Crypto {
}

/** Tweak this key with the merkle root of the given script tree. */
def outputKey(scriptTree: ScriptTree): (XonlyPublicKey, Boolean) = outputKey(TaprootTweak.ScriptTweak(scriptTree))
def outputKey(scriptTree: ScriptTree): (XonlyPublicKey, Boolean) = outputKey(TaprootTweak.ScriptPathTweak(scriptTree))

/** Tweak this key with the merkle root provided. */
def outputKey(merkleRoot: ByteVector32): (XonlyPublicKey, Boolean) = outputKey(TaprootTweak.ScriptTweak(merkleRoot))
def outputKey(merkleRoot: ByteVector32): (XonlyPublicKey, Boolean) = outputKey(TaprootTweak.ScriptPathTweak(merkleRoot))

/**
* add a public key to this x-only key
Expand Down Expand Up @@ -295,8 +288,8 @@ object Crypto {
* the key (there is an extra "1" appended to the key)
* @return a signature in compact format (64 bytes)
*/
def signSchnorr(data: ByteVector32, privateKey: PrivateKey, schnorrTweak: SchnorrTweak = SchnorrTweak.NoTweak, auxrand32: Option[ByteVector32] = None): ByteVector64 = {
bitcoin.Crypto.signSchnorr(data, privateKey, scala2kmp(schnorrTweak), auxrand32.map(scala2kmp).orNull)
def signSchnorr(data: ByteVector32, privateKey: PrivateKey, schnorrTweak_opt: Option[TaprootTweak] = None, auxrand32: Option[ByteVector32] = None): ByteVector64 = {
bitcoin.Crypto.signSchnorr(data, privateKey, schnorrTweak_opt.map(scala2kmp).orNull, auxrand32.map(scala2kmp).orNull)
}

/**
Expand Down
20 changes: 4 additions & 16 deletions src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,25 +65,13 @@ object KotlinUtils {
}

implicit def kmp2scala(input: bitcoin.Crypto.TaprootTweak): TaprootTweak = input match {
case bitcoin.Crypto.TaprootTweak.NoScriptTweak.INSTANCE => TaprootTweak.NoScriptTweak
case tweak: bitcoin.Crypto.TaprootTweak.ScriptTweak => TaprootTweak.ScriptTweak(kmp2scala(tweak.getMerkleRoot))
case _ => ??? // this cannot happen, but the compiler cannot know that there aren't other cases
case bitcoin.Crypto.TaprootTweak.KeyPathTweak.INSTANCE => TaprootTweak.KeyPathTweak
case tweak: bitcoin.Crypto.TaprootTweak.ScriptPathTweak => TaprootTweak.ScriptPathTweak(kmp2scala(tweak.getMerkleRoot))
}

implicit def scala2kmp(input: TaprootTweak): bitcoin.Crypto.TaprootTweak = input match {
case TaprootTweak.NoScriptTweak => bitcoin.Crypto.TaprootTweak.NoScriptTweak.INSTANCE
case tweak: TaprootTweak.ScriptTweak => new bitcoin.Crypto.TaprootTweak.ScriptTweak(scala2kmp(tweak.merkleRoot))
}

implicit def kmp2scala(input: bitcoin.Crypto.SchnorrTweak): SchnorrTweak = input match {
case bitcoin.Crypto.SchnorrTweak.NoTweak.INSTANCE => SchnorrTweak.NoTweak
case tweak: bitcoin.Crypto.TaprootTweak => kmp2scala(tweak)
case _ => ??? // this cannot happen, but the compiler cannot know that there aren't other cases
}

implicit def scala2kmp(input: SchnorrTweak): bitcoin.Crypto.SchnorrTweak = input match {
case SchnorrTweak.NoTweak => bitcoin.Crypto.SchnorrTweak.NoTweak.INSTANCE
case tweak: TaprootTweak => scala2kmp(tweak)
case TaprootTweak.KeyPathTweak => bitcoin.Crypto.TaprootTweak.KeyPathTweak.INSTANCE
case tweak: TaprootTweak.ScriptPathTweak => new bitcoin.Crypto.TaprootTweak.ScriptPathTweak(scala2kmp(tweak.merkleRoot))
}

implicit def kmp2scala(input: bitcoin.TxIn): TxIn = TxIn(input.outPoint, input.signatureScript, input.sequence, input.witness)
Expand Down
16 changes: 11 additions & 5 deletions src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,22 @@ object Script {
def witnessPay2wpkh(pubKey: PublicKey, sig: ByteVector): ScriptWitness = bitcoin.Script.witnessPay2wpkh(pubKey, sig)

/**
* @param outputKey public key exposed by the taproot script (tweaked based on the tapscripts).
* @return a pay-to-taproot script.
* @param internalKey internal public key that will be tweaked with the [scripts] provided.
* @param scriptsRoot spending scripts that can be used instead of key-path spending.
*/
def pay2tr(outputKey: XonlyPublicKey): Seq[ScriptElt] = bitcoin.Script.pay2tr(outputKey.pub).asScala.map(kmp2scala).toList
def pay2tr(internalKey: XonlyPublicKey, scriptsRoot: ByteVector32): Seq[ScriptElt] = bitcoin.Script.pay2tr(internalKey.pub, scriptsRoot).asScala.map(kmp2scala).toList

/**
* @param internalKey internal public key that will be tweaked with the [scripts] provided.
* @param scripts_opt optional spending scripts that can be used instead of key-path spending.
* @param scripts spending scripts that can be used instead of key-path spending.
*/
def pay2tr(internalKey: XonlyPublicKey, scripts: ScriptTree): Seq[ScriptElt] = pay2tr(internalKey, scripts.hash())

/**
* @param internalKey internal public key that will be tweaked with the provided [taprootTweak].
* @param taprootTweak tweak to apply to [internalKey].
*/
def pay2tr(internalKey: XonlyPublicKey, scripts_opt: Option[ScriptTree]): Seq[ScriptElt] = bitcoin.Script.pay2tr(internalKey.pub, scripts_opt.map(scala2kmp).orNull).asScala.map(kmp2scala).toList
def pay2tr(internalKey: XonlyPublicKey, taprootTweak: Crypto.TaprootTweak): Seq[ScriptElt] = bitcoin.Script.pay2tr(internalKey.pub, taprootTweak).asScala.map(kmp2scala).toList

def isPay2tr(script: Seq[ScriptElt]): Boolean = bitcoin.Script.isPay2tr(script.map(scala2kmp).asJava)

Expand Down
5 changes: 3 additions & 2 deletions src/test/scala/fr/acinq/bitcoin/scalacompat/Musig2Spec.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fr.acinq.bitcoin.scalacompat

import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
import fr.acinq.bitcoin.scalacompat.Crypto.TaprootTweak.KeyPathTweak
import fr.acinq.bitcoin.{ScriptFlags, SigHash}
import org.scalatest.FunSuite
import scodec.bits.{ByteVector, HexStringSyntax}
Expand All @@ -19,7 +20,7 @@ class Musig2Spec extends FunSuite {
val internalPubKey = Musig2.aggregateKeys(Seq(alicePubKey, bobPubKey))

// This tx sends to a taproot script that doesn't contain any script path.
val tx = Transaction(2, Nil, Seq(TxOut(10_000 sat, Script.pay2tr(internalPubKey, scripts_opt = None))), 0)
val tx = Transaction(2, Nil, Seq(TxOut(10_000 sat, Script.pay2tr(internalPubKey, KeyPathTweak))), 0)
// This tx spends the previous tx with Alice and Bob's signatures.
val spendingTx = Transaction(2, Seq(TxIn(OutPoint(tx, 0), ByteVector.empty, 0)), Seq(TxOut(10_000 sat, Script.pay2wpkh(alicePubKey))), 0)

Expand Down Expand Up @@ -59,7 +60,7 @@ class Musig2Spec extends FunSuite {
// The internal pubkey is the musig2 aggregation of the user's and server's public keys: it does not depend upon the user's refund's key.
val aggregatedKey = Musig2.aggregateKeys(Seq(userPublicKey, serverPublicKey))
// It is tweaked with the script's merkle root to get the pubkey that will be exposed.
val pubkeyScript = Script.pay2tr(aggregatedKey, Some(scriptTree))
val pubkeyScript = Script.pay2tr(aggregatedKey, scriptTree)

val swapInTx = Transaction(
version = 2,
Expand Down
21 changes: 11 additions & 10 deletions src/test/scala/fr/acinq/bitcoin/scalacompat/TaprootSpec.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package fr.acinq.bitcoin.scalacompat

import fr.acinq.bitcoin.scalacompat.Crypto.TaprootTweak.KeyPathTweak
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, TaprootTweak}
import fr.acinq.bitcoin.scalacompat.KotlinUtils._
import fr.acinq.bitcoin.{Bech32, ScriptFlags, SigHash, SigVersion}
Expand All @@ -14,10 +15,10 @@ class TaprootSpec extends FunSuite {
val (_, master) = DeterministicWallet.ExtendedPrivateKey.decode("tprv8ZgxMBicQKsPeQQADibg4WF7mEasy3piWZUHyThAzJCPNgMHDVYhTCVfev3jFbDhcYm4GimeFMbbi9z1d9rfY1aL5wfJ9mNebQ4thJ62EJb")
val key = DeterministicWallet.derivePrivateKey(master, "86'/1'/0'/0/1")
val internalKey = key.publicKey.xOnly
val script = Script.pay2tr(internalKey, scripts_opt = None)
val (outputKey, _) = internalKey.outputKey(TaprootTweak.NoScriptTweak)
val script = Script.pay2tr(internalKey, KeyPathTweak)
val (outputKey, _) = internalKey.outputKey(TaprootTweak.KeyPathTweak)
assert("tb1phlhs7afhqzkgv0n537xs939s687826vn8l24ldkrckvwsnlj3d7qj6u57c" == Bech32.encodeWitnessAddress("tb", 1, outputKey.pub.value.toByteArray))
assert(script == Script.pay2tr(outputKey))
assert(addressFromPublicKeyScript(Block.Testnet3GenesisBlock.hash, script).contains("tb1phlhs7afhqzkgv0n537xs939s687826vn8l24ldkrckvwsnlj3d7qj6u57c"))

// tx sends to tb1phlhs7afhqzkgv0n537xs939s687826vn8l24ldkrckvwsnlj3d7qj6u57c
val tx = Transaction.read(
Expand All @@ -41,31 +42,31 @@ class TaprootSpec extends FunSuite {
assert(Crypto.verifySignatureSchnorr(hash, sig, outputKey))

// re-create signature
val ourSig = Crypto.signSchnorr(hash, key.privateKey, TaprootTweak.NoScriptTweak)
val ourSig = Crypto.signSchnorr(hash, key.privateKey, Some(TaprootTweak.KeyPathTweak))
assert(Crypto.verifySignatureSchnorr(hash, ourSig, outputKey))
assert(Secp256k1.get().verifySchnorr(ourSig.toArray, hash.toArray, outputKey.pub.value.toByteArray))

// setting auxiliary random data to all-zero yields the same result as not setting any auxiliary random data
val ourSig1 = Crypto.signSchnorr(hash, key.privateKey, TaprootTweak.NoScriptTweak, Some(ByteVector32.Zeroes))
val ourSig1 = Crypto.signSchnorr(hash, key.privateKey, Some(TaprootTweak.KeyPathTweak), Some(ByteVector32.Zeroes))
assert(ourSig == ourSig1)

// setting auxiliary random data to a non-zero value yields a different result
val ourSig2 = Crypto.signSchnorr(hash, key.privateKey, TaprootTweak.NoScriptTweak, Some(ByteVector32.One))
val ourSig2 = Crypto.signSchnorr(hash, key.privateKey, Some(TaprootTweak.KeyPathTweak), Some(ByteVector32.One))
assert(ourSig != ourSig2)
}

test("send to and spend from taproot addresses") {
val privateKey = PrivateKey(ByteVector32.fromValidHex("0101010101010101010101010101010101010101010101010101010101010101"))
val internalKey = privateKey.publicKey.xOnly
val (outputKey, _) = internalKey.outputKey(TaprootTweak.NoScriptTweak)
val (outputKey, _) = internalKey.outputKey(TaprootTweak.KeyPathTweak)
val address = Bech32.encodeWitnessAddress("tb", 1, outputKey.pub.value.toByteArray)
assert("tb1p33wm0auhr9kkahzd6l0kqj85af4cswn276hsxg6zpz85xe2r0y8snwrkwy" == address)

// this tx sends to tb1p33wm0auhr9kkahzd6l0kqj85af4cswn276hsxg6zpz85xe2r0y8snwrkwy
val tx = Transaction.read(
"02000000000101bf77ef36f2c0f32e0822cef0514948254997495a34bfba7dd4a73aabfcbb87900000000000fdffffff02c2c2000000000000160014b5c3dbfeb8e7d0c809c3ba3f815fd430777ef4be50c30000000000002251208c5db7f797196d6edc4dd7df6048f4ea6b883a6af6af032342088f436543790f0140583f758bea307216e03c1f54c3c6088e8923c8e1c89d96679fb00de9e808a79d0fba1cc3f9521cb686e8f43fb37cc6429f2e1480c70cc25ecb4ac0dde8921a01f1f70000"
)
assert(Script.pay2tr(internalKey, scripts_opt = None) == Script.parse(tx.txOut(1).publicKeyScript))
assert(Script.pay2tr(internalKey, KeyPathTweak) == Script.parse(tx.txOut(1).publicKeyScript))

// we want to spend
val Right(outputScript) = addressToPublicKeyScript(Block.Testnet3GenesisBlock.hash, "tb1pn3g330w4n5eut7d4vxq0pp303267qc6vg8d2e0ctjuqre06gs3yqnc5yx0")
Expand Down Expand Up @@ -157,7 +158,7 @@ class TaprootSpec extends FunSuite {
val internalPubkey = PublicKey.fromBin(ByteVector.fromValidHex("0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0")).xOnly

// funding tx sends to our tapscript
val fundingTx = Transaction(version = 2, txIn = Nil, txOut = Seq(TxOut(Satoshi(1000000), Script.pay2tr(internalPubkey, Some(scriptTree)))), lockTime = 0)
val fundingTx = Transaction(version = 2, txIn = Nil, txOut = Seq(TxOut(Satoshi(1000000), Script.pay2tr(internalPubkey, scriptTree))), lockTime = 0)

// create an unsigned transaction
val tmp = Transaction(
Expand Down Expand Up @@ -213,7 +214,7 @@ class TaprootSpec extends FunSuite {
val (tweakedKey, _) = internalPubkey.outputKey(scriptTree)

// this is the tapscript we send funds to
val script = Script.pay2tr(internalPubkey, Some(scriptTree))
val script = Script.pay2tr(internalPubkey, scriptTree)
val bip350Address = Bech32.encodeWitnessAddress(Bech32.hrp(blockchain), 1.toByte, tweakedKey.pub.value.toByteArray)
assert(bip350Address == "tb1p78gx95syx0qz8w5nftk8t7nce78zlpqpsxugcvq5xpfy4tvn6rasd7wk0y")
val Right(sweepPublicKeyScript) = addressToPublicKeyScript(blockchain, "tb1qxy9hhxkw7gt76qrm4yzw4j06gkk4evryh8ayp7")
Expand Down