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:
parent
9da330478a
commit
ad31d89e54
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user