-
Notifications
You must be signed in to change notification settings - Fork 267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for zero-conf and scid-alias #2224
Conversation
682d9cb
to
d091440
Compare
f21861b
to
aa2956f
Compare
case w: WatchFundingConfirmed if confirmations == 0 && w.minDepth == 0 => | ||
// if the tx doesn't have confirmations but we don't require any, we reply with a fake block index | ||
// otherwise, we get the real short id | ||
context.self ! TriggerEvent(w.replyTo, w, WatchFundingConfirmedTriggered(BlockHeight(0), 0, tx)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a blockheight 0 for unconfirmed transactions is a bit hacky.
@@ -101,33 +101,46 @@ object ChannelTypes { | |||
override def commitmentFormat: CommitmentFormat = UnsafeLegacyAnchorOutputsCommitmentFormat | |||
override def toString: String = "anchor_outputs" | |||
} | |||
case object AnchorOutputsZeroFeeHtlcTx extends SupportedChannelType { | |||
override def features: Set[InitFeature] = Set(Features.StaticRemoteKey, Features.AnchorOutputsZeroFeeHtlcTx) | |||
case class AnchorOutputsZeroFeeHtlcTx(scidAlias: Boolean, zeroConf: Boolean) extends SupportedChannelType { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To reduce the cardinality of channel types, only AnchorOutputsZeroFeeHtlcTx
channels can support scidAlias
or zeroConf
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with that choice, it's a sensible one, but it means we won't be compatible with LDK (I believe they haven't shipped anchor outputs yet). But it's fine, we can wait for them to ship anchor outputs which should high be on their roadmap.
We do need to add tests to verify that we correctly reject channel types that use zero_conf
or scid_alias
with previous commitment types.
* As funder we trust ourselves to not double spend funding txs: we could always use a zero-confirmation watch, | ||
* but we need a scid to send the initial channel_update and remote may not provide an alias (and we don't want to | ||
* trust the real scid sent by remote in their channel_ready). So we always wait for one conf, except if the channel | ||
* has the zero-conf feature (because presumably the peer will sends an alias in that case). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: a consequence of this is that the channel will only be zero-conf if the feature is set. It's not enough for the fundee to just trust us and send an early channel_ready
because we will already have decided to wait for one conf (we would receive their alias in the channel_ready
, but we put the watch before that in the channel fsm). I guess if we receive an early channel_ready
with an alias, we can put a 2nd zero-conf watch, the first one will be ignored.
* @return number of confirmations needed | ||
*/ | ||
def minDepthFundee(channelConf: ChannelConf, channelFeatures: ChannelFeatures, fundingSatoshis: Satoshi): Long = fundingSatoshis match { | ||
case _ if channelFeatures.hasFeature(Features.ZeroConf) => 0 // zero-conf stay zero-conf, whatever the funding amount is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit reckless, even if option_zeroconf
should only be enabled for a set of trusted peers.
@@ -59,6 +60,7 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm | |||
// we pass these to helpers classes so that they have the logging context | |||
implicit def implicitLog: DiagnosticLoggingAdapter = diagLog | |||
|
|||
context.system.eventStream.subscribe(self, classOf[ShortChannelIdAssigned]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really happy with the router listening to yet another event, but I couldn't find a better way to fix the race condition (see bf39b07).
At least I was able to have ChannelRelayer
stop listening anymore to that same event 🤷
("shortChannelId" | realshortchannelid) :: | ||
("lastSent" | channelReadyCodec)).map { | ||
case commitments :: shortChannelId :: lastSent :: HNil => | ||
DATA_WAIT_FOR_CHANNEL_READY(commitments, realShortChannelId_opt = Some(shortChannelId), localAlias = shortChannelId.toAlias, lastSent = lastSent) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note how for backward compatibility we set both the realShortChannelId_opt
and localAlias
to the same value. It may not actually be the real scid on feature branches.
@@ -56,6 +65,8 @@ object ShortChannelId { | |||
def outputIndex(shortChannelId: ShortChannelId): Int = (shortChannelId.id & 0xFFFF).toInt | |||
|
|||
def coordinates(shortChannelId: ShortChannelId): TxCoordinates = TxCoordinates(blockHeight(shortChannelId), txIndex(shortChannelId), outputIndex(shortChannelId)) | |||
|
|||
def generateLocalAlias(): LocalAlias = new ShortChannelId(System.nanoTime()) with LocalAlias // TODO: fixme (duplicate, etc.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since generating an alias only happens at channel creation, we can afford doing something "costly". For example this could be an AtomicLong
that we initialize at startup by checking all currently assigned localAlias
, and then we do an addAndGet
with a random increment when we need to generate a new alias.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we really need a counter, wouldn't a randomLong()
in the range that cannot be used by normal channels be enough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We cannot afford a duplicate here though and the space isn't that big. We should expect a collision for 5 billion attempts on 64 bits and only 80k attempts for 32 bits right? https://en.wikipedia.org/wiki/Birthday_attack
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's true, the space isn't that big, but I'm afraid there may be privacy issues with generating those incrementally...I'll check what other implementations do
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did some maths to confirm this, let me know if my calculations look correct:
- scids are 64 bits long
- we want to reserve the range [0; 2^22] for real scids
- that leaves us with 2^42 available values for aliases
- we'll consider that we want to have 250k channels using aliases (~2^18)
- the probability of a collision is then p(n) ~= 2^36 / 2^43 ~= 0.8%
This is indeed not great! But using a counter that we increment leaks information about the approximate creation time of the channel, which could let an attacker find the channel outpoint on-chain...so I believe that what we should do instead is:
- generate an alias randomly in the [2^22:2^64] range
- check our alias map to see if it's already assigned and re-roll if it is
It means we'll probably need to send a message to the router to obtain a new alias, the router is where we can guarantee uniqueness.
Rebased |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a first pass on some details and high-level architecture, I'll dive into the actual usage of scid_alias
in a second pass later, my brain wasn't able to handle it today :)
@@ -56,6 +65,8 @@ object ShortChannelId { | |||
def outputIndex(shortChannelId: ShortChannelId): Int = (shortChannelId.id & 0xFFFF).toInt | |||
|
|||
def coordinates(shortChannelId: ShortChannelId): TxCoordinates = TxCoordinates(blockHeight(shortChannelId), txIndex(shortChannelId), outputIndex(shortChannelId)) | |||
|
|||
def generateLocalAlias(): LocalAlias = new ShortChannelId(System.nanoTime()) with LocalAlias // TODO: fixme (duplicate, etc.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we really need a counter, wouldn't a randomLong()
in the range that cannot be used by normal channels be enough?
eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala
Outdated
Show resolved
Hide resolved
eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala
Outdated
Show resolved
Hide resolved
eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala
Outdated
Show resolved
Hide resolved
eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala
Show resolved
Hide resolved
eclair-core/src/test/scala/fr/acinq/eclair/wire/protocol/LightningMessageCodecsSpec.scala
Outdated
Show resolved
Hide resolved
eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala
Outdated
Show resolved
Hide resolved
eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala
Outdated
Show resolved
Hide resolved
802ac3a
to
9e1a675
Compare
fc7d4ee
to
ea388c5
Compare
Rebased. |
Follow up on #2269 (comment): I think it's better to defer to the dual-funding PR, as the problem still doesn't exist, and the present PR is already heavy enough. |
…e subclass registered
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelayer.scala
Outdated
Show resolved
Hide resolved
eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala
Outdated
Show resolved
Hide resolved
eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala
Outdated
Show resolved
Hide resolved
da46456
to
c20f672
Compare
Instead of using a watch with minDepth=0, we can directly skip the wait_for_funding_confirmed state when using 0-conf, which is less hacky. Co-authored-by: pm47 <pm.padiou@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
e9739d1
to
02bfd0f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, we're almost ready to go!
assert(!getChannelData(bob, channelId).asInstanceOf[PersistentChannelData].commitments.channelFeatures.hasFeature(ZeroConf)) | ||
} | ||
|
||
test("open a channel alice-bob (zero-conf disabled on both sides, requested via channel type by alice)") { f => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a follow-up PR, we should introduce a whitelist of nodes from which we accept 0-conf channels. If nodes are in that whitelist, we should accept the channel_type
even though they didn't set the feature in their init
.
We should maybe even go further than that and use 0-conf with those peers even if the channel_type
doesn't include it. Either they'll accept it and send their channel_ready
, or they won't and it's ok, we'll just wait for their channel_ready
automatically.
eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ZeroConfActivationSpec.scala
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🚀 ⚡
🤟 |
Follow-ups to this PR include:
|
Builds on #2221 and #2269.
This implements lightning/bolts#910.
Commit-by-commit review strongly advised. 7e28c1a is a bit of a mouthful, but the other commits are mostly small.
Commit e7f7ef0 is optional, it skips blockchain validation for local channels, which would save a lot of bitcoind calls at startup for large nodes.