Skip to content

Commit 8bd1397

Browse files
authored
Add support for standard P2A scripts (#164)
In order to support v3 commitments in lightning, we need to use P2A (pay-to-anchor) outputs, which are currently rejected by our script interpreter. See bitcoin/bitcoin#30352 where this output type was introduced to `bitcoind`. Their amount can be set to dust with bitcoin/bitcoin#30239. Both of those changes are available in Bitcoin Core v29.
1 parent cec2a87 commit 8bd1397

File tree

3 files changed

+84
-7
lines changed

3 files changed

+84
-7
lines changed

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ plugins {
1313
val currentOs = org.gradle.internal.os.OperatingSystem.current()
1414

1515
group = "fr.acinq.bitcoin"
16-
version = "0.27.0"
16+
version = "0.27.1-SNAPSHOT"
1717

1818
repositories {
1919
google()

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,14 @@ public object Script {
524524
return ScriptWitness(witness.stack + script.script + controlBlock)
525525
}
526526

527+
/** Standard P2A (pay-to-anchor) output. */
528+
@JvmStatic
529+
public val pay2anchor: List<ScriptElt> = listOf(OP_1, OP_PUSHDATA(ByteVector("4e73")))
530+
531+
/** An empty witness script is used to spend [pay2anchor] outputs. */
532+
@JvmStatic
533+
public val witnessPay2anchor: ScriptWitness = ScriptWitness.empty
534+
527535
public fun removeSignature(script: List<ScriptElt>, signature: ByteVector): List<ScriptElt> {
528536
val toRemove = OP_PUSHDATA(signature)
529537
return script.filterNot { it == toRemove }
@@ -1488,12 +1496,11 @@ public object Script {
14881496
}
14891497
}
14901498
}
1491-
1499+
// Standard P2A script (see github.com/bitcoin/bitcoin/pull/30352).
1500+
witnessVersion == 1L && program.contentEquals(byteArrayOf(0x4e, 0x73)) -> require(witness == witnessPay2anchor) { "P2A output must be spent with an empty witness" }
14921501
(scriptFlag and ScriptFlags.SCRIPT_VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM) != 0 -> throw IllegalArgumentException("Witness version $witnessVersion reserved for soft-fork upgrades")
1493-
else -> {
1494-
// Higher version witness scripts return true for future softfork compatibility
1495-
return
1496-
}
1502+
// Higher version witness scripts return true for future softfork compatibility
1503+
else -> {}
14971504
}
14981505
}
14991506

src/commonTest/kotlin/fr/acinq/bitcoin/reference/TransactionTestsCommon.kt

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import fr.acinq.bitcoin.*
2020
import kotlinx.serialization.json.*
2121
import kotlin.test.Test
2222
import kotlin.test.assertEquals
23+
import kotlin.test.assertFails
2324
import kotlin.test.fail
2425

2526
class TransactionTestsCommon {
@@ -91,7 +92,7 @@ class TransactionTestsCommon {
9192
}
9293
}
9394
true
94-
} catch (t: Throwable) {
95+
} catch (_: Throwable) {
9596
false
9697
}
9798
assertEquals(valid, result, "failed valid=$valid test $testCase")
@@ -117,6 +118,75 @@ class TransactionTestsCommon {
117118
assertEquals(93, count)
118119
}
119120

