diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index bef04f1e6..a6ac85de8 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -44,6 +44,7 @@ eclair { # local-features = "" # } ] + sync-whitelist = [] // a list of public keys; if non-empty, we will only do the initial sync with those peers channel-flags = 1 // announce channels dust-limit-satoshis = 546 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 5b13c67c6..21e20c532 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -47,6 +47,7 @@ case class NodeParams(keyManager: KeyManager, globalFeatures: ByteVector, localFeatures: ByteVector, overrideFeatures: Map[PublicKey, (ByteVector, ByteVector)], + syncWhitelist: Set[PublicKey], dustLimit: Satoshi, onChainFeeConf: OnChainFeeConf, maxHtlcValueInFlightMsat: UInt64, @@ -162,6 +163,8 @@ object NodeParams { p -> (gf, lf) }.toMap + val syncWhitelist: Set[PublicKey] = config.getStringList("sync-whitelist").map(s => PublicKey(ByteVector.fromValidHex(s))).toSet + val socksProxy_opt = if (config.getBoolean("socks5.enabled")) { Some(Socks5ProxyParams( address = new InetSocketAddress(config.getString("socks5.host"), config.getInt("socks5.port")), @@ -204,6 +207,7 @@ object NodeParams { globalFeatures = ByteVector.fromValidHex(config.getString("global-features")), localFeatures = ByteVector.fromValidHex(config.getString("local-features")), overrideFeatures = overrideFeatures, + syncWhitelist = syncWhitelist, dustLimit = dustLimitSatoshis, onChainFeeConf = OnChainFeeConf( feeTargets = feeTargets, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 5757953b0..ed54b12b2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -159,8 +159,12 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, authenticator: A Some(QueryChannelRangeTlv.QueryFlags(QueryChannelRangeTlv.QueryFlags.WANT_ALL)) case _ => None } - log.info(s"sending sync channel range query with flags_opt=$flags_opt") - router ! SendChannelQuery(remoteNodeId, d.transport, flags_opt = flags_opt) + if (nodeParams.syncWhitelist.isEmpty || nodeParams.syncWhitelist.contains(remoteNodeId)) { + log.info(s"sending sync channel range query with flags_opt=$flags_opt") + router ! SendChannelQuery(remoteNodeId, d.transport, flags_opt = flags_opt) + } else { + log.info("not syncing with this peer") + } } // let's bring existing/requested channels online diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index ae5874939..1495bc968 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -70,6 +70,7 @@ object TestConstants { globalFeatures = ByteVector.empty, localFeatures = ByteVector(0), overrideFeatures = Map.empty, + syncWhitelist = Set.empty, dustLimit = 1100 sat, onChainFeeConf = OnChainFeeConf( feeTargets = FeeTargets(6, 2, 2, 6), @@ -142,6 +143,7 @@ object TestConstants { globalFeatures = ByteVector.empty, localFeatures = ByteVector.empty, // no announcement overrideFeatures = Map.empty, + syncWhitelist = Set.empty, dustLimit = 1000 sat, onChainFeeConf = OnChainFeeConf( feeTargets = FeeTargets(6, 2, 2, 6), 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 597d938bb..8079ae366 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,7 +22,7 @@ import akka.actor.FSM.{CurrentState, SubscribeTransitionCallBack, Transition} import akka.actor.{ActorRef, PoisonPill} import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{Satoshi} +import fr.acinq.bitcoin.Satoshi import fr.acinq.eclair.TestConstants._ import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.{EclairWallet, TestWallet} @@ -31,10 +31,10 @@ import fr.acinq.eclair.channel.{ChannelCreated, HasCommitments} import fr.acinq.eclair.crypto.TransportHandler import fr.acinq.eclair.io.Peer._ import fr.acinq.eclair.router.RoutingSyncSpec.makeFakeRoutingInfo -import fr.acinq.eclair.router.{Rebroadcast, RoutingSyncSpec} -import fr.acinq.eclair.wire.{ChannelCodecsSpec, Color, EncodedShortChannelIds, EncodingType, Error, IPv4, NodeAddress, NodeAnnouncement, Ping, Pong, QueryShortChannelIds, Tlv, TlvStream} +import fr.acinq.eclair.router.{Rebroadcast, RoutingSyncSpec, SendChannelQuery} +import fr.acinq.eclair.wire.{ChannelCodecsSpec, Color, EncodedShortChannelIds, EncodingType, Error, IPv4, NodeAddress, NodeAnnouncement, Ping, Pong, QueryShortChannelIds, TlvStream} import org.scalatest.{Outcome, Tag} -import scodec.bits.ByteVector +import scodec.bits.{ByteVector, _} import scala.concurrent.duration._ @@ -52,15 +52,6 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { case class FixtureParam(remoteNodeId: PublicKey, authenticator: TestProbe, watcher: TestProbe, router: TestProbe, relayer: TestProbe, connection: TestProbe, transport: TestProbe, peer: TestFSMRef[Peer.State, Peer.Data, Peer]) override protected def withFixture(test: OneArgTest): Outcome = { - val aParams = Alice.nodeParams - val aliceParams = test.tags.contains("with_node_announcements") match { - case true => - val bobAnnouncement = NodeAnnouncement(randomBytes64, ByteVector.empty, 1, Bob.nodeParams.nodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", fakeIPAddress :: Nil) - aParams.db.network.addNode(bobAnnouncement) - aParams - case false => aParams - } - val authenticator = TestProbe() val watcher = TestProbe() val router = TestProbe() @@ -69,20 +60,35 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { val transport = TestProbe() val wallet: EclairWallet = new TestWallet() val remoteNodeId = Bob.nodeParams.nodeId + + import com.softwaremill.quicklens._ + val aliceParams = TestConstants.Alice.nodeParams + .modify(_.syncWhitelist).setToIf(test.tags.contains("sync-whitelist-bob"))(Set(remoteNodeId)) + .modify(_.syncWhitelist).setToIf(test.tags.contains("sync-whitelist-random"))(Set(randomKey.publicKey)) + + if (test.tags.contains("with_node_announcements")) { + val bobAnnouncement = NodeAnnouncement(randomBytes64, ByteVector.empty, 1, Bob.nodeParams.nodeId, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", fakeIPAddress :: Nil) + aliceParams.db.network.addNode(bobAnnouncement) + } + val peer: TestFSMRef[Peer.State, Peer.Data, Peer] = TestFSMRef(new Peer(aliceParams, remoteNodeId, authenticator.ref, watcher.ref, router.ref, relayer.ref, wallet)) withFixture(test.toNoArgTest(FixtureParam(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer))) } - def connect(remoteNodeId: PublicKey, authenticator: TestProbe, watcher: TestProbe, router: TestProbe, relayer: TestProbe, connection: TestProbe, transport: TestProbe, peer: ActorRef, channels: Set[HasCommitments] = Set.empty): Unit = { + def connect(remoteNodeId: PublicKey, authenticator: TestProbe, watcher: TestProbe, router: TestProbe, relayer: TestProbe, connection: TestProbe, transport: TestProbe, peer: ActorRef, channels: Set[HasCommitments] = Set.empty, remoteInit: wire.Init = wire.Init(Bob.nodeParams.globalFeatures, Bob.nodeParams.localFeatures), expectSync: Boolean = false): Unit = { // let's simulate a connection val probe = TestProbe() probe.send(peer, Peer.Init(None, channels)) authenticator.send(peer, Authenticator.Authenticated(connection.ref, transport.ref, remoteNodeId, fakeIPAddress.socketAddress, outgoing = true, None)) transport.expectMsgType[TransportHandler.Listener] transport.expectMsgType[wire.Init] - transport.send(peer, wire.Init(Bob.nodeParams.globalFeatures, Bob.nodeParams.localFeatures)) + transport.send(peer, remoteInit) transport.expectMsgType[TransportHandler.ReadAck] - router.expectNoMsg(1 second) // bob's features require no sync + if (expectSync) { + router.expectMsgType[SendChannelQuery] + } else { + router.expectNoMsg(1 second) + } probe.send(peer, Peer.GetPeerInfo) assert(probe.expectMsgType[Peer.PeerInfo].state == "CONNECTED") } @@ -255,6 +261,24 @@ class PeerSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(channelCreated.fundingTxFeeratePerKw.get == peer.feeEstimator.getFeeratePerKw(peer.feeTargets.fundingBlockTarget)) } + test("sync if no whitelist is defined") { f => + import f._ + val remoteInit = wire.Init(Bob.nodeParams.globalFeatures, bin"10000000".toByteVector) // bob support channel range queries + connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer, Set.empty, remoteInit, expectSync = true) + } + + test("sync if whitelist contains peer", Tag("sync-whitelist-bob")) { f => + import f._ + val remoteInit = wire.Init(Bob.nodeParams.globalFeatures, bin"10000000".toByteVector) // bob support channel range queries + connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer, Set.empty, remoteInit, expectSync = true) + } + + test("don't sync if whitelist doesn't contain peer", Tag("sync-whitelist-random")) { f => + import f._ + val remoteInit = wire.Init(Bob.nodeParams.globalFeatures, bin"10000000".toByteVector) // bob support channel range queries + connect(remoteNodeId, authenticator, watcher, router, relayer, connection, transport, peer, Set.empty, remoteInit, expectSync = false) + } + test("reply to ping") { f => import f._ val probe = TestProbe()