Skip to content

Commit 18a7814

Browse files
authored
Add ScriptTree helper to find leaf by hash (#148)
This allows us to store a `ScriptTree` with only the hash of the leaf we want to spend, and fetch the actual script from the tree. This avoids duplicating the script in our encoded data in `eclair` or `lightning-kmp`.
1 parent a2ba3f9 commit 18a7814

File tree

3 files changed

+28
-12
lines changed

3 files changed

+28
-12
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ build/
33

44
# Idea
55
.idea
6+
.kotlin
67
*.iml
78

89
# Gradle

src/commonMain/kotlin/fr/acinq/bitcoin/ScriptTree.kt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,17 @@
1515
*/
1616
package fr.acinq.bitcoin
1717

18-
import fr.acinq.bitcoin.io.ByteArrayInput
1918
import fr.acinq.bitcoin.io.ByteArrayOutput
2019
import fr.acinq.bitcoin.io.Input
2120
import fr.acinq.bitcoin.io.Output
2221
import kotlin.jvm.JvmStatic
2322

2423
/** Simple binary tree structure containing taproot spending scripts. */
2524
public sealed class ScriptTree {
26-
public abstract fun write(output: Output, level: Int): Unit
25+
public abstract fun write(output: Output, level: Int)
2726

2827
/**
29-
* @return the tree serialised with the format defined in BIP 371
28+
* @return the tree serialized with the format defined in BIP 371
3029
*/
3130
public fun write(): ByteArray {
3231
val output = ByteArrayOutput()
@@ -46,15 +45,15 @@ public sealed class ScriptTree {
4645
public constructor(script: List<ScriptElt>, leafVersion: Int) : this(Script.write(script).byteVector(), leafVersion)
4746
public constructor(script: String, leafVersion: Int) : this(ByteVector.fromHex(script), leafVersion)
4847

49-
override fun write(output: Output, level: Int): Unit {
48+
override fun write(output: Output, level: Int) {
5049
output.write(level)
5150
output.write(leafVersion)
5251
BtcSerializer.writeScript(script, output)
5352
}
5453
}
5554

5655
public data class Branch(val left: ScriptTree, val right: ScriptTree) : ScriptTree() {
57-
override fun write(output: Output, level: Int): Unit {
56+
override fun write(output: Output, level: Int) {
5857
left.write(output, level + 1)
5958
right.write(output, level + 1)
6059
}
@@ -68,7 +67,6 @@ public sealed class ScriptTree {
6867
BtcSerializer.writeScript(this.script, buffer)
6968
Crypto.taggedHash(buffer.toByteArray(), "TapLeaf")
7069
}
71-
7270
is Branch -> {
7371
val h1 = this.left.hash()
7472
val h2 = this.right.hash()
@@ -83,6 +81,12 @@ public sealed class ScriptTree {
8381
is Branch -> this.left.findScript(script) ?: this.right.findScript(script)
8482
}
8583

84+
/** Return the first leaf with a matching leaf hash, if any. */
85+
public fun findScript(leafHash: ByteVector32): Leaf? = when (this) {
86+
is Leaf -> if (this.hash() == leafHash) this else null
87+
is Branch -> this.left.findScript(leafHash) ?: this.right.findScript(leafHash)
88+
}
89+
8690
/**
8791
* Compute a merkle proof for the given script leaf.
8892
* This merkle proof is encoded for creating control blocks in taproot script path witnesses.
@@ -128,7 +132,7 @@ public sealed class ScriptTree {
128132
public fun read(input: Input): ScriptTree {
129133
val leaves = readLeaves(input)
130134
merge(leaves)
131-
require(leaves.size == 1) { "invalid serialised script tree" }
135+
require(leaves.size == 1) { "invalid serialized script tree" }
132136
return leaves[0].second
133137
}
134138
}

src/commonTest/kotlin/fr/acinq/bitcoin/TaprootTestsCommon.kt

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -415,17 +415,28 @@ class TaprootTestsCommon {
415415

416416
@Test
417417
fun `serialize script tree -- reference test`() {
418-
val tree =
419-
ScriptTree.read(ByteArrayInput(Hex.decode("02c02220736e572900fe1252589a2143c8f3c79f71a0412d2353af755e9701c782694a02ac02c02220631c5f3b5832b8fbdebfb19704ceeb323c21f40f7a24f43d68ef0cc26b125969ac01c0222044faa49a0338de488c8dfffecdfb6f329f380bd566ef20c8df6d813eab1c4273ac")))
418+
val encoded = "02c02220736e572900fe1252589a2143c8f3c79f71a0412d2353af755e9701c782694a02ac02c02220631c5f3b5832b8fbdebfb19704ceeb323c21f40f7a24f43d68ef0cc26b125969ac01c0222044faa49a0338de488c8dfffecdfb6f329f380bd566ef20c8df6d813eab1c4273ac"
419+
val tree = ScriptTree.read(ByteArrayInput(Hex.decode(encoded)))
420+
val leaves = listOf(
421+
ScriptTree.Leaf("20736e572900fe1252589a2143c8f3c79f71a0412d2353af755e9701c782694a02ac", 0xc0),
422+
ScriptTree.Leaf("20631c5f3b5832b8fbdebfb19704ceeb323c21f40f7a24f43d68ef0cc26b125969ac", 0xc0),
423+
ScriptTree.Leaf("2044faa49a0338de488c8dfffecdfb6f329f380bd566ef20c8df6d813eab1c4273ac", 0xc0),
424+
)
420425
assertEquals(
421426
ScriptTree.Branch(
422427
ScriptTree.Branch(
423-
ScriptTree.Leaf("20736e572900fe1252589a2143c8f3c79f71a0412d2353af755e9701c782694a02ac", 0xc0),
424-
ScriptTree.Leaf("20631c5f3b5832b8fbdebfb19704ceeb323c21f40f7a24f43d68ef0cc26b125969ac", 0xc0),
428+
leaves[0],
429+
leaves[1],
425430
),
426-
ScriptTree.Leaf("2044faa49a0338de488c8dfffecdfb6f329f380bd566ef20c8df6d813eab1c4273ac", 0xc0)
431+
leaves[2]
427432
), tree
428433
)
434+
// We're able to find leaves in that script tree.
435+
leaves.forEach { l ->
436+
assertEquals(l, tree.findScript(l.script))
437+
assertEquals(l, tree.findScript(l.hash()))
438+
}
439+
assertNull(tree.findScript(ByteVector.fromHex("deadbeef")))
429440
}
430441

431442
@Test

0 commit comments

Comments
 (0)