Skip to content

Commit 733c6e7

Browse files
authored
Refactor global balance tests (#1874)
Move balance check test to their own file instead of adding bloat to the NormalStateSpec tests. Remove unnecessary parts that belonged to the NormalStateSpec test.
1 parent e9df4ee commit 733c6e7

File tree

2 files changed

+178
-66
lines changed

2 files changed

+178
-66
lines changed

eclair-core/src/test/scala/fr/acinq/eclair/balance/CheckBalanceSpec.scala

+177-9
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,192 @@
11
package fr.acinq.eclair.balance
22

3+
import akka.pattern.pipe
4+
import akka.testkit.TestProbe
35
import fr.acinq.bitcoin.{ByteVector32, SatoshiLong}
46
import fr.acinq.eclair.balance.CheckBalance.{ClosingBalance, OffChainBalance, PossiblyPublishedMainAndHtlcBalance, PossiblyPublishedMainBalance}
7+
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{apply => _, _}
58
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}
613
import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._
714
import fr.acinq.eclair.db.pg.PgUtils.using
8-
import fr.acinq.eclair.randomBytes32
915
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
1120
import org.sqlite.SQLiteConfig
1221

1322
import java.io.File
1423
import java.sql.DriverManager
1524
import scala.collection.immutable.Queue
25+
import scala.concurrent.ExecutionContext.Implicits.global
1626
import scala.concurrent.duration.DurationInt
17-
import scala.concurrent.{Await, ExecutionContext, Future}
27+
import scala.concurrent.{ExecutionContext, Future}
1828

19-
class CheckBalanceSpec extends AnyFunSuite {
29+
class CheckBalanceSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTestsBase {
2030

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") { _ =>
22190
val dbFile = new File("eclair.sqlite")
23191
val sqliteConfig = new SQLiteConfig()
24192
sqliteConfig.setReadOnly(true)
@@ -40,8 +208,7 @@ class CheckBalanceSpec extends AnyFunSuite {
40208
println(res.total)
41209
}
42210

43-
test("tx pruning") {
44-
211+
test("tx pruning") { _ =>
45212
val txids = (for (_ <- 0 until 20) yield randomBytes32()).toList
46213
val knownTxids = Set(txids(1), txids(3), txids(4), txids(6), txids(9), txids(12), txids(13))
47214

@@ -83,8 +250,9 @@ class CheckBalanceSpec extends AnyFunSuite {
83250
)
84251
)
85252

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]
88256

89257
assert(bal2 == OffChainBalance(
90258
closing = ClosingBalance(

0 commit comments

Comments
 (0)