1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-20 02:27:32 +01:00

Disable local channels below reserve (#763)

The goal is to reduce attempts from other nodes in the network to use
channels that are unbalanced and can't be used to relay payments.

This leaks information about the current balance and is a privacy
tradeoff, particularly in this simplistic implementation. A better way
would be to add some kind of hysteresis in order to prevent trivial
probing of channels.
This commit is contained in:
Pierre-Marie Padiou 2019-01-03 14:02:39 +01:00 committed by GitHub
parent 9da330478a
commit ad31d89e54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 57 additions and 6 deletions

View File

@ -485,7 +485,8 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
// used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly)
blockchain ! WatchConfirmed(self, commitments.commitInput.outPoint.txid, commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED)
context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId))
val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, d.commitments.remoteParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, commitments.localCommit.spec.totalFunds, enable = true)
// we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced
val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDeltaBlocks, d.commitments.remoteParams.htlcMinimumMsat, nodeParams.feeBaseMsat, nodeParams.feeProportionalMillionth, commitments.localCommit.spec.totalFunds, enable = Helpers.aboveReserve(d.commitments))
goto(NORMAL) using store(DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, buried = false, None, initialChannelUpdate, None, None))
case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_WAIT_FOR_FUNDING_LOCKED) if d.commitments.announceChannel =>
@ -623,6 +624,11 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
log.info(s"adding paymentHash=${u.paymentHash} cltvExpiry=${u.cltvExpiry} to htlcs db for commitNumber=$nextCommitNumber")
nodeParams.channelsDb.addOrUpdateHtlcInfo(d.channelId, nextCommitNumber, u.paymentHash, u.cltvExpiry)
}
if (!Helpers.aboveReserve(d.commitments) && Helpers.aboveReserve(commitments1)) {
// we just went above reserve (can't go below), let's refresh our channel_update to enable/disable it accordingly
log.info(s"updating channel_update aboveReserve=${Helpers.aboveReserve(commitments1)}")
self ! TickRefreshChannelUpdate
}
context.system.eventStream.publish(ChannelSignatureSent(self, commitments1))
context.system.eventStream.publish(AvailableBalanceChanged(self, d.channelId, d.shortChannelId, nextRemoteCommit.spec.toRemoteMsat)) // note that remoteCommit.toRemote == toLocal
// we expect a quick response from our peer
@ -777,7 +783,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
// we need to re-announce this shortChannelId
context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, shortChannelId))
// we re-announce the channelUpdate for the same reason
Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = true)
Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = Helpers.aboveReserve(d.commitments))
} else d.channelUpdate
val localAnnSigs_opt = if (d.commitments.announceChannel) {
// if channel is public we need to send our announcement_signatures in order to generate the channel_announcement
@ -820,13 +826,13 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
case Event(TickRefreshChannelUpdate, d: DATA_NORMAL) =>
// periodic refresh is used as a keep alive
log.info(s"sending channel_update announcement (refresh)")
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = true)
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = Helpers.aboveReserve(d.commitments))
// we use GOTO instead of stay because we want to fire transitions
goto(NORMAL) using store(d.copy(channelUpdate = channelUpdate))
case Event(CMD_UPDATE_RELAY_FEE(feeBaseMsat, feeProportionalMillionths), d: DATA_NORMAL) =>
log.info(s"updating relay fees: prevFeeBaseMsat={} nextFeeBaseMsat={} prevFeeProportionalMillionths={} nextFeeProportionalMillionths={}", d.channelUpdate.feeBaseMsat, feeBaseMsat, d.channelUpdate.feeProportionalMillionths, feeProportionalMillionths)
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = true)
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = Helpers.aboveReserve(d.commitments))
// we use GOTO instead of stay because we want to fire transitions
goto(NORMAL) using store(d.copy(channelUpdate = channelUpdate)) replying "ok"
@ -1412,7 +1418,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu
}
}
// re-enable the channel
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, nodeParams.expiryDeltaBlocks, d.commitments.remoteParams.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = true)
val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, nodeParams.expiryDeltaBlocks, d.commitments.remoteParams.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = Helpers.aboveReserve(d.commitments))
goto(NORMAL) using d.copy(commitments = commitments1, channelUpdate = channelUpdate)
}

