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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 2 additions & 2 deletions
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.*
Lines changed: 55 additions & 0 deletions
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+
}
Lines changed: 71 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 3 additions & 3 deletions
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))

0 commit comments

Comments
 (0)