Skip to content

Commit 7514347

Browse files
authored
Add wrapper classes for various Taproot objects (#97)
We add wrapper classes to simplify usability from Scala for: - musig2 nonces - taproot script trees - schnorr signature tweaks - taproot signature tweaks We also update the library version to include those changes in a minor release.
1 parent 54ebfb8 commit 7514347

File tree

9 files changed

+202
-67
lines changed

9 files changed

+202
-67
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<groupId>fr.acinq</groupId>
44
<artifactId>bitcoin-lib_2.13</artifactId>
55
<packaging>jar</packaging>
6-
<version>0.43</version>
6+
<version>0.43.1</version>
77
<description>Simple Scala Bitcoin library</description>
88
<url>https://github.com/ACINQ/bitcoin-lib</url>
99
<name>bitcoin-lib</name>

src/main/scala/fr/acinq/bitcoin/scalacompat/Crypto.scala

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,27 @@ import fr.acinq.bitcoin.scalacompat.KotlinUtils._
55
import scodec.bits.ByteVector
66

77
object Crypto {
8+
9+
// @formatter:off
10+
/** Specify how private keys are tweaked when creating Schnorr signatures. */
11+
sealed trait SchnorrTweak
12+
object SchnorrTweak {
13+
/** The private key is directly used, without any tweaks. */
14+
case object NoTweak extends SchnorrTweak
15+
}
16+
17+
sealed trait TaprootTweak extends SchnorrTweak
18+
object TaprootTweak {
19+
/** The private key is tweaked with H_TapTweak(public key) (this is used for key path spending when there is no script tree). */
20+
case object NoScriptTweak extends TaprootTweak
21+
/** The private key is tweaked with H_TapTweak(public key || merkle_root) (this is used for key path spending when a script tree exists). */
22+
case class ScriptTweak(merkleRoot: ByteVector32) extends TaprootTweak
23+
object ScriptTweak {
24+
def apply(scriptTree: ScriptTree): ScriptTweak = ScriptTweak(scriptTree.hash())
25+
}
26+
}
27+
// @formatter:on
28+
829
/**
930
* A bitcoin private key.
1031
* A private key is valid if it is not 0 and less than the secp256k1 curve order when interpreted as an integer (most significant byte first).
@@ -124,24 +145,24 @@ object Crypto {
124145
case class XonlyPublicKey(pub: bitcoin.XonlyPublicKey) {
125146
val publicKey: PublicKey = PublicKey(pub.getPublicKey)
126147

127-
def tweak(tapTweak: bitcoin.Crypto.TaprootTweak): ByteVector32 = pub.tweak(tapTweak)
148+
def tweak(tapTweak: TaprootTweak): ByteVector32 = pub.tweak(scala2kmp(tapTweak))
128149

129150
/**
130151
* "tweaks" this key with an optional merkle root
131152
*
132153
* @param tapTweak taproot tweak
133154
* @return an (x-only pubkey, parity) pair
134155
*/
135-
def outputKey(tapTweak: bitcoin.Crypto.TaprootTweak): (XonlyPublicKey, Boolean) = {
136-
val p = pub.outputKey(tapTweak)
156+
def outputKey(tapTweak: TaprootTweak): (XonlyPublicKey, Boolean) = {
157+
val p = pub.outputKey(scala2kmp(tapTweak))
137158
(XonlyPublicKey(p.getFirst), p.getSecond)
138159
}
139160

140161
/** Tweak this key with the merkle root of the given script tree. */
141-
def outputKey(scriptTree: bitcoin.ScriptTree): (XonlyPublicKey, Boolean) = outputKey(new bitcoin.Crypto.TaprootTweak.ScriptTweak(scriptTree))
162+
def outputKey(scriptTree: ScriptTree): (XonlyPublicKey, Boolean) = outputKey(TaprootTweak.ScriptTweak(scriptTree))
142163

143164
/** Tweak this key with the merkle root provided. */
144-
def outputKey(merkleRoot: ByteVector32): (XonlyPublicKey, Boolean) = outputKey(new bitcoin.Crypto.TaprootTweak.ScriptTweak(merkleRoot))
165+
def outputKey(merkleRoot: ByteVector32): (XonlyPublicKey, Boolean) = outputKey(TaprootTweak.ScriptTweak(merkleRoot))
145166

146167
/**
147168
* add a public key to this x-only key
@@ -274,8 +295,8 @@ object Crypto {
274295
* the key (there is an extra "1" appended to the key)
275296
* @return a signature in compact format (64 bytes)
276297
*/
277-
def signSchnorr(data: ByteVector32, privateKey: PrivateKey, schnorrTweak: bitcoin.Crypto.SchnorrTweak = bitcoin.Crypto.SchnorrTweak.NoTweak.INSTANCE, auxrand32: Option[ByteVector32] = None): ByteVector64 = {
278-
bitcoin.Crypto.signSchnorr(data, privateKey, schnorrTweak, auxrand32.map(scala2kmp).orNull)
298+
def signSchnorr(data: ByteVector32, privateKey: PrivateKey, schnorrTweak: SchnorrTweak = SchnorrTweak.NoTweak, auxrand32: Option[ByteVector32] = None): ByteVector64 = {
299+
bitcoin.Crypto.signSchnorr(data, privateKey, scala2kmp(schnorrTweak), auxrand32.map(scala2kmp).orNull)
279300
}
280301

281302
/**

src/main/scala/fr/acinq/bitcoin/scalacompat/KotlinUtils.scala

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package fr.acinq.bitcoin.scalacompat
22

33
import fr.acinq.bitcoin
4-
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey}
4+
import fr.acinq.bitcoin.scalacompat.Crypto._
55
import scodec.bits.ByteVector
66

77
import java.io.{InputStream, OutputStream}
88
import scala.jdk.CollectionConverters.{ListHasAsScala, SeqHasAsJava}
99

1010
object KotlinUtils {
11+
1112
implicit def kmp2scala(input: bitcoin.ByteVector32): ByteVector32 = ByteVector32(ByteVector(input.toByteArray))
1213

1314
implicit def scala2kmp(input: ByteVector32): bitcoin.ByteVector32 = new bitcoin.ByteVector32(input.toArray)
@@ -44,6 +45,47 @@ object KotlinUtils {
4445

4546
implicit def scala2kmp(input: ScriptWitness): bitcoin.ScriptWitness = new bitcoin.ScriptWitness(input.stack.map(scala2kmp).asJava)
4647

48+
implicit def kmp2scala(input: bitcoin.ScriptTree.Leaf): ScriptTree.Leaf = ScriptTree.Leaf(kmp2scala(input.getScript), input.getLeafVersion)
49+
50+
implicit def scala2kmp(input: ScriptTree.Leaf): bitcoin.ScriptTree.Leaf = new bitcoin.ScriptTree.Leaf(scala2kmp(input.script), input.leafVersion)
51+
52+
implicit def kmp2scala(input: bitcoin.ScriptTree.Branch): ScriptTree.Branch = ScriptTree.Branch(kmp2scala(input.getLeft), kmp2scala(input.getRight))
53+
54+
implicit def scala2kmp(input: ScriptTree.Branch): bitcoin.ScriptTree.Branch = new bitcoin.ScriptTree.Branch(scala2kmp(input.left), scala2kmp(input.right))
55+
56+
implicit def kmp2scala(input: bitcoin.ScriptTree): ScriptTree = input match {
57+
case branch: bitcoin.ScriptTree.Branch => kmp2scala(branch)
58+
case leaf: bitcoin.ScriptTree.Leaf => kmp2scala(leaf)
59+
case _ => ??? // this cannot happen, but the compiler cannot know that there aren't other cases
60+
}
61+
62+
implicit def scala2kmp(input: ScriptTree): bitcoin.ScriptTree = input match {
63+
case leaf: ScriptTree.Leaf => scala2kmp(leaf)
64+
case branch: ScriptTree.Branch => scala2kmp(branch)
65+
}
66+
67+
implicit def kmp2scala(input: bitcoin.Crypto.TaprootTweak): TaprootTweak = input match {
68+
case bitcoin.Crypto.TaprootTweak.NoScriptTweak.INSTANCE => TaprootTweak.NoScriptTweak
69+
case tweak: bitcoin.Crypto.TaprootTweak.ScriptTweak => TaprootTweak.ScriptTweak(kmp2scala(tweak.getMerkleRoot))
70+
case _ => ??? // this cannot happen, but the compiler cannot know that there aren't other cases
71+
}
72+
73+
implicit def scala2kmp(input: TaprootTweak): bitcoin.Crypto.TaprootTweak = input match {
74+
case TaprootTweak.NoScriptTweak => bitcoin.Crypto.TaprootTweak.NoScriptTweak.INSTANCE
75+
case tweak: TaprootTweak.ScriptTweak => new bitcoin.Crypto.TaprootTweak.ScriptTweak(scala2kmp(tweak.merkleRoot))
76+
}
77+
78+
implicit def kmp2scala(input: bitcoin.Crypto.SchnorrTweak): SchnorrTweak = input match {
79+
case bitcoin.Crypto.SchnorrTweak.NoTweak.INSTANCE => SchnorrTweak.NoTweak
80+
case tweak: bitcoin.Crypto.TaprootTweak => kmp2scala(tweak)
81+
case _ => ??? // this cannot happen, but the compiler cannot know that there aren't other cases
82+
}
83+
84+
implicit def scala2kmp(input: SchnorrTweak): bitcoin.Crypto.SchnorrTweak = input match {
85+
case SchnorrTweak.NoTweak => bitcoin.Crypto.SchnorrTweak.NoTweak.INSTANCE
86+
case tweak: TaprootTweak => scala2kmp(tweak)
87+
}
88+
4789
implicit def kmp2scala(input: bitcoin.TxIn): TxIn = TxIn(input.outPoint, input.signatureScript, input.sequence, input.witness)
4890

4991
implicit def scala2kmp(input: Satoshi): bitcoin.Satoshi = new bitcoin.Satoshi(input.toLong)
@@ -229,5 +271,6 @@ object KotlinUtils {
229271
OP_INVALIDOPCODE -> bitcoin.OP_INVALIDOPCODE.INSTANCE)
230272

231273
private val scriptEltMapKmp2Scala2Map: Map[bitcoin.ScriptElt, ScriptElt] = scriptEltMapScala2Kmp.map { case (k, v) => v -> k }
274+
232275
}
233276

src/main/scala/fr/acinq/bitcoin/scalacompat/Musig2.scala

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,51 @@
11
package fr.acinq.bitcoin.scalacompat
22

3-
import fr.acinq.bitcoin.ScriptTree
4-
import fr.acinq.bitcoin.crypto.musig2.{IndividualNonce, SecretNonce}
3+
import fr.acinq.bitcoin.crypto.musig2
54
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey}
65
import fr.acinq.bitcoin.scalacompat.KotlinUtils._
6+
import fr.acinq.secp256k1.Secp256k1
7+
import scodec.bits.ByteVector
78

89
import scala.jdk.CollectionConverters.SeqHasAsJava
910

1011
object Musig2 {
1112

13+
/**
14+
* Musig2 secret nonce, that should be treated as a private opaque blob.
15+
* This nonce must never be persisted or reused across signing sessions.
16+
*/
17+
case class SecretNonce(inner: musig2.SecretNonce)
18+
19+
/**
20+
* Musig2 public nonce, that must be shared with other participants in the signing session.
21+
* It contains two elliptic curve points, but should be treated as an opaque blob.
22+
*/
23+
case class IndividualNonce(data: ByteVector) {
24+
require(data.size == Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE, "invalid musig2 public nonce size")
25+
}
26+
27+
/** A locally-generated nonce, for which both the secret and public parts are known. */
28+
case class LocalNonce(secretNonce: SecretNonce, publicNonce: IndividualNonce)
29+
1230
/**
1331
* Aggregate the public keys of a musig2 session into a single public key.
1432
* Note that this function doesn't apply any tweak: when used for taproot, it computes the internal public key, not
1533
* the public key exposed in the script (which is tweaked with the script tree).
1634
*
1735
* @param publicKeys public keys of all participants: callers must verify that all public keys are valid.
1836
*/
19-
def aggregateKeys(publicKeys: Seq[PublicKey]): XonlyPublicKey = XonlyPublicKey(fr.acinq.bitcoin.crypto.musig2.Musig2.aggregateKeys(publicKeys.map(scala2kmp).asJava))
37+
def aggregateKeys(publicKeys: Seq[PublicKey]): XonlyPublicKey = XonlyPublicKey(musig2.Musig2.aggregateKeys(publicKeys.map(scala2kmp).asJava))
2038

2139
/**
22-
* @param sessionId a random, unique session ID.
23-
* @param signingKey either the signer's private key or public key
24-
* @param publicKeys public keys of all participants: callers must verify that all public keys are valid.
25-
* @param message_opt (optional) message that will be signed, if already known.
40+
* @param sessionId a random, unique session ID.
41+
* @param signingKey either the signer's private key or public key
42+
* @param publicKeys public keys of all participants: callers must verify that all public keys are valid.
43+
* @param message_opt (optional) message that will be signed, if already known.
2644
* @param extraInput_opt (optional) additional random data.
2745
*/
28-
def generateNonce(sessionId: ByteVector32, signingKey: Either[PrivateKey, PublicKey], publicKeys: Seq[PublicKey], message_opt: Option[ByteVector32], extraInput_opt: Option[ByteVector32]): (SecretNonce, IndividualNonce) = {
29-
val nonce = fr.acinq.bitcoin.crypto.musig2.Musig2.generateNonce(sessionId, either2keitherkmp(signingKey.map(scala2kmp).left.map(scala2kmp)), publicKeys.map(scala2kmp).asJava, message_opt.map(scala2kmp).orNull, extraInput_opt.map(scala2kmp).orNull)
30-
(nonce.getFirst, nonce.getSecond)
46+
def generateNonce(sessionId: ByteVector32, signingKey: Either[PrivateKey, PublicKey], publicKeys: Seq[PublicKey], message_opt: Option[ByteVector32], extraInput_opt: Option[ByteVector32]): LocalNonce = {
47+
val nonce = musig2.Musig2.generateNonce(sessionId, either2keitherkmp(signingKey.map(scala2kmp).left.map(scala2kmp)), publicKeys.map(scala2kmp).asJava, message_opt.map(scala2kmp).orNull, extraInput_opt.map(scala2kmp).orNull)
48+
LocalNonce(SecretNonce(nonce.getFirst), IndividualNonce(nonce.getSecond.getData))
3149
}
3250

3351
/**
@@ -37,9 +55,9 @@ object Musig2 {
3755
* @param message_opt (optional) message that will be signed, if already known.
3856
* @param extraInput_opt (optional) additional random data.
3957
*/
40-
def generateNonceWithCounter(nonRepeatingCounter: Long, privateKey: PrivateKey, publicKeys: Seq[PublicKey], message_opt: Option[ByteVector32], extraInput_opt: Option[ByteVector32]): (SecretNonce, IndividualNonce) = {
41-
val nonce = fr.acinq.bitcoin.crypto.musig2.Musig2.generateNonceWithCounter(nonRepeatingCounter, privateKey, publicKeys.map(scala2kmp).asJava, message_opt.map(scala2kmp).orNull, extraInput_opt.map(scala2kmp).orNull)
42-
(nonce.getFirst, nonce.getSecond)
58+
def generateNonceWithCounter(nonRepeatingCounter: Long, privateKey: PrivateKey, publicKeys: Seq[PublicKey], message_opt: Option[ByteVector32], extraInput_opt: Option[ByteVector32]): LocalNonce = {
59+
val nonce = musig2.Musig2.generateNonceWithCounter(nonRepeatingCounter, privateKey, publicKeys.map(scala2kmp).asJava, message_opt.map(scala2kmp).orNull, extraInput_opt.map(scala2kmp).orNull)
60+
LocalNonce(SecretNonce(nonce.getFirst), IndividualNonce(nonce.getSecond.getData))
4361
}
4462

4563
/**
@@ -55,7 +73,7 @@ object Musig2 {
5573
* @param scriptTree_opt tapscript tree of the taproot input, if it has script paths.
5674
*/
5775
def signTaprootInput(privateKey: PrivateKey, tx: Transaction, inputIndex: Int, inputs: Seq[TxOut], publicKeys: Seq[PublicKey], secretNonce: SecretNonce, publicNonces: Seq[IndividualNonce], scriptTree_opt: Option[ScriptTree]): Either[Throwable, ByteVector32] = {
58-
fr.acinq.bitcoin.crypto.musig2.Musig2.signTaprootInput(privateKey, tx, inputIndex, inputs.map(scala2kmp).asJava, publicKeys.map(scala2kmp).asJava, secretNonce, publicNonces.asJava, scriptTree_opt.orNull).map(kmp2scala)
76+
musig2.Musig2.signTaprootInput(privateKey, tx, inputIndex, inputs.map(scala2kmp).asJava, publicKeys.map(scala2kmp).asJava, secretNonce.inner, publicNonces.map(n => new musig2.IndividualNonce(n.data.toArray)).asJava, scriptTree_opt.map(scala2kmp).orNull).map(kmp2scala)
5977
}
6078

6179
/**
@@ -73,7 +91,7 @@ object Musig2 {
7391
* @return true if the partial signature is valid.
7492
*/
7593
def verifyTaprootSignature(partialSig: ByteVector32, nonce: IndividualNonce, publicKey: PublicKey, tx: Transaction, inputIndex: Int, inputs: Seq[TxOut], publicKeys: Seq[PublicKey], publicNonces: Seq[IndividualNonce], scriptTree_opt: Option[ScriptTree]): Boolean = {
76-
fr.acinq.bitcoin.crypto.musig2.Musig2.verify(partialSig, nonce, publicKey, tx, inputIndex, inputs.map(scala2kmp).asJava, publicKeys.map(scala2kmp).asJava, publicNonces.asJava, scriptTree_opt.orNull)
94+
musig2.Musig2.verify(partialSig, new musig2.IndividualNonce(nonce.data.toArray), publicKey, tx, inputIndex, inputs.map(scala2kmp).asJava, publicKeys.map(scala2kmp).asJava, publicNonces.map(n => new musig2.IndividualNonce(n.data.toArray)).asJava, scriptTree_opt.map(scala2kmp).orNull)
7795
}
7896

7997
/**
@@ -88,7 +106,7 @@ object Musig2 {
88106
* @param scriptTree_opt tapscript tree of the taproot input, if it has script paths.
89107
*/
90108
def aggregateTaprootSignatures(partialSigs: Seq[ByteVector32], tx: Transaction, inputIndex: Int, inputs: Seq[TxOut], publicKeys: Seq[PublicKey], publicNonces: Seq[IndividualNonce], scriptTree_opt: Option[ScriptTree]): Either[Throwable, ByteVector64] = {
91-
fr.acinq.bitcoin.crypto.musig2.Musig2.aggregateTaprootSignatures(partialSigs.map(scala2kmp).asJava, tx, inputIndex, inputs.map(scala2kmp).asJava, publicKeys.map(scala2kmp).asJava, publicNonces.asJava, scriptTree_opt.orNull).map(kmp2scala)
109+
musig2.Musig2.aggregateTaprootSignatures(partialSigs.map(scala2kmp).asJava, tx, inputIndex, inputs.map(scala2kmp).asJava, publicKeys.map(scala2kmp).asJava, publicNonces.map(n => new musig2.IndividualNonce(n.data.toArray)).asJava, scriptTree_opt.map(scala2kmp).orNull).map(kmp2scala)
92110
}
93111

94112
}

src/main/scala/fr/acinq/bitcoin/scalacompat/Script.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ object Script {
175175
* @param internalKey internal public key that will be tweaked with the [scripts] provided.
176176
* @param scripts_opt optional spending scripts that can be used instead of key-path spending.
177177
*/
178-
def pay2tr(internalKey: XonlyPublicKey, scripts_opt: Option[bitcoin.ScriptTree]): Seq[ScriptElt] = bitcoin.Script.pay2tr(internalKey.pub, scripts_opt.orNull).asScala.map(kmp2scala).toList
178+
def pay2tr(internalKey: XonlyPublicKey, scripts_opt: Option[ScriptTree]): Seq[ScriptElt] = bitcoin.Script.pay2tr(internalKey.pub, scripts_opt.map(scala2kmp).orNull).asScala.map(kmp2scala).toList
179179

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

@@ -188,6 +188,6 @@ object Script {
188188
* @param witness witness for the spent [script].
189189
* @param scriptTree tapscript tree.
190190
*/
191-
def witnessScriptPathPay2tr(internalKey: XonlyPublicKey, script: bitcoin.ScriptTree.Leaf, witness: ScriptWitness, scriptTree: bitcoin.ScriptTree): ScriptWitness = bitcoin.Script.witnessScriptPathPay2tr(internalKey.pub, script, witness, scriptTree)
191+
def witnessScriptPathPay2tr(internalKey: XonlyPublicKey, script: ScriptTree.Leaf, witness: ScriptWitness, scriptTree: ScriptTree): ScriptWitness = bitcoin.Script.witnessScriptPathPay2tr(internalKey.pub, scala2kmp(script), witness, scala2kmp(scriptTree))
192192

193193
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package fr.acinq.bitcoin.scalacompat
2+
3+
import scodec.bits.ByteVector
4+
5+
/** Simple binary tree structure containing taproot spending scripts. */
6+
sealed trait ScriptTree {
7+
8+
/** Compute the merkle root of the script tree. */
9+
def hash(): ByteVector32 = KotlinUtils.kmp2scala(KotlinUtils.scala2kmp(this).hash())
10+
11+
/** Return the first leaf with a matching script, if any. */
12+
def findScript(script: ByteVector): Option[ScriptTree.Leaf] = this match {
13+
case leaf: ScriptTree.Leaf if leaf.script == script => Some(leaf)
14+
case _: ScriptTree.Leaf => None
15+
case branch: ScriptTree.Branch => branch.left.findScript(script).orElse(branch.right.findScript(script))
16+
}
17+
18+
/** Return the first leaf with a matching leaf hash, if any. */
19+
def findScript(leafHash: ByteVector32): Option[ScriptTree.Leaf] = this match {
20+
case leaf: ScriptTree.Leaf if leaf.hash() == leafHash => Some(leaf)
21+
case _: ScriptTree.Leaf => None
22+
case branch: ScriptTree.Branch => branch.left.findScript(leafHash).orElse(branch.right.findScript(leafHash))
23+
}
24+
25+
/**
26+
* Compute a merkle proof for the given script leaf.
27+
* This merkle proof is encoded for creating control blocks in taproot script path witnesses.
28+
* If the leaf doesn't belong to the script tree, this function will return None.
29+
*/
30+
def merkleProof(leafHash: ByteVector32): Option[ByteVector] = {
31+
val proof_opt = KotlinUtils.scala2kmp(this).merkleProof(KotlinUtils.scala2kmp(leafHash))
32+
if (proof_opt == null) None else Some(ByteVector(proof_opt))
33+
}
34+
35+
}
36+
37+
object ScriptTree {
38+
/**
39+
* Multiple spending scripts can be placed in the leaves of a taproot tree. When using one of those scripts to spend
40+
* funds, we only need to reveal that specific script and a merkle proof that it is a leaf of the tree.
41+
*
42+
* @param script serialized spending script.
43+
* @param leafVersion tapscript version.
44+
*/
45+
case class Leaf(script: ByteVector, leafVersion: Int) extends ScriptTree
46+
47+
object Leaf {
48+
// @formatter:off
49+
def apply(script: ByteVector): Leaf = Leaf(script, fr.acinq.bitcoin.Script.TAPROOT_LEAF_TAPSCRIPT)
50+
def apply(script: Seq[ScriptElt]): Leaf = Leaf(script, fr.acinq.bitcoin.Script.TAPROOT_LEAF_TAPSCRIPT)
51+
def apply(script: Seq[ScriptElt], leafVersion: Int): Leaf = Leaf(Script.write(script), leafVersion)
52+
// @formatter:on
53+
}
54+
55+
case class Branch(left: ScriptTree, right: ScriptTree) extends ScriptTree
56+
}

0 commit comments

Comments
 (0)