Skip to content

Commit

Permalink
Add option_simple_close feature bit and codecs
Browse files Browse the repository at this point in the history
  • Loading branch information
t-bast committed Mar 27, 2024
1 parent cb0f5c5 commit 9bbf579
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 11 deletions.
13 changes: 11 additions & 2 deletions src/commonMain/kotlin/fr/acinq/lightning/Features.kt
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,6 @@ sealed class Feature {
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Invoice)
}

// The following features have not been standardised, hence the high feature bits to avoid conflicts.

// We historically used the following feature bit in our invoices.
// However, the spec assigned the same feature bit to `option_scid_alias` (https://github.com/lightning/bolts/pull/910).
// We're moving this feature bit to 148, but we have to keep supporting it until enough wallet users have migrated, then we can remove it.
Expand All @@ -153,6 +151,15 @@ sealed class Feature {
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice)
}

@Serializable
object SimpleClose : Feature() {
override val rfcName get() = "option_simple_close"
override val mandatory get() = 60
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

// The following features have not been standardised, hence the high feature bits to avoid conflicts.

/** This feature bit should be activated when a node accepts having their channel reserve set to 0. */
@Serializable
object ZeroReserveChannels : Feature() {
Expand Down Expand Up @@ -324,6 +331,7 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.ChannelType,
Feature.PaymentMetadata,
Feature.TrampolinePayment,
Feature.SimpleClose,
Feature.ExperimentalTrampolinePayment,
Feature.ZeroReserveChannels,
Feature.ZeroConfChannels,
Expand Down Expand Up @@ -367,6 +375,7 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.PaymentSecret to listOf(Feature.VariableLengthOnion),
Feature.BasicMultiPartPayment to listOf(Feature.PaymentSecret),
Feature.AnchorOutputs to listOf(Feature.StaticRemoteKey),
Feature.SimpleClose to listOf(Feature.ShutdownAnySegwit),
Feature.TrampolinePayment to listOf(Feature.PaymentSecret),
Feature.ExperimentalTrampolinePayment to listOf(Feature.PaymentSecret)
)
Expand Down
90 changes: 90 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,96 @@ sealed class ClosingSignedTlv : Tlv {
}
}

sealed class ClosingCompleteTlv : Tlv {
/** Signature for a closing transaction containing only the closer's output. */
data class CloserNoClosee(val sig: ByteVector64) : ClosingCompleteTlv() {
override val tag: Long get() = CloserNoClosee.tag
override fun write(out: Output) = LightningCodecs.writeBytes(sig.toByteArray(), out)

companion object : TlvValueReader<CloserNoClosee> {
const val tag: Long = 1
override fun read(input: Input): CloserNoClosee = CloserNoClosee(LightningCodecs.bytes(input, 64).toByteVector64())
}
}

/** Signature for a closing transaction containing only the closee's output. */
data class NoCloserClosee(val sig: ByteVector64) : ClosingCompleteTlv() {
override val tag: Long get() = NoCloserClosee.tag
override fun write(out: Output) = LightningCodecs.writeBytes(sig.toByteArray(), out)

companion object : TlvValueReader<NoCloserClosee> {
const val tag: Long = 2
override fun read(input: Input): NoCloserClosee = NoCloserClosee(LightningCodecs.bytes(input, 64).toByteVector64())
}
}

/** Signature for a closing transaction containing the closer and closee's outputs. */
data class CloserAndClosee(val sig: ByteVector64) : ClosingCompleteTlv() {
override val tag: Long get() = CloserAndClosee.tag
override fun write(out: Output) = LightningCodecs.writeBytes(sig.toByteArray(), out)

companion object : TlvValueReader<CloserAndClosee> {
const val tag: Long = 3
override fun read(input: Input): CloserAndClosee = CloserAndClosee(LightningCodecs.bytes(input, 64).toByteVector64())
}
}

data class ChannelData(val ecb: EncryptedChannelData) : ClosingCompleteTlv() {
override val tag: Long get() = ChannelData.tag
override fun write(out: Output) = LightningCodecs.writeBytes(ecb.data, out)

companion object : TlvValueReader<ChannelData> {
const val tag: Long = 0x47010000
override fun read(input: Input): ChannelData = ChannelData(EncryptedChannelData(LightningCodecs.bytes(input, input.availableBytes).toByteVector()))
}
}
}