121+
@Test
122+
fun `TRUC transaction with p2a -- no external input`() {
123+
val priv = PrivateKey.fromHex("de1fa92dc352791cb83646513bda82bc2d44b80b42d6efceca6789a2b1b34bb8")
124+
// The parent transaction usually doesn't pay any fees.
125+
val parentTx = Transaction(
126+
version = 3,
127+
txIn = listOf(TxIn(OutPoint(TxId("007ef4c2f775ae04b67f942cd1e1dc4eb950f857401315e2aaad45eac1f355fa"), 1), 0)),
128+
txOut = listOf(
129+
TxOut(100_000.sat(), Script.pay2wpkh(priv.publicKey())),
130+
TxOut(0.sat(), Script.pay2anchor),
131+
),
132+
lockTime = 0
133+
)
134+
// The child transaction is used to pay fees (CPFP using P2A).
135+
// In this example, we also spend the p2wpkh output to pay the fees.
136+
val unsignedChildTx = Transaction(
137+
version = 3,
138+
txIn = listOf(
139+
TxIn(OutPoint(parentTx.txid, 0), 0),
140+
TxIn(OutPoint(parentTx.txid, 1), 0),
141+
),
142+
txOut = listOf(TxOut(95_000.sat(), Script.pay2wpkh(priv.publicKey()))),
143+
lockTime = 0
144+
)
145+
val sig0 = unsignedChildTx.signInput(0, Script.pay2pkh(priv.publicKey()), SigHash.SIGHASH_ALL, 100_000.sat(), SigVersion.SIGVERSION_WITNESS_V0, priv)
146+
val childTx = unsignedChildTx
147+
.updateWitness(0, Script.witnessPay2wpkh(priv.publicKey(), sig0.byteVector()))
148+
.updateWitness(1, Script.witnessPay2anchor)
149+
Transaction.correctlySpends(childTx, listOf(parentTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
150+
// The anchor output MUST have an empty witness to be valid.
151+
assertFails { Transaction.correctlySpends(childTx.updateWitness(1, ScriptWitness(listOf(ByteVector("deadbeef")))), listOf(parentTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }
152+
}
153+
154+
@Test
155+
fun `TRUC transaction with p2a -- external input`() {
156+
val priv = PrivateKey.fromHex("de1fa92dc352791cb83646513bda82bc2d44b80b42d6efceca6789a2b1b34bb8")
157+
// The parent transaction usually doesn't pay any fees.
158+
val parentTx = Transaction(
159+
version = 3,
160+
txIn = listOf(TxIn(OutPoint(TxId("cf637c92da728399142665f03ed7451ed5c4e501015189f593b6e5c878e40d72"), 0), 0)),
161+
txOut = listOf(
162+
TxOut(50_000.sat(), Script.pay2wsh(ByteVector("deadbeef"))),
163+
TxOut(0.sat(), Script.pay2anchor),
164+
),
165+
lockTime = 0
166+
)
167+
// The following transaction has been confirmed: its output will be used to pay fees for the parent transaction.
168+
val walletTx = Transaction(
169+
version = 2,
170+
txIn = listOf(TxIn(OutPoint(TxId("72d59754a65ac76c75d51484279f083862cdd8d067efb1ab07e1bf9185e8436d"), 0), 0)),
171+
txOut = listOf(TxOut(100_000.sat(), Script.pay2wpkh(priv.publicKey()))),
172+
lockTime = 0
173+
)
174+
val unsignedChildTx = Transaction(
175+
version = 3,
176+
txIn = listOf(
177+
TxIn(OutPoint(walletTx, 0), 0),
178+
TxIn(OutPoint(parentTx, 1), 0),
179+
),
180+
txOut = listOf(TxOut(95_000.sat(), Script.pay2wpkh(priv.publicKey()))),
181+
lockTime = 0
182+
)
183+
val sig0 = unsignedChildTx.signInput(0, Script.pay2pkh(priv.publicKey()), SigHash.SIGHASH_ALL, 100_000.sat(), SigVersion.SIGVERSION_WITNESS_V0, priv)
184+
val childTx = unsignedChildTx
185+
.updateWitness(0, Script.witnessPay2wpkh(priv.publicKey(), sig0.byteVector()))
186+
.updateWitness(1, Script.witnessPay2anchor)
187+
Transaction.correctlySpends(childTx, listOf(walletTx, parentTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
188+
}
189+
120190
@Test
121191
fun `input and output weights`() {
122192
val publicKey1 = PublicKey.fromHex("03949633a194a43a310c5a593aada2f2d4a4e3c181880e2b396facfb2130a7f0b5")

0 commit comments

Comments
 (0)