diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index d3b2a69fc3..a2ed4f93e4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -70,7 +70,7 @@ object Features { } case object StaticRemoteKey extends Feature { - val rfcName = "staticremotekey" + val rfcName = "option_static_remotekey" val mandatory = 12 } @@ -132,8 +132,8 @@ object Features { def hasFeature(features: ByteVector, feature: Feature, support: Option[FeatureSupport]): Boolean = hasFeature(features.bits, feature, support) /** returns true if both have at least optional support */ - def canUseFeature(localFeatures: ByteVector, remoteFeatures: ByteVector, feature: Feature, support: Option[FeatureSupport] = None): Boolean = { - hasFeature(localFeatures, feature, support) && hasFeature(remoteFeatures, feature, support) + def canUseFeature(localFeatures: ByteVector, remoteFeatures: ByteVector, feature: Feature): Boolean = { + hasFeature(localFeatures, feature) && hasFeature(remoteFeatures, feature) } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 95efee8aa5..76e306e14f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -2104,7 +2104,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => val remoteCommitPublished = RemoteCommitPublished(commitTx, None, List.empty, List.empty, Map.empty) val nextData = DATA_CLOSING(d.commitments, fundingTx = None, waitingSince = now, Nil, futureRemoteCommitPublished = Some(remoteCommitPublished)) - goto(CLOSING) using nextData storing() + goto(CLOSING) using nextData storing() // we don't need to claim our main output in the remote commit because it already spends to our wallet address case _ => val remotePerCommitmentPoint = d.remoteChannelReestablish.myCurrentPerCommitmentPoint val remoteCommitPublished = Helpers.Closing.claimRemoteCommitMainOutput(keyManager, d.commitments, remotePerCommitmentPoint, commitTx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 20a5bb0267..1ead8cec9f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -629,8 +629,8 @@ object Commitments { case _ => keyManager.paymentPoint(channelKeyPath).publicKey } val localPaymentPubkey = channelVersion match { - case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => localParams.localPaymentBasepoint.get - case _ => Generators.derivePubKey(keyManager.paymentPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) + case v if v.isSet(USE_STATIC_REMOTEKEY_BIT) => localPaymentBasepoint + case _ => Generators.derivePubKey(localPaymentBasepoint, remotePerCommitmentPoint) } val localHtlcPubkey = Generators.derivePubKey(keyManager.htlcPoint(channelKeyPath).publicKey, remotePerCommitmentPoint) val remoteDelayedPaymentPubkey = Generators.derivePubKey(remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 9769bcf608..b525283a4f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -587,7 +587,7 @@ object Helpers { /** * Claim all the HTLCs that we've received from their current commit tx, if the channel used option_static_remotekey - * we don't claim our main output. + * we don't need to claim our main output because it directly pays to one of our wallet's p2wpkh addresses. * * @param commitments our commitment data, which include payment preimages * @param remoteCommit the remote commitment data to use to claim outputs (it can be their current or next commitment) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 9b85df4fe7..b85587f5da 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -22,7 +22,7 @@ import akka.actor.Status import akka.actor.Status.Failure import akka.testkit.TestProbe import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.{Bech32, ByteVector32, ByteVector64, Crypto, Script, ScriptFlags, Transaction} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, ScriptFlags, Transaction} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.blockchain._ @@ -43,7 +43,6 @@ import fr.acinq.eclair.{TestConstants, TestkitBaseClass, randomBytes32, _} import org.scalatest.{Outcome, Tag} import scodec.bits._ -import scala.concurrent.Future import scala.concurrent.duration._ /** @@ -57,19 +56,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { implicit val log: akka.event.LoggingAdapter = akka.event.NoLogging override def withFixture(test: OneArgTest): Outcome = { - val testWallet = if(test.tags.contains("static_remotekey")){ - val randomKey = PrivateKey(randomBytes32).publicKey - new TestWallet{ - override def getReceivePubkey(receiveAddress: Option[String] = None): Future[Crypto.PublicKey] = Future.successful(randomKey) - override def getReceiveAddress: Future[String] = Future.successful({ - val scriptPubKey = Script.write(Script.pay2wpkh(randomKey)) - Bech32.encodeWitnessAddress("bcrt", 0, scriptPubKey) - }) - } - } else { - new TestWallet - } - val setup = init(wallet = testWallet) + val setup = init() import setup._ within(30 seconds) { reachNormal(setup, test.tags) @@ -1119,7 +1106,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.features == hex"2000") + assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.features == hex"2000") assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localParams.features == hex"2000") def aliceToRemoteScript() = { @@ -1169,8 +1156,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { peer.expectMsg(Peer.Disconnect(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteParams.nodeId)) } - test("recv CMD_FULFILL_HTLC") { f => + private def testReceiveCmdFulfillHtlc(f: FixtureParam): Unit = { import f._ + val sender = TestProbe() val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) @@ -1185,6 +1173,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fulfill)))) } + test("recv CMD_FULFILL_HTLC") { testReceiveCmdFulfillHtlc _ } + + test("recv CMD_FULFILL_HTLC (static_remotekey)", Tag("static_remotekey")) { testReceiveCmdFulfillHtlc _ } + test("recv CMD_FULFILL_HTLC (unknown htlc id)") { f => import f._ val sender = TestProbe() @@ -1219,7 +1211,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { relayerB.expectMsg(CommandBuffer.CommandAck(initialState.channelId, 42)) } - test("recv UpdateFulfillHtlc") { f => + private def testUpdateFulfillHtlc(f: FixtureParam) = { import f._ val sender = TestProbe() val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1239,6 +1231,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(forward.htlc === htlc) } + test("recv UpdateFulfillHtlc") { testUpdateFulfillHtlc _ } + + test("recv UpdateFulfillHtlc (static_remotekey)", Tag("(static_remotekey)")) { testUpdateFulfillHtlc _ } + test("recv UpdateFulfillHtlc (sender has not signed htlc)") { f => import f._ val sender = TestProbe() @@ -1294,7 +1290,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { alice2blockchain.expectMsgType[WatchConfirmed] } - test("recv CMD_FAIL_HTLC") { f => + private def testCmdFailHtlc(f: FixtureParam) = { import f._ val sender = TestProbe() val (r, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1308,8 +1304,13 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(bob.stateData == initialState.copy( commitments = initialState.commitments.copy( localChanges = initialState.commitments.localChanges.copy(initialState.commitments.localChanges.proposed :+ fail)))) + } + test("recv CMD_FAIL_HTLC") { testCmdFailHtlc _ } + + test("recv CMD_FAIL_HTLC (static_remotekey)", Tag("static_remotekey")) { testCmdFailHtlc _ } + test("recv CMD_FAIL_HTLC (unknown htlc id)") { f => import f._ val sender = TestProbe() @@ -1377,7 +1378,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { relayerB.expectMsg(CommandBuffer.CommandAck(initialState.channelId, 42)) } - test("recv UpdateFailHtlc") { f => + private def testUpdateFailHtlc(f: FixtureParam) = { import f._ val sender = TestProbe() val (_, htlc) = addHtlc(50000000 msat, alice, bob, alice2bob, bob2alice) @@ -1395,6 +1396,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { relayerA.expectNoMsg() } + test("recv UpdateFailHtlc") { testUpdateFailHtlc _ } + test("recv UpdateFailHtlc (static_remotekey)", Tag("static_remotekey")) { testUpdateFailHtlc _ } + test("recv UpdateFailMalformedHtlc") { f => import f._ val sender = TestProbe() diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index f862e9e59c..7dd9faca34 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -548,6 +548,22 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(alice.stateName == CLOSED) } + test("recv BITCOIN_TX_CONFIRMED (remote commit, option_static_remotekey)", Tag("static_remotekey")) { f => + import f._ + mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) + // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state + val bobCommitTx = bobCommitTxes.last.commitTx.tx + assert(bobCommitTx.txOut.size == 2) // two main outputs + alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) + + // alice won't create a claimMainOutputTx because her main output is already spendable by the wallet + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimMainOutputTx.isEmpty) + assert(alice.stateName == CLOSING) + // once the remote commit is confirmed the channel is definitively closed + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0, bobCommitTx) + awaitCond(alice.stateName == CLOSED) + } + test("recv BITCOIN_TX_CONFIRMED (remote commit with multiple htlcs for the same payment)") { f => import f._ @@ -632,7 +648,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { relayerA.expectNoMsg(100 millis) } - test("recv BITCOIN_TX_CONFIRMED (next remote commit)") { f => + private def testNextRemoteCommitTxConfirmed(f: FixtureParam): (Transaction, RemoteCommitPublished, Set[UpdateAddHtlc]) = { import f._ // alice sends a first htlc to bob @@ -655,7 +671,12 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(bobCommitTx.txOut.length === 5) // two main outputs + 3 HTLCs val closingState = remoteClose(bobCommitTx, alice, alice2blockchain) assert(closingState.claimHtlcTimeoutTxs.length === 3) + (bobCommitTx, closingState, Set(htlca1, htlca2, htlca3)) + } + test("recv BITCOIN_TX_CONFIRMED (next remote commit)") { f => + import f._ + val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 42, 0, bobCommitTx) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(closingState.claimMainOutputTx.get), 45, 0, closingState.claimMainOutputTx.get) relayerA.expectNoMsg(100 millis) @@ -667,7 +688,26 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { relayerA.expectNoMsg(250 millis) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(closingState.claimHtlcTimeoutTxs(2)), 203, 1, closingState.claimHtlcTimeoutTxs(2)) val forwardedFail3 = relayerA.expectMsgType[ForwardOnChainFail].htlc - assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === Set(htlca1, htlca2, htlca3)) + assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === htlcs) + relayerA.expectNoMsg(250 millis) + awaitCond(alice.stateName == CLOSED) + } + + test("recv BITCOIN_TX_CONFIRMED (next remote commit, static_remotekey)", Tag("static_remotekey")) { f => + import f._ + val (bobCommitTx, closingState, htlcs) = testNextRemoteCommitTxConfirmed(f) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 42, 0, bobCommitTx) + assert(closingState.claimMainOutputTx.isEmpty) // with static_remotekey we don't claim out main output + relayerA.expectNoMsg(100 millis) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(closingState.claimHtlcTimeoutTxs.head), 201, 0, closingState.claimHtlcTimeoutTxs.head) + val forwardedFail1 = relayerA.expectMsgType[ForwardOnChainFail].htlc + relayerA.expectNoMsg(250 millis) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(closingState.claimHtlcTimeoutTxs(1)), 202, 0, closingState.claimHtlcTimeoutTxs(1)) + val forwardedFail2 = relayerA.expectMsgType[ForwardOnChainFail].htlc + relayerA.expectNoMsg(250 millis) + alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(closingState.claimHtlcTimeoutTxs(2)), 203, 1, closingState.claimHtlcTimeoutTxs(2)) + val forwardedFail3 = relayerA.expectMsgType[ForwardOnChainFail].htlc + assert(Set(forwardedFail1, forwardedFail2, forwardedFail3) === htlcs) relayerA.expectNoMsg(250 millis) awaitCond(alice.stateName == CLOSED) } @@ -722,23 +762,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { relayerA.expectNoMsg(100 millis) } - test("recv BITCOIN_TX_CONFIRMED (remote commit, option_static_remotekey)", Tag("static_remotekey")) { f => - import f._ - mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - // bob publishes his last current commit tx, the one it had when entering NEGOTIATING state - val bobCommitTx = bobCommitTxes.last.commitTx.tx - assert(bobCommitTx.txOut.size == 2) // two main outputs - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) - - // alice won't create a claimMainOutputTx because her main output is already spendable by the wallet - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].remoteCommitPublished.get.claimMainOutputTx.isEmpty) - assert(alice.stateName == CLOSING) - // once the remote commit is confirmed the channel is definitively closed - alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0, bobCommitTx) - awaitCond(alice.stateName == CLOSED) - } - - test("recv BITCOIN_TX_CONFIRMED (future remote commit)") { f => + private def testFutureRemoteCommitTxConfirmed(f: FixtureParam): Transaction = { import f._ val sender = TestProbe() val oldStateData = alice.stateData @@ -777,6 +801,12 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx assert(bobCommitTx.txOut.length === 4) // two main outputs + 2 HTLCs alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) + bobCommitTx + } + + test("recv BITCOIN_TX_CONFIRMED (future remote commit)") { f => + import f._ + val bobCommitTx = testFutureRemoteCommitTxConfirmed(f) // alice is able to claim its main output val claimMainTx = alice2blockchain.expectMsgType[PublishAsap].tx Transaction.correctlySpends(claimMainTx, bobCommitTx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) @@ -791,42 +821,9 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(alice.stateName == CLOSED) } - test("recv BITCOIN_TX_CONFIRMED (future remote commit, option_static_remotekey)", Tag("static_remotekey")) { f => import f._ - val sender = TestProbe() - val oldStateData = alice.stateData - val (ra1, htlca1) = addHtlc(25000000 msat, alice, bob, alice2bob, bob2alice) - crossSign(alice, bob, alice2bob, bob2alice) - fulfillHtlc(htlca1.id, ra1, bob, alice, bob2alice, alice2bob) - crossSign(bob, alice, bob2alice, alice2bob) - // we simulate a disconnection - sender.send(alice, INPUT_DISCONNECTED) - sender.send(bob, INPUT_DISCONNECTED) - awaitCond(alice.stateName == OFFLINE) - awaitCond(bob.stateName == OFFLINE) - assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion.isSet(ChannelVersion.USE_STATIC_REMOTEKEY_BIT)) - assert(bob.stateData.asInstanceOf[DATA_NORMAL].commitments.channelVersion.isSet(ChannelVersion.USE_STATIC_REMOTEKEY_BIT)) - // then we manually replace alice's state with an older one - alice.setState(OFFLINE, oldStateData) - // then we reconnect them - val aliceInit = Init(Alice.nodeParams.features) - val bobInit = Init(Bob.nodeParams.features) - sender.send(alice, INPUT_RECONNECTED(alice2bob.ref, aliceInit, bobInit)) - sender.send(bob, INPUT_RECONNECTED(bob2alice.ref, bobInit, aliceInit)) - // peers exchange channel_reestablish messages - alice2bob.expectMsgType[ChannelReestablish] - bob2alice.expectMsgType[ChannelReestablish] - // alice then realizes it has an old state... - bob2alice.forward(alice) - // ... and ask bob to publish its current commitment - val error = alice2bob.expectMsgType[Error] - assert(new String(error.data.toArray) === PleasePublishYourCommitment(channelId(alice)).getMessage) - // alice now waits for bob to publish its commitment - awaitCond(alice.stateName == WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT) - // bob is nice and publishes its commitment - val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobCommitTx) + val bobCommitTx = testFutureRemoteCommitTxConfirmed(f) // using option_static_remotekey alice doesn't need to swipe her output awaitCond(alice.stateName == CLOSING, 10 seconds) alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(bobCommitTx), 0, 0, bobCommitTx) @@ -834,7 +831,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { awaitCond(alice.stateName == CLOSED, 10 seconds) } - test("recv BITCOIN_FUNDING_SPENT (one revoked tx)") { f => + private def testFundingSpentRevokedTx(f: FixtureParam) = { import f._ mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] @@ -842,6 +839,13 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val bobRevokedTx = bobCommitTxes.head.commitTx.tx alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) + awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].copy(revokedCommitPublished = Nil) == initialState) + } + + test("recv BITCOIN_FUNDING_SPENT (one revoked tx)") { f => + import f._ + testFundingSpentRevokedTx(f) // alice publishes and watches the penalty tx alice2blockchain.expectMsgType[PublishAsap] // claim-main alice2blockchain.expectMsgType[PublishAsap] // main-penalty @@ -851,20 +855,11 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { alice2blockchain.expectMsgType[WatchSpent] // main-penalty alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty alice2blockchain.expectNoMsg(1 second) - - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].copy(revokedCommitPublished = Nil) == initialState) } - test("recv BITCOIN_FUNDING_SPENT (one revoked tx, option_static_remotekey)", Tag("static_remotekey")) { f => import f._ - mutualClose(alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain) - val initialState = alice.stateData.asInstanceOf[DATA_CLOSING] - // bob publishes one of his revoked txes - val bobRevokedTx = bobCommitTxes.head.commitTx.tx - alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, bobRevokedTx) - + testFundingSpentRevokedTx(f) // alice publishes and watches the penalty tx, but she won't claim her main output (claim-main) alice2blockchain.expectMsgType[PublishAsap] // main-penalty alice2blockchain.expectMsgType[PublishAsap] // htlc-penalty @@ -872,9 +867,6 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { alice2blockchain.expectMsgType[WatchSpent] // main-penalty alice2blockchain.expectMsgType[WatchSpent] // htlc-penalty alice2blockchain.expectNoMsg(1 second) - - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1) - awaitCond(alice.stateData.asInstanceOf[DATA_CLOSING].copy(revokedCommitPublished = Nil) == initialState) } test("recv BITCOIN_FUNDING_SPENT (multiple revoked tx)") { f => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 533e3f334c..8573b2befb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -850,7 +850,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService val pr = sender.expectMsgType[PaymentRequest] // then we make the actual payment - sender.send(nodes("C").paymentInitiator, SendPaymentRequest(amountMsat, pr.paymentHash, nodes("F6").nodeParams.nodeId, routeParams = integrationTestRouteParams, maxAttempts = 1)) + sender.send(nodes("C").paymentInitiator, SendPaymentRequest(amountMsat, pr.paymentHash, nodes("F6").nodeParams.nodeId, maxAttempts = 1)) val paymentId = sender.expectMsgType[UUID](5 seconds) val ps = sender.expectMsgType[PaymentSent](5 seconds) assert(ps.id == paymentId) @@ -865,7 +865,8 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(commitmentIndex == initialCommitmentIndex + 1) // script pubkeys of toRemote output remained the same across commitments - assert(toRemoteOutC.publicKeyScript == toRemoteOutCNew.publicKeyScript) + assert(toRemoteOutCNew.publicKeyScript == toRemoteOutC.publicKeyScript) + assert(toRemoteOutCNew.amount < toRemoteOutC.amount) // now let's force close the channel and check the toRemote is what we had at the beginning sender.send(nodes("F6").register, Forward(channelId, CMD_FORCECLOSE)) @@ -881,6 +882,13 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService // the unilateral close contains the static toRemote output assert(Transaction.read(rawTx).txOut.exists(_.publicKeyScript == toRemoteOutC.publicKeyScript)) + + // bury the unilateral close in a block, since there are no outputs to claim the channel can go to CLOSED state + generateBlocks(bitcoincli, 2) + awaitCond({ + sender.send(nodes("C").register, Forward(channelId, CMD_GETSTATE)) + sender.expectMsgType[State] == CLOSED + }, max = 20 seconds, interval = 1 second) } /** diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index b0680c5003..3bac9d8923 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -22,13 +22,13 @@ import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} import akka.actor.Status.Failure import akka.testkit.{TestFSMRef, TestProbe} import com.google.common.net.HostAndPort -import fr.acinq.bitcoin.Btc +import fr.acinq.bitcoin.{Btc, Script} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.{EclairWallet, TestWallet} import fr.acinq.eclair.channel.states.StateTestsHelperMethods -import fr.acinq.eclair.channel.{Channel, ChannelCreated, HasCommitments} +import fr.acinq.eclair.channel.{CMD_GETINFO, Channel, ChannelCreated, ChannelVersion, DATA_NORMAL, HasCommitments, NORMAL, RES_GETINFO} import fr.acinq.eclair.io.Peer._ import fr.acinq.eclair.wire.{ChannelCodecsSpec, Color, NodeAddress, NodeAnnouncement} import org.scalatest.{Outcome, Tag} @@ -54,6 +54,7 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { import com.softwaremill.quicklens._ val aliceParams = TestConstants.Alice.nodeParams + .modify(_.features).setToIf(test.tags.contains("static_remotekey"))(hex"2200") .modify(_.features).setToIf(test.tags.contains("wumbo"))(hex"80000") .modify(_.maxFundingSatoshis).setToIf(test.tags.contains("high-max-funding-satoshis"))(Btc(0.9)) .modify(_.autoReconnect).setToIf(test.tags.contains("auto_reconnect"))(true) @@ -194,4 +195,20 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(channelCreated.initialFeeratePerKw == peer.feeEstimator.getFeeratePerKw(peer.feeTargets.commitmentBlockTarget)) assert(channelCreated.fundingTxFeeratePerKw.get == peer.feeEstimator.getFeeratePerKw(peer.feeTargets.fundingBlockTarget)) } + + test("use correct final script if option_static_remotekey is negotiated", Tag("static_remotekey")) { f => + import f._ + + val probe = TestProbe() + connect(remoteNodeId, switchboard, peer, peerConnection, remoteInit = wire.Init(hex"2200")) // Bob supports option_static_remotekey + peer.stateData.channels.foreach { case (_, channelRef) => + probe.send(channelRef, CMD_GETINFO) + val info = probe.expectMsgType[RES_GETINFO] + assert(info.state == NORMAL) + val commitments = info.data.asInstanceOf[DATA_NORMAL].commitments + assert(commitments.channelVersion.isSet(ChannelVersion.USE_STATIC_REMOTEKEY_BIT)) + assert(commitments.localParams.localPaymentBasepoint.isDefined) + assert(commitments.localParams.defaultFinalScriptPubKey === Script.pay2wpkh(commitments.localParams.localPaymentBasepoint.get)) + } + } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index 7988e3754d..86725bee35 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -101,9 +101,9 @@ class ChannelCodecsSpec extends FunSuite { isFunder = Random.nextBoolean(), features = randomBytes(256)) val encoded = localParamsCodec(ChannelVersion.ZEROES).encode(o).require - val decoded = localParamsCodec(ChannelVersion.ZEROES).decode(encoded).require - assert(o.localPaymentBasepoint.isEmpty) - assert(o === decoded.value) + val decoded = localParamsCodec(ChannelVersion.ZEROES).decode(encoded).require.value + assert(decoded.localPaymentBasepoint.isEmpty) + assert(o === decoded) // Backwards-compatibility: decode localparams with global features. val withGlobalFeatures = hex"033b1d42aa7c6a1a3502cbcfe4d2787e9f96237465cd1ba675f50cadf0be17092500010000002a0000000026cb536b00000000568a2768000000004f182e8d0000000040dd1d3d10e3040d00422f82d368b09056d1dcb2d67c4e8cae516abbbc8932f2b7d8f93b3be8e8cc6b64bb164563d567189bad0e07e24e821795aaef2dcbb9e5c1ad579961680202b38de5dd5426c524c7523b1fcdcf8c600d47f4b96a6dd48516b8e0006e81c83464b2800db0f3f63ceeb23a81511d159bae9ad07d10c0d144ba2da6f0cff30e7154eb48c908e9000101000001044500" @@ -124,9 +124,9 @@ class ChannelCodecsSpec extends FunSuite { isFunder = Random.nextBoolean(), features = randomBytes(256)) val encoded1 = localParamsCodec(ChannelVersion.STATIC_REMOTEKEY).encode(o1).require - val decoded1 = localParamsCodec(ChannelVersion.STATIC_REMOTEKEY).decode(encoded1).require - assert(o1.localPaymentBasepoint.isDefined) - assert(o1 === decoded1.value) + val decoded1 = localParamsCodec(ChannelVersion.STATIC_REMOTEKEY).decode(encoded1).require.value + assert(decoded1.localPaymentBasepoint.isDefined) + assert(o1 === decoded1) } test("encode/decode remoteparams") {