View File

@ -187,6 +187,23 @@ object Helpers {
AnnouncementSignatures(commitments.channelId, shortChannelId, localNodeSig, localBitcoinSig)
}
/**
* This indicates whether our side of the channel is above the reserve requested by our counterparty. In other words,
* this tells if we can use the channel to make a payment.
*
*/
def aboveReserve(commitments: Commitments)(implicit log: LoggingAdapter): Boolean = {
val remoteCommit = commitments.remoteNextCommitInfo match {
case Left(waitingForRevocation) => waitingForRevocation.nextRemoteCommit
case _ => commitments.remoteCommit
}
val toRemoteSatoshis = remoteCommit.spec.toRemoteMsat / 1000
// NB: this is an approximation (we don't take network fees into account)
val result = toRemoteSatoshis > commitments.remoteParams.channelReserveSatoshis
log.debug(s"toRemoteSatoshis=$toRemoteSatoshis reserve=${commitments.remoteParams.channelReserveSatoshis} aboveReserve=$result for remoteCommitNumber=${remoteCommit.index}")
result
}
def getFinalScriptPubKey(wallet: EclairWallet, chainHash: BinaryData): BinaryData = {
import scala.concurrent.duration._
val finalAddress = Await.result(wallet.getFinalAddress, 40 seconds)

View File

@ -71,12 +71,13 @@ trait StateTestsHelperMethods extends TestKitBase {
tags: Set[String] = Set.empty): Unit = {
import setup._
val channelFlags = if (tags.contains("channels_public")) ChannelFlags.AnnounceChannel else ChannelFlags.Empty
val pushMsat = if (tags.contains("no_push_msat")) 0 else TestConstants.pushMsat
val (aliceParams, bobParams) = (Alice.channelParams, Bob.channelParams)
val aliceInit = Init(aliceParams.globalFeatures, aliceParams.localFeatures)
val bobInit = Init(bobParams.globalFeatures, bobParams.localFeatures)
// reset global feerates (they may have been changed by previous tests)
Globals.feeratesPerKw.set(FeeratesPerKw.single(TestConstants.feeratePerKw))
alice ! INPUT_INIT_FUNDER("00" * 32, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags)
alice ! INPUT_INIT_FUNDER("00" * 32, TestConstants.fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags)
bob ! INPUT_INIT_FUNDEE("00" * 32, bobParams, bob2alice.ref, aliceInit)
alice2bob.expectMsgType[OpenChannel]
alice2bob.forward(bob)

View File

@ -585,6 +585,32 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.remoteNextCommitInfo === Left(waitForRevocation.copy(reSignAsap = true)))
}
test("recv CMD_SIGN (going above reserve)", Tag("no_push_msat")) { f =>
import f._
val sender = TestProbe()
// channel starts with all funds on alice's side, so channel will be initially disabled on bob's side
assert(Announcements.isEnabled(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.channelFlags) === false)
// alice will send enough funds to bob to make it go above reserve
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
sender.send(bob, CMD_FULFILL_HTLC(htlc.id, r))
sender.expectMsg("ok")
bob2alice.expectMsgType[UpdateFulfillHtlc]
// we listen to channel_update events
val listener = TestProbe()
system.eventStream.subscribe(listener.ref, classOf[LocalChannelUpdate])
// actual test starts here
// when signing the fulfill, bob will have its main output go above reserve in alice's commitment tx
sender.send(bob, CMD_SIGN)
sender.expectMsg("ok")
bob2alice.expectMsgType[CommitSig]
// it should update its channel_update
awaitCond(Announcements.isEnabled(bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate.channelFlags) == true)
// and broadcast it
assert(listener.expectMsgType[LocalChannelUpdate].channelUpdate === bob.stateData.asInstanceOf[DATA_NORMAL].channelUpdate)
}
test("recv CommitSig (one htlc received)") { f =>
import f._
val sender = TestProbe()

View File

@ -693,6 +693,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
val pr = sender.expectMsgType[PaymentRequest]
val sendReq = SendPayment(amountMsat, pr.paymentHash, pr.nodeId)
sender.send(paymentInitiator, sendReq)
sender.expectNoMsg()
}
val buffer = TestProbe()