1
1
package fr .acinq .eclair .balance
2
2
3
+ import akka .pattern .pipe
4
+ import akka .testkit .TestProbe
3
5
import fr .acinq .bitcoin .{ByteVector32 , SatoshiLong }
4
6
import fr .acinq .eclair .balance .CheckBalance .{ClosingBalance , OffChainBalance , PossiblyPublishedMainAndHtlcBalance , PossiblyPublishedMainBalance }
7
+ import fr .acinq .eclair .blockchain .bitcoind .ZmqWatcher .{apply => _ , _ }
5
8
import fr .acinq .eclair .blockchain .bitcoind .rpc .ExtendedBitcoinClient
9
+ import fr .acinq .eclair .channel .Helpers .Closing .{CurrentRemoteClose , LocalClose }
10
+ import fr .acinq .eclair .channel .publish .TxPublisher .PublishRawTx
11
+ import fr .acinq .eclair .channel .states .StateTestsBase
12
+ import fr .acinq .eclair .channel .{CLOSING , CMD_SIGN , DATA_CLOSING , DATA_NORMAL }
6
13
import fr .acinq .eclair .db .jdbc .JdbcUtils .ExtendedResultSet ._
7
14
import fr .acinq .eclair .db .pg .PgUtils .using
8
- import fr .acinq .eclair .randomBytes32
9
15
import fr .acinq .eclair .wire .internal .channel .ChannelCodecs .stateDataCodec
10
- import org .scalatest .funsuite .AnyFunSuite
16
+ import fr .acinq .eclair .wire .protocol .{CommitSig , Error , RevokeAndAck }
17
+ import fr .acinq .eclair .{MilliSatoshiLong , TestKitBaseClass , randomBytes32 }
18
+ import org .scalatest .Outcome
19
+ import org .scalatest .funsuite .FixtureAnyFunSuiteLike
11
20
import org .sqlite .SQLiteConfig
12
21
13
22
import java .io .File
14
23
import java .sql .DriverManager
15
24
import scala .collection .immutable .Queue
25
+ import scala .concurrent .ExecutionContext .Implicits .global
16
26
import scala .concurrent .duration .DurationInt
17
- import scala .concurrent .{Await , ExecutionContext , Future }
27
+ import scala .concurrent .{ExecutionContext , Future }
18
28
19
- class CheckBalanceSpec extends AnyFunSuite {
29
+ class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTestsBase {
20
30
21
- ignore(" compute from eclair.sqlite" ) {
31
+ type FixtureParam = SetupFixture
32
+
33
+ override def withFixture (test : OneArgTest ): Outcome = {
34
+ val setup = init()
35
+ within(30 seconds) {
36
+ reachNormal(setup, test.tags)
37
+ withFixture(test.toNoArgTest(setup))
38
+ }
39
+ }
40
+
41
+ test(" take published remote commit tx into account" ) { f =>
42
+ import f ._
43
+
44
+ // We add 3 htlcs Alice -> Bob (one of them below dust) and 2 htlcs Bob -> Alice
45
+ addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice)
46
+ val (ra2, htlca2) = addHtlc(100000000 msat, alice, bob, alice2bob, bob2alice)
47
+ val (_, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice)
48
+ val (rb1, htlcb1) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob)
49
+ val (_, htlcb2) = addHtlc(55000000 msat, bob, alice, bob2alice, alice2bob)
50
+ crossSign(alice, bob, alice2bob, bob2alice)
51
+ // And fulfill one htlc in each direction without signing a new commit tx
52
+ fulfillHtlc(htlca2.id, ra2, bob, alice, bob2alice, alice2bob)
53
+ fulfillHtlc(htlcb1.id, rb1, alice, bob, alice2bob, bob2alice)
54
+
55
+ // bob publishes his current commit tx
56
+ val bobCommitTx = bob.stateData.asInstanceOf [DATA_NORMAL ].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
57
+ assert(bobCommitTx.txOut.size == 6 ) // two main outputs and 4 pending htlcs
58
+ alice ! WatchFundingSpentTriggered (bobCommitTx)
59
+ // in response to that, alice publishes its claim txs
60
+ val claimTxs = for (_ <- 0 until 4 ) yield alice2blockchain.expectMsgType[PublishRawTx ].tx
61
+
62
+ val commitments = alice.stateData.asInstanceOf [DATA_CLOSING ].commitments
63
+ val remoteCommitPublished = alice.stateData.asInstanceOf [DATA_CLOSING ].remoteCommitPublished.get
64
+ val knownPreimages = Set ((commitments.channelId, htlcb1.id))
65
+ assert(CheckBalance .computeRemoteCloseBalance(commitments, CurrentRemoteClose (commitments.remoteCommit, remoteCommitPublished), knownPreimages) ===
66
+ PossiblyPublishedMainAndHtlcBalance (
67
+ toLocal = Map (remoteCommitPublished.claimMainOutputTx.get.tx.txid -> remoteCommitPublished.claimMainOutputTx.get.tx.txOut.head.amount),
68
+ htlcs = claimTxs.drop(1 ).map(claimTx => claimTx.txid -> claimTx.txOut.head.amount.toBtc).toMap,
69
+ htlcsUnpublished = htlca3.amountMsat.truncateToSatoshi
70
+ ))
71
+ // assuming alice gets the preimage for the 2nd htlc
72
+ val knownPreimages1 = Set ((commitments.channelId, htlcb1.id), (commitments.channelId, htlcb2.id))
73
+ assert(CheckBalance .computeRemoteCloseBalance(commitments, CurrentRemoteClose (commitments.remoteCommit, remoteCommitPublished), knownPreimages1) ===
74
+ PossiblyPublishedMainAndHtlcBalance (
75
+ toLocal = Map (remoteCommitPublished.claimMainOutputTx.get.tx.txid -> remoteCommitPublished.claimMainOutputTx.get.tx.txOut.head.amount),
76
+ htlcs = claimTxs.drop(1 ).map(claimTx => claimTx.txid -> claimTx.txOut.head.amount.toBtc).toMap,
77
+ htlcsUnpublished = htlca3.amountMsat.truncateToSatoshi + htlcb2.amountMsat.truncateToSatoshi
78
+ ))
79
+ }
80
+
81
+ test(" take published next remote commit tx into account" ) { f =>
82
+ import f ._
83
+
84
+ // We add 3 htlcs Alice -> Bob (one of them below dust) and 2 htlcs Bob -> Alice
85
+ addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice)
86
+ val (ra2, htlca2) = addHtlc(100000000 msat, alice, bob, alice2bob, bob2alice)
87
+ val (_, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice)
88
+ val (rb1, htlcb1) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob)
89
+ val (_, htlcb2) = addHtlc(55000000 msat, bob, alice, bob2alice, alice2bob)
90
+ crossSign(alice, bob, alice2bob, bob2alice)
91
+ // And fulfill one htlc in each direction
92
+ fulfillHtlc(htlca2.id, ra2, bob, alice, bob2alice, alice2bob)
93
+ fulfillHtlc(htlcb1.id, rb1, alice, bob, alice2bob, bob2alice)
94
+ // alice signs but we intercept bob's revocation
95
+ alice ! CMD_SIGN ()
96
+ alice2bob.expectMsgType[CommitSig ]
97
+ alice2bob.forward(bob)
98
+ bob2alice.expectMsgType[RevokeAndAck ]
99
+
100
+ // as far as alice knows, bob currently has two valid unrevoked commitment transactions
101
+ // bob publishes his current commit tx
102
+ val bobCommitTx = bob.stateData.asInstanceOf [DATA_NORMAL ].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
103
+ assert(bobCommitTx.txOut.size == 5 ) // two main outputs and 3 pending htlcs
104
+ alice ! WatchFundingSpentTriggered (bobCommitTx)
105
+
106
+ // in response to that, alice publishes its claim txs
107
+ val claimTxs = for (_ <- 0 until 3 ) yield alice2blockchain.expectMsgType[PublishRawTx ].tx
108
+
109
+ val commitments = alice.stateData.asInstanceOf [DATA_CLOSING ].commitments
110
+ val remoteCommitPublished = alice.stateData.asInstanceOf [DATA_CLOSING ].nextRemoteCommitPublished.get
111
+ val knownPreimages = Set ((commitments.channelId, htlcb1.id))
112
+ assert(CheckBalance .computeRemoteCloseBalance(commitments, CurrentRemoteClose (commitments.remoteNextCommitInfo.left.get.nextRemoteCommit, remoteCommitPublished), knownPreimages) ===
113
+ PossiblyPublishedMainAndHtlcBalance (
114
+ toLocal = Map (remoteCommitPublished.claimMainOutputTx.get.tx.txid -> remoteCommitPublished.claimMainOutputTx.get.tx.txOut.head.amount),
115
+ htlcs = claimTxs.drop(1 ).map(claimTx => claimTx.txid -> claimTx.txOut.head.amount.toBtc).toMap,
116
+ htlcsUnpublished = htlca3.amountMsat.truncateToSatoshi
117
+ ))
118
+ // assuming alice gets the preimage for the 2nd htlc
119
+ val knownPreimages1 = Set ((commitments.channelId, htlcb1.id), (commitments.channelId, htlcb2.id))
120
+ assert(CheckBalance .computeRemoteCloseBalance(commitments, CurrentRemoteClose (commitments.remoteNextCommitInfo.left.get.nextRemoteCommit, remoteCommitPublished), knownPreimages1) ===
121
+ PossiblyPublishedMainAndHtlcBalance (
122
+ toLocal = Map (remoteCommitPublished.claimMainOutputTx.get.tx.txid -> remoteCommitPublished.claimMainOutputTx.get.tx.txOut.head.amount),
123
+ htlcs = claimTxs.drop(1 ).map(claimTx => claimTx.txid -> claimTx.txOut.head.amount.toBtc).toMap,
124
+ htlcsUnpublished = htlca3.amountMsat.truncateToSatoshi + htlcb2.amountMsat.truncateToSatoshi
125
+ ))
126
+ }
127
+
128
+ test(" take published local commit tx into account" ) { f =>
129
+ import f ._
130
+
131
+ // We add 3 htlcs Alice -> Bob (one of them below dust) and 2 htlcs Bob -> Alice
132
+ val (_, htlca1) = addHtlc(250000000 msat, alice, bob, alice2bob, bob2alice)
133
+ val (ra2, htlca2) = addHtlc(100000000 msat, alice, bob, alice2bob, bob2alice)
134
+ val (_, htlca3) = addHtlc(10000 msat, alice, bob, alice2bob, bob2alice)
135
+ val (rb1, htlcb1) = addHtlc(50000000 msat, bob, alice, bob2alice, alice2bob)
136
+ addHtlc(55000000 msat, bob, alice, bob2alice, alice2bob)
137
+ crossSign(alice, bob, alice2bob, bob2alice)
138
+ // And fulfill one htlc in each direction without signing a new commit tx
139
+ fulfillHtlc(htlca2.id, ra2, bob, alice, bob2alice, alice2bob)
140
+ fulfillHtlc(htlcb1.id, rb1, alice, bob, alice2bob, bob2alice)
141
+
142
+ // alice publishes her commit tx
143
+ val aliceCommitTx = alice.stateData.asInstanceOf [DATA_NORMAL ].commitments.localCommit.commitTxAndRemoteSig.commitTx.tx
144
+ alice ! Error (ByteVector32 .Zeroes , " oops" )
145
+ assert(alice2blockchain.expectMsgType[PublishRawTx ].tx.txid === aliceCommitTx.txid)
146
+ assert(aliceCommitTx.txOut.size == 6 ) // two main outputs and 4 pending htlcs
147
+ awaitCond(alice.stateName == CLOSING )
148
+ assert(alice.stateData.asInstanceOf [DATA_CLOSING ].localCommitPublished.isDefined)
149
+ val commitments = alice.stateData.asInstanceOf [DATA_CLOSING ].commitments
150
+ val localCommitPublished = alice.stateData.asInstanceOf [DATA_CLOSING ].localCommitPublished.get
151
+ val knownPreimages = Set ((commitments.channelId, htlcb1.id))
152
+ assert(CheckBalance .computeLocalCloseBalance(commitments, LocalClose (commitments.localCommit, localCommitPublished), knownPreimages) ===
153
+ PossiblyPublishedMainAndHtlcBalance (
154
+ toLocal = Map (localCommitPublished.claimMainDelayedOutputTx.get.tx.txid -> localCommitPublished.claimMainDelayedOutputTx.get.tx.txOut.head.amount),
155
+ htlcs = Map .empty,
156
+ htlcsUnpublished = htlca1.amountMsat.truncateToSatoshi + htlca3.amountMsat.truncateToSatoshi + htlcb1.amountMsat.truncateToSatoshi
157
+ ))
158
+
159
+ alice2blockchain.expectMsgType[PublishRawTx ] // claim-main
160
+ val htlcTx1 = alice2blockchain.expectMsgType[PublishRawTx ].tx
161
+ val htlcTx2 = alice2blockchain.expectMsgType[PublishRawTx ].tx
162
+ val htlcTx3 = alice2blockchain.expectMsgType[PublishRawTx ].tx
163
+ alice2blockchain.expectMsgType[WatchTxConfirmed ] // commit tx
164
+ alice2blockchain.expectMsgType[WatchTxConfirmed ] // main-delayed
165
+ alice2blockchain.expectMsgType[WatchOutputSpent ] // htlc 1
166
+ alice2blockchain.expectMsgType[WatchOutputSpent ] // htlc 2
167
+ alice2blockchain.expectMsgType[WatchOutputSpent ] // htlc 3
168
+ alice2blockchain.expectMsgType[WatchOutputSpent ] // htlc 4
169
+
170
+ // 3rd-stage txs are published when htlc txs confirm
171
+ val claimHtlcDelayedTxs = Seq (htlcTx1, htlcTx2, htlcTx3).map { htlcTimeoutTx =>
172
+ alice ! WatchOutputSpentTriggered (htlcTimeoutTx)
173
+ assert(alice2blockchain.expectMsgType[WatchTxConfirmed ].txId === htlcTimeoutTx.txid)
174
+ alice ! WatchTxConfirmedTriggered (2701 , 3 , htlcTimeoutTx)
175
+ val claimHtlcDelayedTx = alice2blockchain.expectMsgType[PublishRawTx ].tx
176
+ assert(alice2blockchain.expectMsgType[WatchTxConfirmed ].txId === claimHtlcDelayedTx.txid)
177
+ claimHtlcDelayedTx
178
+ }
179
+ awaitCond(alice.stateData.asInstanceOf [DATA_CLOSING ].localCommitPublished.get.claimHtlcDelayedTxs.length == 3 )
180
+
181
+ assert(CheckBalance .computeLocalCloseBalance(commitments, LocalClose (commitments.localCommit, alice.stateData.asInstanceOf [DATA_CLOSING ].localCommitPublished.get), knownPreimages) ===
182
+ PossiblyPublishedMainAndHtlcBalance (
183
+ toLocal = Map (localCommitPublished.claimMainDelayedOutputTx.get.tx.txid -> localCommitPublished.claimMainDelayedOutputTx.get.tx.txOut.head.amount),
184
+ htlcs = claimHtlcDelayedTxs.map(claimTx => claimTx.txid -> claimTx.txOut.head.amount.toBtc).toMap,
185
+ htlcsUnpublished = htlca3.amountMsat.truncateToSatoshi
186
+ ))
187
+ }
188
+
189
+ ignore(" compute from eclair.sqlite" ) { _ =>
22
190
val dbFile = new File (" eclair.sqlite" )
23
191
val sqliteConfig = new SQLiteConfig ()
24
192
sqliteConfig.setReadOnly(true )
@@ -40,8 +208,7 @@ class CheckBalanceSpec extends AnyFunSuite {
40
208
println(res.total)
41
209
}
42
210
43
- test(" tx pruning" ) {
44
-
211
+ test(" tx pruning" ) { _ =>
45
212
val txids = (for (_ <- 0 until 20 ) yield randomBytes32()).toList
46
213
val knownTxids = Set (txids(1 ), txids(3 ), txids(4 ), txids(6 ), txids(9 ), txids(12 ), txids(13 ))
47
214
@@ -83,8 +250,9 @@ class CheckBalanceSpec extends AnyFunSuite {
83
250
)
84
251
)
85
252
86
- val bal2 = Await .result(CheckBalance .prunePublishedTransactions(bal1, bitcoinClient)(ExecutionContext .Implicits .global), 10 seconds)
87
-
253
+ val sender = TestProbe ()
254
+ CheckBalance .prunePublishedTransactions(bal1, bitcoinClient).pipeTo(sender.ref)
255
+ val bal2 = sender.expectMsgType[OffChainBalance ]
88
256
89
257
assert(bal2 == OffChainBalance (
90
258
closing = ClosingBalance (
0 commit comments