sealed class ClosingSigTlv : Tlv {
/** Signature for a closing transaction containing only the closer's output. */
data class CloserNoClosee(val sig: ByteVector64) : ClosingSigTlv() {
override val tag: Long get() = CloserNoClosee.tag
override fun write(out: Output) = LightningCodecs.writeBytes(sig.toByteArray(), out)

companion object : TlvValueReader<CloserNoClosee> {
const val tag: Long = 1
override fun read(input: Input): CloserNoClosee = CloserNoClosee(LightningCodecs.bytes(input, 64).toByteVector64())
}
}

/** Signature for a closing transaction containing only the closee's output. */
data class NoCloserClosee(val sig: ByteVector64) : ClosingSigTlv() {
override val tag: Long get() = NoCloserClosee.tag
override fun write(out: Output) = LightningCodecs.writeBytes(sig.toByteArray(), out)

companion object : TlvValueReader<NoCloserClosee> {
const val tag: Long = 2
override fun read(input: Input): NoCloserClosee = NoCloserClosee(LightningCodecs.bytes(input, 64).toByteVector64())
}
}

/** Signature for a closing transaction containing the closer and closee's outputs. */
data class CloserAndClosee(val sig: ByteVector64) : ClosingSigTlv() {
override val tag: Long get() = CloserAndClosee.tag
override fun write(out: Output) = LightningCodecs.writeBytes(sig.toByteArray(), out)

companion object : TlvValueReader<CloserAndClosee> {
const val tag: Long = 3
override fun read(input: Input): CloserAndClosee = CloserAndClosee(LightningCodecs.bytes(input, 64).toByteVector64())
}
}

data class ChannelData(val ecb: EncryptedChannelData) : ClosingSigTlv() {
override val tag: Long get() = ChannelData.tag
override fun write(out: Output) = LightningCodecs.writeBytes(ecb.data, out)

companion object : TlvValueReader<ChannelData> {
const val tag: Long = 0x47010000
override fun read(input: Input): ChannelData = ChannelData(EncryptedChannelData(LightningCodecs.bytes(input, input.availableBytes).toByteVector()))
}
}
}

