Skip to content

Commit 331cc66

Browse files
Implement interactive-tx construction (#349)
We implement the protocol described in the dual funding specification (see lightning/bolts#851) to collaboratively create a shared transaction. We don't support yet funding and signing such transactions.
1 parent 295e0fc commit 331cc66

File tree

7 files changed

+1184
-4
lines changed

7 files changed

+1184
-4
lines changed

src/commonMain/kotlin/fr/acinq/lightning/channel/Channel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,7 @@ sealed class ChannelStateWithCommitments : ChannelState() {
552552
return Pair(Aborted(staticParams, currentTip, currentOnChainFeerates), listOf(ChannelAction.Message.Send(error)))
553553
}
554554
}
555+
555556
object Channel {
556557
// see https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#requirements
557558
const val ANNOUNCEMENTS_MINCONF = 6

src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt

Lines changed: 278 additions & 0 deletions
Large diffs are not rendered by default.

src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ typealias TransactionsCommitmentOutputs = List<Transactions.CommitmentOutputLink
3636
/**
3737
* Created by PM on 15/12/2016.
3838
*/
39-
@OptIn(ExperimentalStdlibApi::class, ExperimentalUnsignedTypes::class)
4039
object Transactions {
4140

41+
const val MAX_STANDARD_TX_WEIGHT = 400_000
42+
4243
@Serializable
4344
data class InputInfo constructor(
4445
@Contextual val outPoint: OutPoint,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package fr.acinq.lightning.wire
2+
3+
import fr.acinq.bitcoin.Satoshi
4+
import fr.acinq.bitcoin.io.Input
5+
import fr.acinq.bitcoin.io.Output
6+
import fr.acinq.lightning.utils.sat
7+
import kotlinx.serialization.Contextual
8+
import kotlinx.serialization.Serializable
9+
10+
sealed class TxAddInputTlv : Tlv
11+
12+
sealed class TxAddOutputTlv : Tlv
13+
14+
sealed class TxRemoveInputTlv : Tlv
15+
16+
sealed class TxRemoveOutputTlv : Tlv
17+
18+
sealed class TxCompleteTlv : Tlv
19+
20+
sealed class TxSignaturesTlv : Tlv
21+
22+
sealed class TxInitRbfTlv : Tlv {
23+
/** Amount that the peer will contribute to the transaction's shared output. */
24+
@Serializable
25+
data class SharedOutputContributionTlv(@Contextual val amount: Satoshi) : TxInitRbfTlv() {
26+
override val tag: Long get() = SharedOutputContributionTlv.tag
27+
28+
override fun write(out: Output) = LightningCodecs.writeTU64(amount.toLong(), out)
29+
30+
companion object : TlvValueReader<SharedOutputContributionTlv> {
31+
const val tag: Long = 0
32+
33+
override fun read(input: Input): SharedOutputContributionTlv = SharedOutputContributionTlv(LightningCodecs.tu64(input).sat)
34+
}
35+
}
36+
}
37+
38+
sealed class TxAckRbfTlv : Tlv {
39+
/** Amount that the peer will contribute to the transaction's shared output. */
40+
@Serializable
41+
data class SharedOutputContributionTlv(@Contextual val amount: Satoshi) : TxAckRbfTlv() {
42+
override val tag: Long get() = SharedOutputContributionTlv.tag
43+
44+
override fun write(out: Output) = LightningCodecs.writeTU64(amount.toLong(), out)
45+
46+
companion object : TlvValueReader<SharedOutputContributionTlv> {
47+
const val tag: Long = 0
48+
49+
override fun read(input: Input): SharedOutputContributionTlv = SharedOutputContributionTlv(LightningCodecs.tu64(input).sat)
50+
}
51+
}
52+
}
53+
54+
sealed class TxAbortTlv : Tlv

src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt

Lines changed: 271 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ interface LightningMessage {
5858
FundingCreated.type -> FundingCreated.read(stream)
5959
FundingSigned.type -> FundingSigned.read(stream)
6060
FundingLocked.type -> FundingLocked.read(stream)
61+
TxAddInput.type -> TxAddInput.read(stream)
62+
TxAddOutput.type -> TxAddOutput.read(stream)
63+
TxRemoveInput.type -> TxRemoveInput.read(stream)
64+
TxRemoveOutput.type -> TxRemoveOutput.read(stream)
65+
TxComplete.type -> TxComplete.read(stream)
66+
TxSignatures.type -> TxSignatures.read(stream)
67+
TxInitRbf.type -> TxInitRbf.read(stream)
68+
TxAckRbf.type -> TxAckRbf.read(stream)
69+
TxAbort.type -> TxAbort.read(stream)
6170
CommitSig.type -> CommitSig.read(stream)
6271
RevokeAndAck.type -> RevokeAndAck.read(stream)
6372
UpdateAddHtlc.type -> UpdateAddHtlc.read(stream)
@@ -123,6 +132,9 @@ interface HtlcSettlementMessage : UpdateMessage {
123132
val id: Long
124133
}
125134

135+
sealed class InteractiveTxMessage : LightningMessage
136+
sealed class InteractiveTxConstructionMessage : InteractiveTxMessage()
137+
126138
interface HasTemporaryChannelId : LightningMessage {
127139
val temporaryChannelId: ByteVector32
128140
}
@@ -131,6 +143,10 @@ interface HasChannelId : LightningMessage {
131143
val channelId: ByteVector32
132144
}
133145

146+
interface HasSerialId : LightningMessage {
147+
val serialId: Long
148+
}
149+
134150
interface HasChainHash : LightningMessage {
135151
val chainHash: ByteVector32
136152
}
@@ -288,6 +304,261 @@ data class Pong(val data: ByteVector) : SetupMessage {
288304
}
289305
}
290306

307+
data class TxAddInput(
308+
override val channelId: ByteVector32,
309+
override val serialId: Long,
310+
val previousTx: Transaction,
311+
val previousTxOutput: Long,
312+
val sequence: Long,
313+
val tlvs: TlvStream<TxAddInputTlv> = TlvStream.empty()
314+
) : InteractiveTxConstructionMessage(), HasChannelId, HasSerialId {
315+
override val type: Long get() = TxAddInput.type
316+
317+
override fun write(out: Output) {
318+
LightningCodecs.writeBytes(channelId.toByteArray(), out)
319+
LightningCodecs.writeU64(serialId, out)
320+
val encodedTx = Transaction.write(previousTx)
321+
LightningCodecs.writeU16(encodedTx.size, out)
322+
LightningCodecs.writeBytes(encodedTx, out)
323+
LightningCodecs.writeU32(previousTxOutput.toInt(), out)
324+
LightningCodecs.writeU32(sequence.toInt(), out)
325+
}
326+
327+
companion object : LightningMessageReader<TxAddInput> {
328+
const val type: Long = 66
329+
330+
override fun read(input: Input): TxAddInput = TxAddInput(
331+
LightningCodecs.bytes(input, 32).byteVector32(),
332+
LightningCodecs.u64(input),
333+
Transaction.read(LightningCodecs.bytes(input, LightningCodecs.u16(input))),
334+
LightningCodecs.u32(input).toLong(),
335+
LightningCodecs.u32(input).toLong(),
336+
)
337+
}
338+
}
339+
340+
data class TxAddOutput(
341+
override val channelId: ByteVector32,
342+
override val serialId: Long,
343+
val amount: Satoshi,
344+
val pubkeyScript: ByteVector,
345+
val tlvs: TlvStream<TxAddOutputTlv> = TlvStream.empty()
346+
) : InteractiveTxConstructionMessage(), HasChannelId, HasSerialId {
347+
override val type: Long get() = TxAddOutput.type
348+
349+
override fun write(out: Output) {
350+
LightningCodecs.writeBytes(channelId.toByteArray(), out)
351+
LightningCodecs.writeU64(serialId, out)
352+
LightningCodecs.writeU64(amount.toLong(), out)
353+
LightningCodecs.writeU16(pubkeyScript.size(), out)
354+
LightningCodecs.writeBytes(pubkeyScript, out)
355+
}
356+
357+
companion object : LightningMessageReader<TxAddOutput> {
358+
const val type: Long = 67
359+
360+
override fun read(input: Input): TxAddOutput = TxAddOutput(
361+
LightningCodecs.bytes(input, 32).byteVector32(),
362+
LightningCodecs.u64(input),
363+
LightningCodecs.u64(input).sat,
364+
LightningCodecs.bytes(input, LightningCodecs.u16(input)).byteVector(),
365+
)
366+
}
367+
}
368+
369+
data class TxRemoveInput(
370+
override val channelId: ByteVector32,
371+
override val serialId: Long,
372+
val tlvs: TlvStream<TxRemoveInputTlv> = TlvStream.empty()
373+
) : InteractiveTxConstructionMessage(), HasChannelId, HasSerialId {
374+
override val type: Long get() = TxRemoveInput.type
375+
376+
override fun write(out: Output) {
377+
LightningCodecs.writeBytes(channelId.toByteArray(), out)
378+
LightningCodecs.writeU64(serialId, out)
379+
}
380+
381+
companion object : LightningMessageReader<TxRemoveInput> {
382+
const val type: Long = 68
383+
384+
override fun read(input: Input): TxRemoveInput = TxRemoveInput(
385+
LightningCodecs.bytes(input, 32).byteVector32(),
386+
LightningCodecs.u64(input),
387+
)
388+
}
389+
}
390+
391+
data class TxRemoveOutput(
392+
override val channelId: ByteVector32,
393+
override val serialId: Long,
394+
val tlvs: TlvStream<TxRemoveOutputTlv> = TlvStream.empty()
395+
) : InteractiveTxConstructionMessage(), HasChannelId, HasSerialId {
396+
override val type: Long get() = TxRemoveOutput.type
397+
398+
override fun write(out: Output) {
399+
LightningCodecs.writeBytes(channelId.toByteArray(), out)
400+
LightningCodecs.writeU64(serialId, out)
401+
}
402+
403+
companion object : LightningMessageReader<TxRemoveOutput> {
404+
const val type: Long = 69
405+
406+
override fun read(input: Input): TxRemoveOutput = TxRemoveOutput(
407+
LightningCodecs.bytes(input, 32).byteVector32(),
408+
LightningCodecs.u64(input),
409+
)
410+
}
411+
}
412+
413+
data class TxComplete(
414+
override val channelId: ByteVector32,
415+
val tlvs: TlvStream<TxCompleteTlv> = TlvStream.empty()
416+
) : InteractiveTxConstructionMessage(), HasChannelId {
417+
override val type: Long get() = TxComplete.type
418+
419+
override fun write(out: Output) = LightningCodecs.writeBytes(channelId.toByteArray(), out)
420+
421+
companion object : LightningMessageReader<TxComplete> {
422+
const val type: Long = 70
423+
424+
override fun read(input: Input): TxComplete = TxComplete(LightningCodecs.bytes(input, 32).byteVector32())
425+
}
426+
}
427+
428+
data class TxSignatures(
429+
override val channelId: ByteVector32,
430+
val txId: ByteVector32,
431+
val witnesses: List<ScriptWitness>,
432+
val tlvs: TlvStream<TxSignaturesTlv> = TlvStream.empty()
433+
) : InteractiveTxMessage(), HasChannelId {
434+
override val type: Long get() = TxSignatures.type
435+
436+
override fun write(out: Output) {
437+
LightningCodecs.writeBytes(channelId.toByteArray(), out)
438+
LightningCodecs.writeBytes(txId.toByteArray(), out)
439+
LightningCodecs.writeU16(witnesses.size, out)
440+
witnesses.forEach { witness ->
441+
LightningCodecs.writeU16(witness.stack.size, out)
442+
witness.stack.forEach { element ->
443+
LightningCodecs.writeU16(element.size(), out)
444+
LightningCodecs.writeBytes(element.toByteArray(), out)
445+
}
446+
}
447+
}
448+
449+
companion object : LightningMessageReader<TxSignatures> {
450+
const val type: Long = 71
451+
452+
override fun read(input: Input): TxSignatures {
453+
val channelId = LightningCodecs.bytes(input, 32).byteVector32()
454+
val txId = LightningCodecs.bytes(input, 32).byteVector32()
455+
val witnessCount = LightningCodecs.u16(input)
456+
val witnesses = ArrayList<ScriptWitness>(witnessCount)
457+
for (i in 1..witnessCount) {
458+
val stackSize = LightningCodecs.u16(input)
459+
val stack = ArrayList<ByteVector>(stackSize)
460+
for (j in 1..stackSize) {
461+
val elementSize = LightningCodecs.u16(input)
462+
stack += LightningCodecs.bytes(input, elementSize).byteVector()
463+
}
464+
witnesses += ScriptWitness(stack.toList())
465+
}
466+
return TxSignatures(channelId, txId, witnesses)
467+
}
468+
}
469+
}
470+
471+
data class TxInitRbf(
472+
override val channelId: ByteVector32,
473+
val lockTime: Long,
474+
val feerate: FeeratePerKw,
475+
val tlvs: TlvStream<TxInitRbfTlv> = TlvStream.empty()
476+
) : InteractiveTxMessage(), HasChannelId {
477+
constructor(channelId: ByteVector32, lockTime: Long, feerate: FeeratePerKw, fundingContribution: Satoshi) : this(channelId, lockTime, feerate, TlvStream(listOf(TxInitRbfTlv.SharedOutputContributionTlv(fundingContribution))))
478+
479+
@Transient
480+
val fundingContribution = tlvs.get<TxInitRbfTlv.SharedOutputContributionTlv>()?.amount ?: 0.sat
481+
482+
override val type: Long get() = TxInitRbf.type
483+
484+
override fun write(out: Output) {
485+
LightningCodecs.writeBytes(channelId.toByteArray(), out)
486+
LightningCodecs.writeU32(lockTime.toInt(), out)
487+
LightningCodecs.writeU32(feerate.toLong().toInt(), out)
488+
TlvStreamSerializer(false, readers).write(tlvs, out)
489+
}
490+
491+
companion object : LightningMessageReader<TxInitRbf> {
492+
const val type: Long = 72
493+
494+
@Suppress("UNCHECKED_CAST")
495+
val readers = mapOf(TxInitRbfTlv.SharedOutputContributionTlv.tag to TxInitRbfTlv.SharedOutputContributionTlv.Companion as TlvValueReader<TxInitRbfTlv>)
496+
497+
override fun read(input: Input): TxInitRbf = TxInitRbf(
498+
LightningCodecs.bytes(input, 32).byteVector32(),
499+
LightningCodecs.u32(input).toLong(),
500+
FeeratePerKw(LightningCodecs.u32(input).toLong().sat),
501+
TlvStreamSerializer(false, readers).read(input),
502+
)
503+
}
504+
}
505+
506+
data class TxAckRbf(
507+
override val channelId: ByteVector32,
508+
val tlvs: TlvStream<TxAckRbfTlv> = TlvStream.empty()
509+
) : InteractiveTxMessage(), HasChannelId {
510+
constructor(channelId: ByteVector32, fundingContribution: Satoshi) : this(channelId, TlvStream(listOf(TxAckRbfTlv.SharedOutputContributionTlv(fundingContribution))))
511+
512+
@Transient
513+
val fundingContribution = tlvs.get<TxAckRbfTlv.SharedOutputContributionTlv>()?.amount ?: 0.sat
514+
515+
override val type: Long get() = TxAckRbf.type
516+
517+
override fun write(out: Output) {
518+
LightningCodecs.writeBytes(channelId.toByteArray(), out)
519+
TlvStreamSerializer(false, readers).write(tlvs, out)
520+
}
521+
522+
companion object : LightningMessageReader<TxAckRbf> {
523+
const val type: Long = 73
524+
525+
@Suppress("UNCHECKED_CAST")
526+
val readers = mapOf(TxAckRbfTlv.SharedOutputContributionTlv.tag to TxAckRbfTlv.SharedOutputContributionTlv.Companion as TlvValueReader<TxAckRbfTlv>)
527+
528+
override fun read(input: Input): TxAckRbf = TxAckRbf(
529+
LightningCodecs.bytes(input, 32).byteVector32(),
530+
TlvStreamSerializer(false, readers).read(input),
531+
)
532+
}
533+
}
534+
535+
data class TxAbort(
536+
override val channelId: ByteVector32,
537+
val data: ByteVector,
538+
val tlvs: TlvStream<TxAbortTlv> = TlvStream.empty()
539+
) : InteractiveTxMessage(), HasChannelId {
540+
constructor(channelId: ByteVector32, message: String?) : this(channelId, ByteVector(message?.encodeToByteArray() ?: ByteArray(0)))
541+
542+
fun toAscii(): String = data.toByteArray().decodeToString()
543+
544+
override val type: Long get() = TxAbort.type
545+
546+
override fun write(out: Output) {
547+
LightningCodecs.writeBytes(channelId, out)
548+
LightningCodecs.writeU16(data.size(), out)
549+
LightningCodecs.writeBytes(data, out)
550+
}
551+
552+
companion object : LightningMessageReader<TxAbort> {
553+
const val type: Long = 74
554+
555+
override fun read(input: Input): TxAbort = TxAbort(
556+
LightningCodecs.bytes(input, 32).byteVector32(),
557+
LightningCodecs.bytes(input, LightningCodecs.u16(input)).byteVector(),
558+
)
559+
}
560+
}
561+
291562
@Serializable
292563
data class OpenChannel(
293564
@Contextual override val chainHash: ByteVector32,
@@ -1353,7 +1624,6 @@ data class PhoenixAndroidLegacyInfo(
13531624
}
13541625
}
13551626

1356-
@OptIn(ExperimentalUnsignedTypes::class)
13571627
data class PhoenixAndroidLegacyMigrate(
13581628
val newNodeId: PublicKey
13591629
) : LightningMessage {
@@ -1372,7 +1642,6 @@ data class PhoenixAndroidLegacyMigrate(
13721642
}
13731643
}
13741644

1375-
@OptIn(ExperimentalUnsignedTypes::class)
13761645
@Serializable
13771646
data class OnionMessage(
13781647
@Contextual val blindingKey: PublicKey,

0 commit comments

Comments
 (0)