Skip to content

Commit dc8142a

Browse files
authored
Merge pull request #108 from input-output-hk/ETCM-414-not-bonding-with-incoming-until-enrolled
ETCM-414: Incoming pings don't trigger bonds until enroll is finished.
2 parents e9c252e + 7ef7f96 commit dc8142a

File tree

5 files changed

+51
-7
lines changed

5 files changed

+51
-7
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ mill __.test
8282

8383
A single test suite can be executed with the `single` helper command, for example:
8484
```bash
85-
mill scalanet.test.single io.iohk.scalanet.crypto.SignatureVerificationSpec
85+
mill scalanet.ut.single io.iohk.scalanet.crypto.SignatureVerificationSpec
8686
```
8787

8888
### Publishing

build.sc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ trait ScalanetModule extends ScalaModule {
6868
trait ScalanetPublishModule extends PublishModule {
6969
def description: String
7070

71-
override def publishVersion = "0.4.3-SNAPSHOT"
71+
override def publishVersion = "0.4.4-SNAPSHOT"
7272

7373
override def pomSettings = PomSettings(
7474
description = description,

scalanet/discovery/it/src/io/iohk/scalanet/kademlia/KademliaIntegrationSpec.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,16 @@ abstract class KademliaIntegrationSpec(name: String)
188188
val lowRefConfig = defaultConfig.copy(refreshRate = 1.seconds)
189189
val randomNode = generatePeerRecordWithKey
190190
(for {
191+
// Start a node which would bootstrap from a peer that's not running yet.
191192
node1 <- startNode(initialNodes = Set(randomNode._1), testConfig = lowRefConfig)
193+
// Start a standalone node.
192194
node2 <- startNode(initialNodes = Set(), testConfig = lowRefConfig)
195+
// A chain of nodes bootstrapping from each other
193196
node3 <- startNode(initialNodes = Set(node2.self), testConfig = lowRefConfig)
194197
node4 <- startNode(initialNodes = Set(node3.self), testConfig = lowRefConfig)
195198
_ <- Resource.liftF(Task.sleep(10.seconds))
199+
// Now start a node that node1 wanted to bootstrap from, and join all the other nodes.
200+
// When node1 refreshes it will try to connect to node5 again and discover the others.
196201
node5 <- startNode(
197202
selfRecordWithKey = randomNode,
198203
initialNodes = Set(node2.self, node3.self, node4.self),

scalanet/discovery/src/io/iohk/scalanet/discovery/ethereum/v4/DiscoveryService.scala

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ object DiscoveryService {
9595
// Start handling requests, we need them during enrolling so the peers can ping and bond with us.
9696
cancelToken <- network.startHandling(service)
9797
// Contact the bootstrap nodes.
98-
enroll = service.enroll()
98+
// Setting the enrolled status here because we could potentially repeat enrollment until it succeeds.
99+
enroll = service.enroll().guarantee(stateRef.update(_.setEnrolled))
99100
// Periodically discover new nodes.
100101
discover = service.lookupRandom.delayExecution(config.discoveryPeriod).loopForever
101102
// Enrollment can be run in the background if it takes very long.
@@ -152,7 +153,9 @@ object DiscoveryService {
152153
// Deferred results so we can ensure there's only one concurrent Ping to a given peer.
153154
bondingResultsMap: Map[Peer[A], BondingResults],
154155
// Deferred ENR fetches so we only do one at a time to a given peer.
155-
fetchEnrMap: Map[Peer[A], FetchEnrResult]
156+
fetchEnrMap: Map[Peer[A], FetchEnrResult],
157+
// Indicate whether enrollment hash finished.
158+
hasEnrolled: Boolean
156159
) {
157160
def isSelf(peer: Peer[A]): Boolean =
158161
peer.id == node.id
@@ -191,6 +194,9 @@ object DiscoveryService {
191194
def clearBondingResults(peer: Peer[A]): State[A] =
192195
copy(bondingResultsMap = bondingResultsMap - peer)
193196

197+
def clearLastPongTimestamp(peer: Peer[A]): State[A] =
198+
copy(lastPongTimestampMap = lastPongTimestampMap - peer)
199+
194200
def withEnrFetch(peer: Peer[A], result: FetchEnrResult): State[A] =
195201
copy(fetchEnrMap = fetchEnrMap.updated(peer, result))
196202

@@ -230,6 +236,9 @@ object DiscoveryService {
230236
kBuckets = kBuckets.remove(Node.kademliaId(peerId)),
231237
kademliaIdToNodeId = kademliaIdToNodeId - Node.kademliaId(peerId)
232238
)
239+
240+
def setEnrolled: State[A] =
241+
copy(hasEnrolled = true)
233242
}
234243
protected[v4] object State {
235244
def apply[A](
@@ -245,7 +254,8 @@ object DiscoveryService {
245254
enrMap = Map(node.id -> enr),
246255
lastPongTimestampMap = Map.empty[Peer[A], Timestamp],
247256
bondingResultsMap = Map.empty[Peer[A], BondingResults],
248-
fetchEnrMap = Map.empty[Peer[A], FetchEnrResult]
257+
fetchEnrMap = Map.empty[Peer[A], FetchEnrResult],
258+
hasEnrolled = false
249259
)
250260
}
251261

@@ -328,6 +338,10 @@ object DiscoveryService {
328338
for {
329339
// Complete any deferred waiting for a ping from this peer, if we initiated the bonding.
330340
_ <- completePing(caller)
341+
// To protect against an eclipse attack filling up the k-table after a reboot,
342+
// only try to bond with an incoming Ping's peer after the initial enrollment
343+
// hash finished.
344+
hasEnrolled <- stateRef.get.map(_.hasEnrolled)
331345
_ <- isBonded(caller)
332346
.ifM(
333347
// We may already be bonded but the remote node could have changed its address.
@@ -338,6 +352,7 @@ object DiscoveryService {
338352
bond(caller)
339353
)
340354
.startAndForget
355+
.whenA(hasEnrolled)
341356
// Return the latet local ENR sequence.
342357
enrSeq <- localEnrSeq
343358
} yield Some(Some(enrSeq))
@@ -745,7 +760,11 @@ object DiscoveryService {
745760
.findNode(peer)(target)
746761
.flatMap {
747762
case None =>
748-
Task(logger.debug(s"Received no response for neighbors for $target from ${peer.address}")).as(Nil)
763+
for {
764+
_ <- Task(logger.debug(s"Received no response for neighbors for $target from ${peer.address}"))
765+
// The other node has possibly unbonded from us, or it was still enrolling when we bonded. Try bonding next time.
766+
_ <- stateRef.update(_.clearLastPongTimestamp(peer))
767+
} yield Nil
749768
case Some(neighbors) =>
750769
Task(logger.debug(s"Received ${neighbors.size} neighbors for $target from ${peer.address}"))
751770
.as(neighbors.toList)

scalanet/discovery/ut/src/io/iohk/scalanet/discovery/ethereum/v4/DiscoveryServiceSpec.scala

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,14 +512,34 @@ class DiscoveryServiceSpec extends AsyncFlatSpec with Matchers {
512512

513513
behavior of "ping"
514514

515-
it should "respond with the ENR sequence and bond with the caller" in test {
515+
it should "respond with the ENR sequence but not bond with the caller before enrollment" in test {
516+
new Fixture {
517+
override val test = for {
518+
hasEnrolled <- stateRef.get.map(_.hasEnrolled)
519+
maybeEnrSeq <- service.ping(remotePeer)(None)
520+
// Shouldn't start bonding, but in case it does, wait until it finishes.
521+
_ <- stateRef.get.flatMap(_.bondingResultsMap.get(remotePeer).fold(Task.unit)(_.pongReceived.get.void))
522+
state <- stateRef.get
523+
} yield {
524+
hasEnrolled shouldBe false
525+
maybeEnrSeq shouldBe Some(Some(localENR.content.seq))
526+
state.nodeMap should not contain key(remotePeer.id)
527+
state.enrMap should not contain key(remotePeer.id)
528+
state.lastPongTimestampMap should not contain key(remotePeer)
529+
}
530+
}
531+
}
532+
533+
it should "respond with the ENR sequence and bond with the caller after enrollment" in test {
516534
new Fixture {
517535
override lazy val rpc = unimplementedRPC.copy(
518536
ping = _ => _ => Task(Some(None)),
519537
enrRequest = _ => _ => Task(Some(remoteENR))
520538
)
521539
override val test = for {
540+
_ <- stateRef.update(_.setEnrolled)
522541
maybeEnrSeq <- service.ping(remotePeer)(None)
542+
// Wait for any ongoing bonding and ENR fetching to finish.
523543
_ <- stateRef.get.flatMap(_.bondingResultsMap.get(remotePeer).fold(Task.unit)(_.pongReceived.get.void))
524544
_ <- stateRef.get.flatMap(_.fetchEnrMap.get(remotePeer).fold(Task.unit)(_.get.void))
525545
state <- stateRef.get

0 commit comments

Comments
 (0)