Skip to content

Commit 7dfe46a

Browse files
pm47t-bast
andauthored
Emit storage actions at channel opening/closing (#219)
The goal is to use those use those events to populate the payments and make it seem like: - Phoenix receives money when an incoming channel with `pushMsat > 0` is created - Phoenix sends money when a channel is closed. For channel creation, we emit the action only when we know for sure that the channel will be created (that is, when we move to `WaitForFundingConfirmed`). Co-authored-by: Bastien Teinturier <[email protected]>
1 parent 597f027 commit 7dfe46a

File tree

8 files changed

+82
-22
lines changed

8 files changed

+82
-22
lines changed

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

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ sealed class ChannelEvent {
4141
val localParams: LocalParams,
4242
val remoteInit: Init,
4343
val channelFlags: Byte,
44-
val channelVersion: ChannelVersion
44+
val channelVersion: ChannelVersion,
45+
val channelOrigin: ChannelOrigin? = null
4546
) : ChannelEvent() {
4647
init {
4748
require(channelVersion.hasStaticRemotekey) { "channel version $channelVersion is invalid (static_remote_key is not set)" }
@@ -91,6 +92,8 @@ sealed class ChannelAction {
9192
data class HtlcInfo(val channelId: ByteVector32, val commitmentNumber: Long, val paymentHash: ByteVector32, val cltvExpiry: CltvExpiry)
9293
data class StoreHtlcInfos(val htlcs: List<HtlcInfo>) : Storage()
9394
data class GetHtlcInfos(val revokedCommitTxId: ByteVector32, val commitmentNumber: Long) : Storage()
95+
data class StoreIncomingAmount(val amount: MilliSatoshi, val origin: ChannelOrigin?) : Storage()
96+
data class StoreOutgoingAmount(val amount: MilliSatoshi) : Storage()
9497
}
9598

9699
data class ProcessIncomingHtlc(val add: UpdateAddHtlc) : ChannelAction()
@@ -152,7 +155,20 @@ sealed class ChannelState {
152155
fun process(event: ChannelEvent): Pair<ChannelState, List<ChannelAction>> {
153156
return try {
154157
val (newState, actions) = processInternal(event)
155-
Pair(newState, newState.updateActions(actions))
158+
val actions1 = when {
159+
this is WaitForFundingCreated && newState is WaitForFundingConfirmed -> {
160+
actions + ChannelAction.Storage.StoreIncomingAmount(pushAmount, channelOrigin)
161+
}
162+
// we only want to fire the PaymentSent event when we transition to Closing for the first time
163+
this is WaitForInit && newState is Closing -> actions
164+
this is Closing && newState is Closing -> actions
165+
this is ChannelStateWithCommitments && newState is Closing -> {
166+
actions + ChannelAction.Storage.StoreOutgoingAmount(this.commitments.localCommit.spec.toLocal)
167+
}
168+
else -> actions
169+
}
170+
val actions2 = newState.updateActions(actions1)
171+
Pair(newState, actions2)
156172
} catch (t: Throwable) {
157173
handleLocalError(event, t)
158174
}
@@ -174,6 +190,7 @@ sealed class ChannelState {
174190
it is ChannelAction.Message.Send && it.message is RevokeAndAck -> it.copy(message = it.message.copy(channelData = Serialization.encrypt(privateKey.value, this)))
175191
it is ChannelAction.Message.Send && it.message is ClosingSigned -> it.copy(message = it.message.copy(channelData = Serialization.encrypt(privateKey.value, this)))
176192
else -> it
193+
177194
}
178195
}
179196
else -> actions
@@ -544,10 +561,10 @@ data class WaitForInit(override val staticParams: StaticParams, override val cur
544561
// In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script.
545562
// See https://github.com/lightningnetwork/lightning-rfc/pull/714.
546563
tlvStream = TlvStream(
547-
if (event.channelVersion.isSet(ChannelVersion.ZERO_RESERVE_BIT)) {
548-
listOf(ChannelTlv.UpfrontShutdownScript(ByteVector.empty), ChannelTlv.ChannelVersionTlv(event.channelVersion))
549-
} else {
550-
listOf(ChannelTlv.UpfrontShutdownScript(ByteVector.empty))
564+
buildList {
565+
add(ChannelTlv.UpfrontShutdownScript(ByteVector.empty))
566+
if (event.channelVersion.isSet(ChannelVersion.ZERO_RESERVE_BIT)) add(ChannelTlv.ChannelVersionTlv(event.channelVersion))
567+
if (event.channelOrigin != null) add(ChannelTlv.ChannelOriginTlv(event.channelOrigin))
551568
}
552569
)
553570
)
@@ -1108,6 +1125,7 @@ data class WaitForOpenChannel(
11081125
return Pair(Aborted(staticParams, currentTip, currentOnChainFeerates), listOf(ChannelAction.Message.Send(Error(temporaryChannelId, err.value.message))))
11091126
}
11101127
}
1128+
val channelOrigin = event.message.tlvStream.records.filterIsInstance<ChannelTlv.ChannelOriginTlv>().firstOrNull()?.channelOrigin
11111129

11121130
val fundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey
11131131
val channelKeyPath = keyManager.channelKeyPath(localParams, channelVersion)
@@ -1160,6 +1178,7 @@ data class WaitForOpenChannel(
11601178
event.message.firstPerCommitmentPoint,
11611179
event.message.channelFlags,
11621180
channelVersion,
1181+
channelOrigin,
11631182
accept
11641183
)
11651184
Pair(nextState, listOf(ChannelAction.Message.Send(accept)))
@@ -1198,6 +1217,7 @@ data class WaitForFundingCreated(
11981217
val remoteFirstPerCommitmentPoint: PublicKey,
11991218
val channelFlags: Byte,
12001219
val channelVersion: ChannelVersion,
1220+
val channelOrigin: ChannelOrigin?,
12011221
val lastSent: AcceptChannel
12021222
) : ChannelState() {
12031223
override fun processInternal(event: ChannelEvent): Pair<ChannelState, List<ChannelAction>> {
@@ -1271,7 +1291,16 @@ data class WaitForFundingCreated(
12711291
logger.info { "c:$channelId will wait for $fundingMinDepth confirmations" }
12721292
val watchSpent = WatchSpent(channelId, commitInput.outPoint.txid, commitInput.outPoint.index.toInt(), commitments.commitInput.txOut.publicKeyScript, BITCOIN_FUNDING_SPENT)
12731293
val watchConfirmed = WatchConfirmed(channelId, commitInput.outPoint.txid, commitments.commitInput.txOut.publicKeyScript, fundingMinDepth.toLong(), BITCOIN_FUNDING_DEPTHOK)
1274-
val nextState = WaitForFundingConfirmed(staticParams, currentTip, currentOnChainFeerates, commitments, null, currentBlockHeight.toLong(), null, Either.Right(fundingSigned))
1294+
val nextState = WaitForFundingConfirmed(
1295+
staticParams,
1296+
currentTip,
1297+
currentOnChainFeerates,
1298+
commitments,
1299+
null,
1300+
currentBlockHeight.toLong(),
1301+
null,
1302+
Either.Right(fundingSigned)
1303+
)
12751304
val actions = listOf(
12761305
ChannelAction.Blockchain.SendWatch(watchSpent),
12771306
ChannelAction.Blockchain.SendWatch(watchConfirmed),

src/commonMain/kotlin/fr/acinq/eclair/channel/ChannelTypes.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ data class ClosingTxProposed(val unsignedTx: Transaction, val localClosingSigned
417417
/** This gives the reason for creating a new channel */
418418
@Serializable
419419
sealed class ChannelOrigin {
420-
data class PayToOpenOrigin(val paymentHash: ByteVector32) : ChannelOrigin()
421-
data class SwapInOrigin(val bitcoinAddress: String) : ChannelOrigin()
420+
abstract val fee: Satoshi
421+
data class PayToOpenOrigin(val paymentHash: ByteVector32, override val fee: Satoshi) : ChannelOrigin()
422+
data class SwapInOrigin(val bitcoinAddress: String, override val fee: Satoshi) : ChannelOrigin()
422423
}

src/commonMain/kotlin/fr/acinq/eclair/wire/ChannelTlv.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package fr.acinq.eclair.wire
22

33
import fr.acinq.bitcoin.ByteVector
44
import fr.acinq.bitcoin.ByteVector32
5+
import fr.acinq.bitcoin.Satoshi
56
import fr.acinq.bitcoin.io.Input
67
import fr.acinq.bitcoin.io.Output
78
import fr.acinq.eclair.channel.ChannelOrigin
@@ -74,14 +75,20 @@ sealed class ChannelTlv : Tlv {
7475
}
7576

7677
companion object : TlvValueReader<ChannelOriginTlv> {
77-
const val tag: Long = 0x47000003
78+
const val tag: Long = 0x47000005
7879

7980
override fun read(input: Input): ChannelOriginTlv {
8081
val origin = when (LightningCodecs.u16(input)) {
81-
1 -> ChannelOrigin.PayToOpenOrigin(ByteVector32(LightningCodecs.bytes(input, 32)))
82+
1 -> ChannelOrigin.PayToOpenOrigin(
83+
paymentHash = ByteVector32(LightningCodecs.bytes(input, 32)),
84+
fee = Satoshi(LightningCodecs.u64(input))
85+
)
8286
2 -> {
8387
val len = LightningCodecs.bigSize(input)
84-
ChannelOrigin.SwapInOrigin(LightningCodecs.bytes(input, len).decodeToString())
88+
ChannelOrigin.SwapInOrigin(
89+
bitcoinAddress = LightningCodecs.bytes(input, len).decodeToString(),
90+
fee = Satoshi(LightningCodecs.u64(input))
91+
)
8592
}
8693
else -> TODO("Unsupported channel origin discriminator")
8794
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1235,4 +1235,4 @@ data class SwapInConfirmed(
12351235
)
12361236
}
12371237
}
1238-
}
1238+
}

src/commonTest/kotlin/fr/acinq/eclair/channel/TestsHelper.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ object TestsHelper {
6666
channelVersion: ChannelVersion = ChannelVersion.STANDARD,
6767
currentHeight: Int = TestConstants.defaultBlockHeight,
6868
fundingAmount: Satoshi = TestConstants.fundingAmount,
69-
pushMsat: MilliSatoshi = TestConstants.pushMsat
69+
pushMsat: MilliSatoshi = TestConstants.pushMsat,
70+
channelOrigin: ChannelOrigin? = null
7071
): Triple<WaitForAcceptChannel, WaitForOpenChannel, OpenChannel> {
7172
var alice: ChannelState = WaitForInit(
7273
StaticParams(TestConstants.Alice.nodeParams, TestConstants.Bob.keyManager.nodeId),
@@ -96,7 +97,8 @@ object TestsHelper {
9697
aliceChannelParams,
9798
bobInit,
9899
channelFlags,
99-
channelVersion
100+
channelVersion,
101+
channelOrigin
100102
)
101103
)
102104
alice = ra.first

src/commonTest/kotlin/fr/acinq/eclair/channel/states/ClosingTestsCommon.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1546,7 +1546,8 @@ class ClosingTestsCommon : EclairTestSuite() {
15461546
val alice1 = run {
15471547
val (alice1, actions1) = alice.process(ChannelEvent.ExecuteCommand(CMD_FORCECLOSE))
15481548
assertTrue(alice1 is Closing)
1549-
assertEquals(6, actions1.size)
1549+
assertEquals(7, actions1.size)
1550+
assertTrue(actions1.contains(ChannelAction.Storage.StoreOutgoingAmount(alice1.commitments.localCommit.spec.toLocal)))
15501551

15511552
val error = actions1.hasOutgoingMessage<Error>()
15521553
assertEquals(ForcedLocalCommit(alice.channelId).message, error.toAscii())
@@ -1572,7 +1573,8 @@ class ClosingTestsCommon : EclairTestSuite() {
15721573
val bob1 = run {
15731574
val (bob1, actions1) = bob.process(ChannelEvent.ExecuteCommand(CMD_FORCECLOSE))
15741575
assertTrue(bob1 is Closing)
1575-
assertEquals(6, actions1.size)
1576+
assertEquals(7, actions1.size)
1577+
assertTrue(actions1.contains(ChannelAction.Storage.StoreOutgoingAmount(bob1.commitments.localCommit.spec.toLocal)))
15761578

15771579
val error = actions1.hasOutgoingMessage<Error>()
15781580
assertEquals(ForcedLocalCommit(alice.channelId).message, error.toAscii())

src/commonTest/kotlin/fr/acinq/eclair/channel/states/WaitForFundingCreatedTestsCommon.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ class WaitForFundingCreatedTestsCommon : EclairTestSuite() {
2929
actions1.has<ChannelAction.Storage.StoreState>()
3030
}
3131

32+
@Test
33+
fun `recv FundingCreated (with channel origin)`() {
34+
val (_, bob, fundingCreated) = init(ChannelVersion.STANDARD, TestConstants.fundingAmount, TestConstants.pushMsat, ChannelOrigin.PayToOpenOrigin(ByteVector32.One, 42.sat))
35+
val (bob1, actions1) = bob.process(ChannelEvent.MessageReceived(fundingCreated))
36+
assertTrue { bob1 is WaitForFundingConfirmed }
37+
actions1.findOutgoingMessage<FundingSigned>()
38+
actions1.hasWatch<WatchSpent>()
39+
actions1.hasWatch<WatchConfirmed>()
40+
actions1.has<ChannelAction.ChannelId.IdSwitch>()
41+
actions1.has<ChannelAction.Storage.StoreState>()
42+
actions1.contains(ChannelAction.Storage.StoreIncomingAmount(TestConstants.pushMsat, ChannelOrigin.PayToOpenOrigin(ByteVector32.One, 42.sat)))
43+
}
44+
3245
@Test
3346
fun `recv FundingCreated (funder can't pay fees)`() {
3447
val (_, bob, fundingCreated) = init(ChannelVersion.STANDARD, 1_000_100.sat, 1_000_000.sat.toMilliSatoshi())
@@ -64,8 +77,8 @@ class WaitForFundingCreatedTestsCommon : EclairTestSuite() {
6477
}
6578

6679
companion object {
67-
fun init(channelVersion: ChannelVersion, fundingAmount: Satoshi, pushAmount: MilliSatoshi): Triple<WaitForFundingSigned, WaitForFundingCreated, FundingCreated> {
68-
val (a, b, open) = TestsHelper.init(channelVersion, 0, fundingAmount, pushAmount)
80+
fun init(channelVersion: ChannelVersion, fundingAmount: Satoshi, pushAmount: MilliSatoshi, channelOrigin: ChannelOrigin? = null): Triple<WaitForFundingSigned, WaitForFundingCreated, FundingCreated> {
81+
val (a, b, open) = TestsHelper.init(channelVersion, 0, fundingAmount, pushAmount, channelOrigin)
6982
val (b1, actions) = b.process(ChannelEvent.MessageReceived(open))
7083
val accept = actions.findOutgoingMessage<AcceptChannel>()
7184
val (a1, actions2) = a.process(ChannelEvent.MessageReceived(accept))

src/commonTest/kotlin/fr/acinq/eclair/wire/OpenTlvTestsCommon.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import fr.acinq.eclair.channel.ChannelOrigin
55
import fr.acinq.eclair.channel.ChannelVersion
66
import fr.acinq.eclair.crypto.assertArrayEquals
77
import fr.acinq.eclair.tests.utils.EclairTestSuite
8-
import fr.acinq.eclair.utils.toByteVector
8+
import fr.acinq.eclair.utils.sat
99
import fr.acinq.secp256k1.Hex
1010
import kotlin.test.Test
1111
import kotlin.test.assertEquals
@@ -39,8 +39,14 @@ class OpenTlvTestsCommon : EclairTestSuite() {
3939
@Test
4040
fun `channel origin TLV`() {
4141
val testCases = listOf(
42-
Pair(ChannelOrigin.PayToOpenOrigin(ByteVector32.fromValidHex("187bf923f7f11ef732b73c417eb5a57cd4667b20a6f130ff505cd7ad3ab87281")), Hex.decode("fe47000003 22 0001 187bf923f7f11ef732b73c417eb5a57cd4667b20a6f130ff505cd7ad3ab87281")),
43-
Pair(ChannelOrigin.SwapInOrigin("3AuM8hSkXBetjdHxWthRFiH6hYhqF2Prjr"), Hex.decode("fe47000003 25 0002 223341754d3868536b584265746a644878577468524669483668596871463250726a72")),
42+
Pair(
43+
ChannelOrigin.PayToOpenOrigin(ByteVector32.fromValidHex("187bf923f7f11ef732b73c417eb5a57cd4667b20a6f130ff505cd7ad3ab87281"), 1234.sat),
44+
Hex.decode("fe47000005 2a 0001 187bf923f7f11ef732b73c417eb5a57cd4667b20a6f130ff505cd7ad3ab87281 00000000000004d2")
45+
),
46+
Pair(
47+
ChannelOrigin.SwapInOrigin("3AuM8hSkXBetjdHxWthRFiH6hYhqF2Prjr", 420.sat),
48+
Hex.decode("fe47000005 2d 0002 223341754d3868536b584265746a644878577468524669483668596871463250726a72 00000000000001a4")
49+
)
4450
)
4551

4652
@Suppress("UNCHECKED_CAST")

0 commit comments

Comments
 (0)