Skip to content

Commit 559dc9b

Browse files
authored
Binary serialization for payments data (#739)
This is symmetrical with what we already have for channel data. The goal is to offer ready-made serializers for library integrators, taking care of backward compatibility when the model is updated, instead of pushing the work to integrators. Initially I explored a json-based approach in branch [db-type-module](https://github.com/ACINQ/lightning-kmp/tree/db-type-module) (see module `lightning-kmp-db-types`). But it turned out to be tedious and verbose, with a lot of class definitions and boiler plate code to handle model migrations. These binary codecs are much lighter to write and maintain, and probably faster (although it was not the main objective). The main drawbacks is that the serialized data is not human readable.
1 parent b18fa6c commit 559dc9b

32 files changed

+1120
-184
lines changed

modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import fr.acinq.lightning.channel.Helpers.Closing.getRemotePerCommitmentSecret
1717
import fr.acinq.lightning.crypto.KeyManager
1818
import fr.acinq.lightning.db.ChannelClosingType
1919
import fr.acinq.lightning.logging.*
20-
import fr.acinq.lightning.serialization.Encryption.from
20+
import fr.acinq.lightning.serialization.channel.Encryption.from
2121
import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.ClosingTx
2222
import fr.acinq.lightning.utils.*
2323
import fr.acinq.lightning.wire.*

modules/core/src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt

+2
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ sealed class OutgoingPayment : WalletPayment()
303303
* @param parts list of partial child payments that have actually been sent.
304304
* @param status current status of the payment.
305305
*/
306+
@Suppress("DEPRECATION")
306307
data class LightningOutgoingPayment(
307308
override val id: UUID,
308309
val recipientAmount: MilliSatoshi,
@@ -361,6 +362,7 @@ data class LightningOutgoingPayment(
361362
* Swap-out payments send a lightning payment to a swap server, which will send an on-chain transaction to a given address.
362363
* The swap-out fee is taken by the swap server to cover the miner fee.
363364
*/
365+
@Deprecated("Legacy trusted swap-out, kept for backwards-compatibility with existing databases.")
364366
data class SwapOut(val address: String, override val paymentRequest: PaymentRequest, val swapOutFee: Satoshi) : Details() {
365367
override val paymentHash: ByteVector32 = paymentRequest.paymentHash
366368
}

modules/core/src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import fr.acinq.lightning.logging.MDCLogger
1919
import fr.acinq.lightning.logging.mdc
2020
import fr.acinq.lightning.logging.withMDC
2121
import fr.acinq.lightning.payment.*
22-
import fr.acinq.lightning.serialization.Encryption.from
23-
import fr.acinq.lightning.serialization.Serialization.DeserializationResult
22+
import fr.acinq.lightning.serialization.channel.Encryption.from
23+
import fr.acinq.lightning.serialization.channel.Serialization.DeserializationResult
2424
import fr.acinq.lightning.transactions.Scripts
2525
import fr.acinq.lightning.transactions.Transactions
2626
import fr.acinq.lightning.utils.*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package fr.acinq.lightning.serialization
2+
3+
import fr.acinq.bitcoin.*
4+
import fr.acinq.bitcoin.io.Input
5+
import fr.acinq.bitcoin.utils.Either
6+
import fr.acinq.lightning.utils.UUID
7+
import fr.acinq.lightning.wire.LightningCodecs
8+
import fr.acinq.lightning.wire.LightningMessage
9+
10+
object InputExtensions {
11+
12+
fun Input.readNumber(): Long = LightningCodecs.bigSize(this)
13+
14+
fun Input.readBoolean(): Boolean = read() == 1
15+
16+
fun Input.readString(): String = readDelimitedByteArray().decodeToString()
17+
18+
fun Input.readByteVector32(): ByteVector32 = ByteVector32(ByteArray(32).also { read(it, 0, it.size) })
19+
20+
fun Input.readByteVector64(): ByteVector64 = ByteVector64(ByteArray(64).also { read(it, 0, it.size) })
21+
22+
fun Input.readPublicKey() = PublicKey(ByteArray(33).also { read(it, 0, it.size) })
23+
24+
fun Input.readPrivateKey() = PrivateKey(ByteArray(32).also { read(it, 0, it.size) })
25+
26+
fun Input.readTxId(): TxId = TxId(readByteVector32())
27+
28+
fun Input.readUuid(): UUID = UUID.fromBytes(ByteArray(16).also { read(it, 0, it.size) })
29+
30+
fun Input.readDelimitedByteArray(): ByteArray {
31+
val size = readNumber().toInt()
32+
return ByteArray(size).also { read(it, 0, size) }
33+
}
34+
35+
fun Input.readLightningMessage() = LightningMessage.decode(readDelimitedByteArray())
36+
37+
fun <T> Input.readCollection(readElem: () -> T): Collection<T> {
38+
val size = readNumber()
39+
return buildList {
40+
repeat(size.toInt()) {
41+
add(readElem())
42+
}
43+
}
44+
}
45+
46+
fun <L, R> Input.readEither(readLeft: () -> L, readRight: () -> R): Either<L, R> = when (read()) {
47+
0 -> Either.Left(readLeft())
48+
else -> Either.Right(readRight())
49+
}
50+
51+
fun <T : Any> Input.readNullable(readNotNull: () -> T): T? = when (read()) {
52+
1 -> readNotNull()
53+
else -> null
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package fr.acinq.lightning.serialization
2+
3+
import fr.acinq.bitcoin.*
4+
import fr.acinq.bitcoin.io.Output
5+
import fr.acinq.bitcoin.utils.Either
6+
import fr.acinq.lightning.utils.UUID
7+
import fr.acinq.lightning.wire.LightningCodecs
8+
import fr.acinq.lightning.wire.LightningMessage
9+
10+
object OutputExtensions {
11+
12+
fun Output.writeNumber(o: Number): Unit = LightningCodecs.writeBigSize(o.toLong(), this)
13+
14+
fun Output.writeBoolean(o: Boolean): Unit = if (o) write(1) else write(0)
15+
16+
fun Output.writeString(o: String): Unit = writeDelimited(o.encodeToByteArray())
17+
18+
fun Output.writeByteVector32(o: ByteVector32) = write(o.toByteArray())
19+
20+
fun Output.writeByteVector64(o: ByteVector64) = write(o.toByteArray())
21+
22+
fun Output.writePublicKey(o: PublicKey) = write(o.value.toByteArray())
23+
24+
fun Output.writePrivateKey(o: PrivateKey) = write(o.value.toByteArray())
25+
26+
fun Output.writeTxId(o: TxId) = write(o.value.toByteArray())
27+
28+
fun Output.writeUuid(o: UUID) = o.run {
29+
// NB: copied from kotlin source code (https://github.com/JetBrains/kotlin/blob/v2.1.0/libraries/stdlib/src/kotlin/uuid/Uuid.kt) in order to be forward compatible
30+
fun Long.toByteArray(dst: ByteArray, dstOffset: Int) {
31+
for (index in 0 until 8) {
32+
val shift = 8 * (7 - index)
33+
dst[dstOffset + index] = (this ushr shift).toByte()
34+
}
35+
}
36+
val bytes = ByteArray(16)
37+
mostSignificantBits.toByteArray(bytes, 0)
38+
leastSignificantBits.toByteArray(bytes, 8)
39+
write(bytes)
40+
}
41+
42+
fun Output.writeDelimited(o: ByteArray) {
43+
writeNumber(o.size)
44+
write(o)
45+
}
46+
47+
fun <T : BtcSerializable<T>> Output.writeBtcObject(o: T): Unit = writeDelimited(o.serializer().write(o))
48+
49+
fun Output.writeLightningMessage(o: LightningMessage) = writeDelimited(LightningMessage.encode(o))
50+
51+
fun <T> Output.writeCollection(o: Collection<T>, writeElem: (T) -> Unit) {
52+
writeNumber(o.size)
53+
o.forEach { writeElem(it) }
54+
}
55+
56+
fun <L, R> Output.writeEither(o: Either<L, R>, writeLeft: (L) -> Unit, writeRight: (R) -> Unit) = when (o) {
57+
is Either.Left -> {
58+
write(0); writeLeft(o.value)
59+
}
60+
is Either.Right -> {
61+
write(1); writeRight(o.value)
62+
}
63+
}
64+
65+
fun <T : Any> Output.writeNullable(o: T?, writeNotNull: (T) -> Unit) = when (o) {
66+
is T -> {
67+
write(1); writeNotNull(o)
68+
}
69+
else -> write(0)
70+
}
71+
}

modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/Encryption.kt renamed to modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/Encryption.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package fr.acinq.lightning.serialization
1+
package fr.acinq.lightning.serialization.channel
22

33
import fr.acinq.bitcoin.ByteVector32
44
import fr.acinq.bitcoin.Crypto

modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/Serialization.kt renamed to modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/Serialization.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
package fr.acinq.lightning.serialization
1+
package fr.acinq.lightning.serialization.channel
22

33
import fr.acinq.bitcoin.crypto.Pack
44
import fr.acinq.lightning.channel.states.PersistedChannelState
55

66
object Serialization {
77

88
fun serialize(state: PersistedChannelState): ByteArray {
9-
return fr.acinq.lightning.serialization.v4.Serialization.serialize(state)
9+
return fr.acinq.lightning.serialization.channel.v4.Serialization.serialize(state)
1010
}
1111

1212
fun deserialize(bin: ByteArray): DeserializationResult {
1313
return when {
1414
// v4 uses a 1-byte version discriminator
15-
bin[0].toInt() == 4 -> DeserializationResult.Success(fr.acinq.lightning.serialization.v4.Deserialization.deserialize(bin))
15+
bin[0].toInt() == 4 -> DeserializationResult.Success(fr.acinq.lightning.serialization.channel.v4.Deserialization.deserialize(bin))
1616
// v2/v3 use a 4-bytes version discriminator
1717
Pack.int32BE(bin) == 3 -> DeserializationResult.Success(fr.acinq.lightning.serialization.v3.Serialization.deserialize(bin))
1818
Pack.int32BE(bin) == 2 -> DeserializationResult.Success(fr.acinq.lightning.serialization.v2.Serialization.deserialize(bin))

modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/v4/Deserialization.kt renamed to modules/core/src/commonMain/kotlin/fr/acinq/lightning/serialization/channel/v4/Deserialization.kt

+15-73
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
package fr.acinq.lightning.serialization.v4
1+
package fr.acinq.lightning.serialization.channel.v4
22

33
import fr.acinq.bitcoin.*
4-
import fr.acinq.bitcoin.crypto.musig2.IndividualNonce
54
import fr.acinq.bitcoin.io.ByteArrayInput
65
import fr.acinq.bitcoin.io.Input
76
import fr.acinq.bitcoin.io.readNBytes
@@ -13,6 +12,19 @@ import fr.acinq.lightning.blockchain.fee.FeeratePerKw
1312
import fr.acinq.lightning.channel.*
1413
import fr.acinq.lightning.channel.states.*
1514
import fr.acinq.lightning.crypto.ShaChain
15+
import fr.acinq.lightning.serialization.InputExtensions.readBoolean
16+
import fr.acinq.lightning.serialization.InputExtensions.readByteVector32
17+
import fr.acinq.lightning.serialization.InputExtensions.readByteVector64
18+
import fr.acinq.lightning.serialization.InputExtensions.readCollection
19+
import fr.acinq.lightning.serialization.InputExtensions.readDelimitedByteArray
20+
import fr.acinq.lightning.serialization.InputExtensions.readEither
21+
import fr.acinq.lightning.serialization.InputExtensions.readLightningMessage
22+
import fr.acinq.lightning.serialization.InputExtensions.readNullable
23+
import fr.acinq.lightning.serialization.InputExtensions.readNumber
24+
import fr.acinq.lightning.serialization.InputExtensions.readPublicKey
25+
import fr.acinq.lightning.serialization.InputExtensions.readString
26+
import fr.acinq.lightning.serialization.InputExtensions.readTxId
27+
import fr.acinq.lightning.serialization.common.liquidityads.Deserialization.readLiquidityPurchase
1628
import fr.acinq.lightning.transactions.*
1729
import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.*
1830
import fr.acinq.lightning.utils.UUID
@@ -26,7 +38,7 @@ object Deserialization {
2638
fun deserialize(bin: ByteArray): PersistedChannelState {
2739
val input = ByteArrayInput(bin)
2840
val version = input.read()
29-
require(version == Serialization.versionMagic) { "incorrect version $version, expected ${Serialization.versionMagic}" }
41+
require(version == Serialization.VERSION_MAGIC) { "incorrect version $version, expected ${Serialization.VERSION_MAGIC}" }
3042
return input.readPersistedChannelState()
3143
}
3244

@@ -435,31 +447,6 @@ object Deserialization {
435447
)
436448
)
437449

438-
private fun Input.readLiquidityFees(): LiquidityAds.Fees = LiquidityAds.Fees(miningFee = readNumber().sat, serviceFee = readNumber().sat)
439-
440-
private fun Input.readLiquidityPurchase(): LiquidityAds.Purchase = when (val discriminator = read()) {
441-
0x00 -> LiquidityAds.Purchase.Standard(
442-
amount = readNumber().sat,
443-
fees = readLiquidityFees(),
444-
paymentDetails = readLiquidityAdsPaymentDetails()
445-
)
446-
0x01 -> LiquidityAds.Purchase.WithFeeCredit(
447-
amount = readNumber().sat,
448-
fees = readLiquidityFees(),
449-
feeCreditUsed = readNumber().msat,
450-
paymentDetails = readLiquidityAdsPaymentDetails()
451-
)
452-
else -> error("unknown discriminator $discriminator for class ${LiquidityAds.Purchase::class}")
453-
}
454-
455-
private fun Input.readLiquidityAdsPaymentDetails(): LiquidityAds.PaymentDetails = when (val discriminator = read()) {
456-
0x00 -> LiquidityAds.PaymentDetails.FromChannelBalance
457-
0x80 -> LiquidityAds.PaymentDetails.FromFutureHtlc(readCollection { readByteVector32() }.toList())
458-
0x81 -> LiquidityAds.PaymentDetails.FromFutureHtlcWithPreimage(readCollection { readByteVector32() }.toList())
459-
0x82 -> LiquidityAds.PaymentDetails.FromChannelBalanceForFutureHtlc(readCollection { readByteVector32() }.toList())
460-
else -> error("unknown discriminator $discriminator for class ${LiquidityAds.PaymentDetails::class}")
461-
}
462-
463450
private fun Input.skipLegacyLiquidityLease() {
464451
readNumber() // amount
465452
readNumber() // mining fee
@@ -713,8 +700,6 @@ object Deserialization {
713700

714701
private fun Input.readOutPoint(): OutPoint = OutPoint.read(readDelimitedByteArray())
715702

716-
private fun Input.readTxOut(): TxOut = TxOut.read(readDelimitedByteArray())
717-
718703
private fun Input.readTransaction(): Transaction = Transaction.read(readDelimitedByteArray())
719704

720705
private fun Input.readTransactionWithInputInfo(): Transactions.TransactionWithInputInfo = when (val discriminator = read()) {
@@ -741,47 +726,4 @@ object Deserialization {
741726
min = FeeratePerKw(readNumber().sat),
742727
max = FeeratePerKw(readNumber().sat)
743728
)
744-
745-
private fun Input.readNumber(): Long = LightningCodecs.bigSize(this)
746-
747-
private fun Input.readBoolean(): Boolean = read() == 1
748-
749-
private fun Input.readString(): String = readDelimitedByteArray().decodeToString()
750-
751-
private fun Input.readByteVector32(): ByteVector32 = ByteVector32(ByteArray(32).also { read(it, 0, it.size) })
752-
753-
private fun Input.readByteVector64(): ByteVector64 = ByteVector64(ByteArray(64).also { read(it, 0, it.size) })
754-
755-
private fun Input.readPublicKey() = PublicKey(ByteArray(33).also { read(it, 0, it.size) })
756-
757-
private fun Input.readTxId(): TxId = TxId(readByteVector32())
758-
759-
private fun Input.readPublicNonce() = IndividualNonce(ByteArray(66).also { read(it, 0, it.size) })
760-
761-
private fun Input.readDelimitedByteArray(): ByteArray {
762-
val size = readNumber().toInt()
763-
return ByteArray(size).also { read(it, 0, size) }
764-
}
765-
766-
private fun Input.readLightningMessage() = LightningMessage.decode(readDelimitedByteArray())
767-
768-
private fun <T> Input.readCollection(readElem: () -> T): Collection<T> {
769-
val size = readNumber()
770-
return buildList {
771-
repeat(size.toInt()) {
772-
add(readElem())
773-
}
774-
}
775-
}
776-
777-
private fun <L, R> Input.readEither(readLeft: () -> L, readRight: () -> R): Either<L, R> = when (read()) {
778-
0 -> Either.Left(readLeft())
779-
else -> Either.Right(readRight())
780-
}
781-
782-
private fun <T : Any> Input.readNullable(readNotNull: () -> T): T? = when (read()) {
783-
1 -> readNotNull()
784-
else -> null
785-
}
786-
787729
}

0 commit comments

Comments
 (0)