sealed class PleaseOpenChannelTlv : Tlv {
// NB: this is a temporary tlv that is only used to ensure a smooth migration to lightning-kmp for the android version of Phoenix.
data class GrandParents(val outpoints: List<OutPoint>) : PleaseOpenChannelTlv() {
Expand Down
84 changes: 84 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ interface LightningMessage {
ChannelUpdate.type -> ChannelUpdate.read(stream)
Shutdown.type -> Shutdown.read(stream)
ClosingSigned.type -> ClosingSigned.read(stream)
ClosingComplete.type -> ClosingComplete.read(stream)
ClosingSig.type -> ClosingSig.read(stream)
OnionMessage.type -> OnionMessage.read(stream)
PayToOpenRequest.type -> PayToOpenRequest.read(stream)
PayToOpenResponse.type -> PayToOpenResponse.read(stream)
Expand Down Expand Up @@ -1548,6 +1550,88 @@ data class ClosingSigned(
}
}

data class ClosingComplete(
override val channelId: ByteVector32,
val fees: Satoshi,
val lockTime: Long,
val tlvStream: TlvStream<ClosingCompleteTlv> = TlvStream.empty()
) : ChannelMessage, HasChannelId, HasEncryptedChannelData {
override val type: Long get() = ClosingComplete.type

val closerNoCloseeSig: ByteVector64? = tlvStream.get<ClosingCompleteTlv.CloserNoClosee>()?.sig
val noCloserCloseeSig: ByteVector64? = tlvStream.get<ClosingCompleteTlv.NoCloserClosee>()?.sig
val closerAndCloseeSig: ByteVector64? = tlvStream.get<ClosingCompleteTlv.CloserAndClosee>()?.sig

override val channelData: EncryptedChannelData get() = tlvStream.get<ClosingCompleteTlv.ChannelData>()?.ecb ?: EncryptedChannelData.empty
override fun withNonEmptyChannelData(ecd: EncryptedChannelData): ClosingComplete = copy(tlvStream = tlvStream.addOrUpdate(ClosingCompleteTlv.ChannelData(ecd)))

override fun write(out: Output) {
LightningCodecs.writeBytes(channelId, out)
LightningCodecs.writeU64(fees.toLong(), out)
LightningCodecs.writeU32(lockTime.toInt(), out)
TlvStreamSerializer(false, readers).write(tlvStream, out)
}

companion object : LightningMessageReader<ClosingComplete> {
const val type: Long = 40

@Suppress("UNCHECKED_CAST")
val readers = mapOf(
ClosingCompleteTlv.CloserNoClosee.tag to ClosingCompleteTlv.CloserNoClosee.Companion as TlvValueReader<ClosingCompleteTlv>,
ClosingCompleteTlv.NoCloserClosee.tag to ClosingCompleteTlv.NoCloserClosee.Companion as TlvValueReader<ClosingCompleteTlv>,
ClosingCompleteTlv.CloserAndClosee.tag to ClosingCompleteTlv.CloserAndClosee.Companion as TlvValueReader<ClosingCompleteTlv>,
ClosingCompleteTlv.ChannelData.tag to ClosingCompleteTlv.ChannelData.Companion as TlvValueReader<ClosingCompleteTlv>
)

override fun read(input: Input): ClosingComplete {
return ClosingComplete(
LightningCodecs.bytes(input, 32).byteVector32(),
LightningCodecs.u64(input).sat,
LightningCodecs.u32(input).toLong(),
TlvStreamSerializer(false, readers).read(input)
)
}
}
}

data class ClosingSig(
override val channelId: ByteVector32,
val tlvStream: TlvStream<ClosingSigTlv> = TlvStream.empty()
) : ChannelMessage, HasChannelId, HasEncryptedChannelData {
override val type: Long get() = ClosingSig.type

val closerNoCloseeSig: ByteVector64? = tlvStream.get<ClosingSigTlv.CloserNoClosee>()?.sig
val noCloserCloseeSig: ByteVector64? = tlvStream.get<ClosingSigTlv.NoCloserClosee>()?.sig
val closerAndCloseeSig: ByteVector64? = tlvStream.get<ClosingSigTlv.CloserAndClosee>()?.sig

override val channelData: EncryptedChannelData get() = tlvStream.get<ClosingSigTlv.ChannelData>()?.ecb ?: EncryptedChannelData.empty
override fun withNonEmptyChannelData(ecd: EncryptedChannelData): ClosingSig = copy(tlvStream = tlvStream.addOrUpdate(ClosingSigTlv.ChannelData(ecd)))

override fun write(out: Output) {
LightningCodecs.writeBytes(channelId, out)
TlvStreamSerializer(false, readers).write(tlvStream, out)
}

companion object : LightningMessageReader<ClosingSig> {
const val type: Long = 41

@Suppress("UNCHECKED_CAST")
val readers = mapOf(
ClosingSigTlv.CloserNoClosee.tag to ClosingSigTlv.CloserNoClosee.Companion as TlvValueReader<ClosingSigTlv>,
ClosingSigTlv.NoCloserClosee.tag to ClosingSigTlv.NoCloserClosee.Companion as TlvValueReader<ClosingSigTlv>,
ClosingSigTlv.CloserAndClosee.tag to ClosingSigTlv.CloserAndClosee.Companion as TlvValueReader<ClosingSigTlv>,
ClosingSigTlv.ChannelData.tag to ClosingSigTlv.ChannelData.Companion as TlvValueReader<ClosingSigTlv>
)

override fun read(input: Input): ClosingSig {
return ClosingSig(
LightningCodecs.bytes(input, 32).byteVector32(),
TlvStreamSerializer(false, readers).read(input)
)
}
}
}

data class OnionMessage(
val blindingKey: PublicKey,
val onionRoutingPacket: OnionRoutingPacket
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,34 @@ class LightningCodecsTestsCommon : LightningTestSuite() {
}
}

@Test
fun `encode - decode closing messages`() {
val channelId = ByteVector32("58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86")
val sig1 = ByteVector64("01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101")
val sig2 = ByteVector64("02020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202")
val sig3 = ByteVector64("03030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303")
val testCases = mapOf(
// @formatter:off
Hex.decode("0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0000000000000451 00000000") to ClosingComplete(channelId, 1105.sat, 0),
Hex.decode("0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0000000000000451 000c96a8 024001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") to ClosingComplete(channelId, 1105.sat, 825_000, TlvStream(ClosingCompleteTlv.NoCloserClosee(sig1))),
Hex.decode("0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0000000000000451 00000000 034001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") to ClosingComplete(channelId, 1105.sat, 0, TlvStream(ClosingCompleteTlv.CloserAndClosee(sig1))),
Hex.decode("0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 034002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202") to ClosingComplete(channelId, 1105.sat, 0, TlvStream(ClosingCompleteTlv.CloserNoClosee(sig1), ClosingCompleteTlv.CloserAndClosee(sig2))),
Hex.decode("0028 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 0000000000000451 00000000 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 024002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202 034003030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303") to ClosingComplete(channelId, 1105.sat, 0, TlvStream(ClosingCompleteTlv.CloserNoClosee(sig1), ClosingCompleteTlv.NoCloserClosee(sig2), ClosingCompleteTlv.CloserAndClosee(sig3))),
Hex.decode("0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86") to ClosingSig(channelId),
Hex.decode("0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 024001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") to ClosingSig(channelId, TlvStream(ClosingSigTlv.NoCloserClosee(sig1))),
Hex.decode("0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 034001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101") to ClosingSig(channelId, TlvStream(ClosingSigTlv.CloserAndClosee(sig1))),
Hex.decode("0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 034002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202") to ClosingSig(channelId, TlvStream(ClosingSigTlv.CloserNoClosee(sig1), ClosingSigTlv.CloserAndClosee(sig2))),
Hex.decode("0029 58a00a6f14e69a2e97b18cf76f755c8551fea9947cf7b6ece9d641013eba5f86 014001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 024002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202 034003030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303") to ClosingSig(channelId, TlvStream(ClosingSigTlv.CloserNoClosee(sig1), ClosingSigTlv.NoCloserClosee(sig2), ClosingSigTlv.CloserAndClosee(sig3))),
// @formatter:on
)
testCases.forEach {
val decoded = LightningMessage.decode(it.key)
assertEquals(it.value, decoded)
val encoded = LightningMessage.encode(it.value)
assertArrayEquals(it.key, encoded)
}
}

@Test
fun `nonreg backup channel data`() {
val channelId = randomBytes32()
Expand Down Expand Up @@ -711,6 +739,12 @@ class LightningCodecsTestsCommon : LightningTestSuite() {
Hex.decode("0027") + channelId.toByteArray() + Hex.decode("00000000075bcd15") + signature.toByteArray() + Hex.decode("03 02 0102") + Hex.decode("fe47010000 00") to ClosingSigned(channelId, 123456789.sat, signature, TlvStream(setOf(ClosingSignedTlv.ChannelData(EncryptedChannelData.empty)), setOf(GenericTlv(3, ByteVector("0102"))))),
Hex.decode("0027") + channelId.toByteArray() + Hex.decode("00000000075bcd15") + signature.toByteArray() + Hex.decode("fe47010000 07 cccccccccccccc") to ClosingSigned(channelId, 123456789.sat, signature).withChannelData(ByteVector("cccccccccccccc")),
Hex.decode("0027") + channelId.toByteArray() + Hex.decode("00000000075bcd15") + signature.toByteArray() + Hex.decode("03 02 0102") + Hex.decode("fe47010000 07 cccccccccccccc") to ClosingSigned(channelId, 123456789.sat, signature, TlvStream(setOf(ClosingSignedTlv.ChannelData(EncryptedChannelData(ByteVector("cccccccccccccc")))), setOf(GenericTlv(3, ByteVector("0102")))))
// closing_complete
Hex.decode("0028") + channelId.toByteArray() + Hex.decode("0000000000000451 00000000") + Hex.decode("fe47010000 00") to ClosingComplete(channelId, 1105.sat, 0, TlvStream(ClosingCompleteTlv.ChannelData(EncryptedChannelData.empty))),
Hex.decode("0028") + channelId.toByteArray() + Hex.decode("0000000000000451 00000000") + Hex.decode("fe47010000 07 cccccccccccccc") to ClosingComplete(channelId, 1105.sat, 0).withChannelData(ByteVector("cccccccccccccc")),
// closing_sig
Hex.decode("0029") + channelId.toByteArray() + Hex.decode("fe47010000 00") to ClosingSig(channelId, TlvStream(ClosingSigTlv.ChannelData(EncryptedChannelData.empty))),
Hex.decode("0029") + channelId.toByteArray() + Hex.decode("fe47010000 07 cccccccccccccc") to ClosingSig(channelId).withChannelData(ByteVector("cccccccccccccc")),
)
// @formatter:on

Expand All @@ -734,22 +768,15 @@ class LightningCodecsTestsCommon : LightningTestSuite() {
RevokeAndAck(randomBytes32(), randomKey(), randomKey().publicKey()),
Shutdown(randomBytes32(), ByteVector("deadbeef")),
ClosingSigned(randomBytes32(), 0.sat, randomBytes64()),
ClosingComplete(randomBytes32(), 250.sat, 0),
ClosingSig(randomBytes32()),
)
messages.forEach {
assertEquals(it.withChannelData(belowLimit).channelData, belowLimit)
assertTrue(it.withChannelData(aboveLimit).channelData.isEmpty())
}
}

@Test
fun `skip backup channel data when message is too large`() {
val channelData = EncryptedChannelData(ByteVector(ByteArray(59500) { 42 }))
val smallCommit = CommitSig(randomBytes32(), randomBytes64(), listOf())
assertEquals(smallCommit.withChannelData(channelData).channelData, channelData)
val largeCommit = CommitSig(randomBytes32(), randomBytes64(), List(50) { randomBytes64() })
assertTrue(largeCommit.withChannelData(channelData).channelData.isEmpty())
}

@Test
fun `encode - decode pay-to-open messages`() {
val testCases = listOf(
Expand Down

0 comments on commit 9bbf579

Please sign in to comment.