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.
Using a blockheight 0 for unconfirmed transactions is a bit hacky.
| } | ||
| 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.
To reduce the cardinality of channel types, only AnchorOutputsZeroFeeHtlcTx channels can support scidAlias or zeroConf.
There was a problem hiding this comment.
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.
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.
A bit reckless, even if option_zeroconf should only be enabled for a set of trusted peers.
| // 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.
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.
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.
|
|
||
| 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.
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.
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.
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.
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.
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 |
t-bast
left a comment
There was a problem hiding this comment.
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 :)
|
|
||
| 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.
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
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>
e9739d1 to
02bfd0f
Compare
t-bast
left a comment
There was a problem hiding this comment.
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.
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
|
🤟 |
|
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.