[ETCM-354] Peer fork ID validation#1024
Conversation
423bc93 to
10cdc43
Compare
476bc61 to
7202b88
Compare
jvdp
left a comment
There was a problem hiding this comment.
Looks OK to my eyeballs but the code and tests could be simpler. And it will be more interesting once this is hooked into handshaking and running on a test net.
| val (unpassedFork, i) = (forks :+ maxUInt64).zipWithIndex.find { case (fork, _) => currentHeight < fork }.get | ||
|
|
||
| // The checks are left biased -> whenever a result is found we need to short circuit | ||
| val validate: F[Option[ForkIdValidationResult]] = (for { |
There was a problem hiding this comment.
This method looks a lot more complicated than it is. How about moving the tracing into the check methods, for a start? (Do we still need the tracing btw?)
There was a problem hiding this comment.
This method looks a lot more complicated than it is.
This is vague, can you elaborate on that?
How about moving the tracing into the check methods, for a start?
It could be done, but I can't see how moving logging instructions around is going to make the method less complicated.
(Do we still need the tracing btw?)
Logging is part of our DoD and it might get useful when testing on testnet, so I wouldn't remove it.
| res <- fromEither[F](Either.left[ForkIdValidationResult, Unit](ErrLocalIncompatibleOrStale)) | ||
| } yield (res)).value | ||
| .map(_.swap) | ||
| .flatMap(res => Logger[F].debug(s"Validation result is: $res") >> Monad[F].pure(res.toOption)) |
There was a problem hiding this comment.
Could also use a for-comprehension for this part? (And bind the existing one to a name.)
|
|
||
| val maxUInt64 = (BigInt(0x7fffffffffffffffL) << 1) + 1 // scalastyle:ignore magic.number | ||
|
|
||
| def validatePeer[F[_]: Monad: Logger]( |
There was a problem hiding this comment.
Could do with a scaladoc since it's 'external' API, right?
There was a problem hiding this comment.
Good catch, added
There was a problem hiding this comment.
Do we really need the monad ?
This is a synchronous function, so there is no need for an async abstraction. I get that logging is a kind of side effect, but from the caller point of view it does not really matter, and it adds some noise because everything needs to be inside a liftF now.
There was a problem hiding this comment.
Monad has nothing to do with asynchronicity, it allows chaining effectful computations.
Logging is a side effect. Are you saying we should ignore that fact?
| genesisChecksum +: (forks.map { fork => | ||
| crc.update(bigIntToBytes(fork, 8)) | ||
| BigInt(crc.getValue()) | ||
| }).toVector |
There was a problem hiding this comment.
This code perhaps looks functional but it's really relying on the mutable CRC instance. It's probably more performant this way, but it could be more descriptive using List.unfold or similar. (Eg. first produce a collection of collections to produce the CRC with, then flatten it.)
(But this is just a suggestion ofc.)
There was a problem hiding this comment.
This code isn't functional because of a side effect in map. It is because of the nature of Java's CRC as you correctly noted. This however doesn't break the referential transparency of calculateChecksum, so the function itself remains pure.
| Some(Connect) | ||
| } | ||
| } else { None } | ||
| } |
There was a problem hiding this comment.
Could be nicer as a match on remoteId:
remoteId match {
case ForkId(hash, _) if checksums(i) != hash => None
case ForkId(_, Some(next)) if currentHeight >= next => Some(ErrLocalIncompatibleOrStale)
case _ => Some(Connect)
}(Gets rid of the nested if and the Option.get.)
| "ForkIdValidator" must { | ||
| "correctly validate ETH peers" in { | ||
| // latest fork at the time of writing those assertions (in the spec) was Petersburg | ||
| val ethForksList: List[BigInt] = List(1150000, 1920000, 2463000, 2675000, 4370000, 7280000) |
There was a problem hiding this comment.
Are all these different forks interesting for the purposes of testing? I mean, you are testing it with 6, would it make a material difference to the test quality if it was 5 or 7 or 9000?
Also, would this be a good opportunity for property based testing perhaps?
There was a problem hiding this comment.
now I'm curious - how does one property test an equation? by writing the same equation in a different way?
There was a problem hiding this comment.
Yes, but part of this 'equation' would be in how the arbitrary data is constructed. I agree that just re-implementing the same logic wouldn't make much sense.
There was a problem hiding this comment.
oh this isn't arbitrary data - it's hard fork heights.
There was a problem hiding this comment.
This test was provided as part of the specification in https://eips.ethereum.org/EIPS/eip-2124 so we would have to have a really good reason to modify or not include it here.
Whether all the forks are interesting for the purposes of testing is probably a question to the original EIP's authors.
@jvdp what are the properties you would like to test here?
|
|
||
| def validatePeer[F[_]: Monad: Logger]( | ||
| genesisHash: ByteString, | ||
| forks: List[BigInt] |
There was a problem hiding this comment.
Should forks be validated to not be empty?
There was a problem hiding this comment.
I don't think this is necessary
a32e329 to
1050ee8
Compare
1050ee8 to
3a10074
Compare
| "org.codehaus.janino" % "janino" % "3.1.2" | ||
| "org.codehaus.janino" % "janino" % "3.1.2", | ||
| "org.typelevel" %% "log4cats-core" % "2.1.1", | ||
| "org.typelevel" %% "log4cats-slf4j" % "1.3.1" |
There was a problem hiding this comment.
do we really need to add 3 deps for one class ?
There was a problem hiding this comment.
sorry, it's only 2 my bad
There was a problem hiding this comment.
Do we have a limit for that? :-)
| _ <- liftF(Logger[F].trace(s"Before checkSuperset")) | ||
| sup <- fromEither[F](checkSuperset(checksums, remoteId, i).toLeft("not in superset")) | ||
| _ <- liftF(Logger[F].trace(s"checkSuperset result: $sup")) | ||
| _ <- liftF(Logger[F].trace(s"No check succeeded")) |
There was a problem hiding this comment.
I think we could do with just one log between each step right ?
| checksums: Vector[BigInt], | ||
| remoteId: ForkId, | ||
| currentHeight: BigInt, | ||
| i: Int |
There was a problem hiding this comment.
we could directly pass the current fork block hash instead of i and a list. It's not really obvious that the index of the next fork is actually also the index of the current checksums.
| forks.zipWithIndex.find { case (fork, _) => currentHeight < fork }.getOrElse((maxUInt64, forks.length)) | ||
|
|
||
| // The checks are left biased -> whenever a result is found we need to short circuit | ||
| val validate = (for { |
There was a problem hiding this comment.
can't we find the hash in checksums which match remote hash first ? So that we directly know if we should do the matching/subset/superset check directly ? I feel that it would simplify the function.
There was a problem hiding this comment.
I don't think I can see what you mean. Could you paste a code snippet with the suggested simplification?
| val checksums: Vector[BigInt] = calculateCheckchecksums(genesisHash, forks) | ||
|
|
||
| // find the first unpassed fork and it's index | ||
| val (unpassedFork, i) = |
There was a problem hiding this comment.
i -> nextForkIndex or unpassedForkIndex ?
| } yield (res.getOrElse(Connect)) | ||
| } | ||
|
|
||
| private def calculateCheckchecksums( |
There was a problem hiding this comment.
typo calculateChecksums
|
|
||
| val maxUInt64 = (BigInt(0x7fffffffffffffffL) << 1) + 1 // scalastyle:ignore magic.number | ||
|
|
||
| def validatePeer[F[_]: Monad: Logger]( |
There was a problem hiding this comment.
Do we really need the monad ?
This is a synchronous function, so there is no need for an async abstraction. I get that logging is a kind of side effect, but from the caller point of view it does not really matter, and it adds some noise because everything needs to be inside a liftF now.
dzajkowski
left a comment
There was a problem hiding this comment.
I created a set of topics in slack to continue the discussion but from my pov it LGTM
Description
Adds peer fork id validation as described in the section "validation rules" of https://eips.ethereum.org/EIPS/eip-2124