Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add peer storage backup #721

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/Features.kt
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ sealed class Feature {
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

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

@Serializable
object ChannelType : Feature() {
override val rfcName get() = "option_channel_type"
Expand Down Expand Up @@ -337,6 +344,7 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.ShutdownAnySegwit,
Feature.DualFunding,
Feature.Quiescence,
Feature.ProvideStorage,
Feature.ChannelType,
Feature.PaymentMetadata,
Feature.TrampolinePayment,
Expand Down
49 changes: 48 additions & 1 deletion src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@ class Peer(
private var _channels by _channelsFlow
val channels: Map<ByteVector32, ChannelState> get() = _channelsFlow.value

// backup stored by our peer
private var peerStorage: EncryptedChannelData? = null

private val _connectionState = MutableStateFlow<Connection>(Connection.CLOSED(null))
val connectionState: StateFlow<Connection> get() = _connectionState

Expand Down Expand Up @@ -255,6 +258,15 @@ class Peer(
)
)
return state.run { ctx.process(cmd) }
.let { (state1, actions) ->
if (theirInit?.features?.hasFeature(Feature.ProvideStorage) == true &&
state1 is PersistedChannelState &&
actions.any { it is ChannelAction.Message.Send && it.message is UpdatesChannelData }) {
Pair(state1, actions + ChannelAction.Message.Send(PeerStorageStore(EncryptedChannelData.from(nodeParams.nodePrivateKey, state1))))
} else {
Pair(state1, actions)
}
}
.also { (state1, _) ->
if (state1::class != state::class) {
ctx.logger.info { "${state.stateName} -> ${state1.stateName}" }
Expand Down Expand Up @@ -1096,14 +1108,49 @@ class Peer(
processActions(msg.temporaryChannelId, peerConnection, actions1 + actions2)
}
}
is PeerStorageRetrieval -> {
peerStorage = msg.ecd
}
is ChannelReestablish -> {
val local: ChannelState? = _channels[msg.channelId]
val backup: DeserializationResult? = msg.channelData.takeIf { !it.isEmpty() }?.let { channelData ->
val backupLegacy: DeserializationResult? = msg.channelData.takeIf { !it.isEmpty() }?.let { channelData ->
PersistedChannelState
.from(nodeParams.nodePrivateKey, channelData)
.onFailure { logger.warning(it) { "unreadable backup" } }
.getOrNull()
}
val backupPeerStorage: DeserializationResult? = peerStorage?.let { it.takeIf { !it.isEmpty() }?.let { channelData ->
PersistedChannelState
.from(nodeParams.nodePrivateKey, channelData)
.onFailure { logger.warning(it) { "unreadable backup" } }
.getOrNull()
}}
val backup = when {
backupPeerStorage == null && backupLegacy == null -> {
logger.warning { "No backup available" }
null
}
backupPeerStorage == null -> {
logger.warning { "No peer storage backup, using legacy channel data" }
backupLegacy
}
backupPeerStorage is DeserializationResult.Success && backupPeerStorage.state.channelId != msg.channelId -> {
logger.error { "Received peer storage backup for different channel, using legacy channel data" }
backupLegacy
}
backupPeerStorage != backupLegacy -> {
logger.error { "Peer storage backup and legacy channel data do not match" }
// Selecting most recent backup
when {
backupPeerStorage is DeserializationResult.UnknownVersion && (backupLegacy !is DeserializationResult.UnknownVersion || backupPeerStorage.version > backupLegacy.version) ->
backupPeerStorage
backupPeerStorage is DeserializationResult.Success && backupPeerStorage.state is ChannelStateWithCommitments && backupLegacy is DeserializationResult.Success && backupLegacy.state is ChannelStateWithCommitments && backupLegacy.state.commitments.isMoreRecent(backupPeerStorage.state.commitments) ->
backupLegacy
else -> backupPeerStorage
}
}
else -> backupPeerStorage
}

suspend fun recoverChannel(recovered: PersistedChannelState) {
db.channels.addOrUpdateChannel(recovered)
Expand Down
48 changes: 43 additions & 5 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ interface LightningMessage {
SpliceInit.type -> SpliceInit.read(stream)
SpliceAck.type -> SpliceAck.read(stream)
SpliceLocked.type -> SpliceLocked.read(stream)
PeerStorageStore.type -> PeerStorageStore.read(stream)
PeerStorageRetrieval.type -> PeerStorageRetrieval.read(stream)
else -> UnknownMessage(code.toLong())
}
}
Expand Down Expand Up @@ -167,6 +169,8 @@ data class EncryptedChannelData(val data: ByteVector) {
}
}

interface UpdatesChannelData : HasChannelId

interface HasEncryptedChannelData : HasChannelId {
val channelData: EncryptedChannelData
fun withNonEmptyChannelData(ecd: EncryptedChannelData): HasEncryptedChannelData
Expand Down Expand Up @@ -491,7 +495,7 @@ data class TxSignatures(
val txId: TxId,
val witnesses: List<ScriptWitness>,
val tlvs: TlvStream<TxSignaturesTlv> = TlvStream.empty()
) : InteractiveTxMessage(), HasChannelId, HasEncryptedChannelData {
) : InteractiveTxMessage(), HasChannelId, HasEncryptedChannelData, UpdatesChannelData {
constructor(
channelId: ByteVector32,
tx: Transaction,
Expand Down Expand Up @@ -1233,7 +1237,7 @@ data class CommitSig(
val signature: ByteVector64,
val htlcSignatures: List<ByteVector64>,
val tlvStream: TlvStream<CommitSigTlv> = TlvStream.empty()
) : HtlcMessage, HasChannelId, HasEncryptedChannelData {
) : HtlcMessage, HasChannelId, HasEncryptedChannelData, UpdatesChannelData {
override val type: Long get() = CommitSig.type

override val channelData: EncryptedChannelData get() = tlvStream.get<CommitSigTlv.ChannelData>()?.ecb ?: EncryptedChannelData.empty
Expand Down Expand Up @@ -1278,7 +1282,7 @@ data class RevokeAndAck(
val perCommitmentSecret: PrivateKey,
val nextPerCommitmentPoint: PublicKey,
val tlvStream: TlvStream<RevokeAndAckTlv> = TlvStream.empty()
) : HtlcMessage, HasChannelId, HasEncryptedChannelData {
) : HtlcMessage, HasChannelId, HasEncryptedChannelData, UpdatesChannelData {
override val type: Long get() = RevokeAndAck.type

override val channelData: EncryptedChannelData get() = tlvStream.get<RevokeAndAckTlv.ChannelData>()?.ecb ?: EncryptedChannelData.empty
Expand Down Expand Up @@ -1555,7 +1559,7 @@ data class Shutdown(
override val channelId: ByteVector32,
val scriptPubKey: ByteVector,
val tlvStream: TlvStream<ShutdownTlv> = TlvStream.empty()
) : ChannelMessage, HasChannelId, HasEncryptedChannelData, ForbiddenMessageDuringSplice {
) : ChannelMessage, HasChannelId, HasEncryptedChannelData, ForbiddenMessageDuringSplice, UpdatesChannelData {
override val type: Long get() = Shutdown.type

override val channelData: EncryptedChannelData get() = tlvStream.get<ShutdownTlv.ChannelData>()?.ecb ?: EncryptedChannelData.empty
Expand Down Expand Up @@ -1589,7 +1593,7 @@ data class ClosingSigned(
val feeSatoshis: Satoshi,
val signature: ByteVector64,
val tlvStream: TlvStream<ClosingSignedTlv> = TlvStream.empty()
) : ChannelMessage, HasChannelId, HasEncryptedChannelData {
) : ChannelMessage, HasChannelId, HasEncryptedChannelData, UpdatesChannelData {
override val type: Long get() = ClosingSigned.type

override val channelData: EncryptedChannelData get() = tlvStream.get<ClosingSignedTlv.ChannelData>()?.ecb ?: EncryptedChannelData.empty
Expand Down Expand Up @@ -1955,6 +1959,40 @@ data class RecommendedFeerates(
}
}

data class PeerStorageStore(val ecd: EncryptedChannelData) : LightningMessage {
override val type: Long get() = PeerStorageStore.type

override fun write(out: Output) {
LightningCodecs.writeU16(ecd.data.size(), out)
LightningCodecs.writeBytes(ecd.data, out)
}

companion object : LightningMessageReader<PeerStorageStore> {
const val type: Long = 7

override fun read(input: Input): PeerStorageStore {
return PeerStorageStore(EncryptedChannelData(LightningCodecs.bytes(input, LightningCodecs.u16(input)).toByteVector()))
}
}
}

data class PeerStorageRetrieval(val ecd: EncryptedChannelData) : LightningMessage {
override val type: Long get() = PeerStorageRetrieval.type

override fun write(out: Output) {
LightningCodecs.writeU16(ecd.data.size(), out)
LightningCodecs.writeBytes(ecd.data, out)
}

companion object : LightningMessageReader<PeerStorageRetrieval> {
const val type: Long = 9

override fun read(input: Input): PeerStorageRetrieval {
return PeerStorageRetrieval(EncryptedChannelData(LightningCodecs.bytes(input, LightningCodecs.u16(input)).toByteVector()))
}
}
}

data class UnknownMessage(override val type: Long) : LightningMessage {
override fun write(out: Output) = TODO("Serialization of unknown messages is not implemented")
}