Skip to content

Commit ffdd6db

Browse files
committed
ETCM-167: Testing the ENR codecs.
1 parent 26af5a4 commit ffdd6db

File tree

1 file changed

+124
-0
lines changed

1 file changed

+124
-0
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package io.iohk.ethereum.network.discovery.codecs
2+
3+
import org.scalatest.matchers.should.Matchers
4+
import org.scalatest.flatspec.AnyFlatSpec
5+
import io.iohk.scalanet.discovery.ethereum.{Node, EthereumNodeRecord}
6+
import io.iohk.scalanet.discovery.crypto.{SigAlg, PrivateKey, PublicKey}
7+
import io.iohk.ethereum.network.discovery.crypto.Secp256k1SigAlg
8+
import io.iohk.ethereum.rlp
9+
import io.iohk.ethereum.rlp.{RLPList, RLPEncoder}
10+
import io.iohk.ethereum.rlp.RLPImplicits._
11+
import io.iohk.ethereum.rlp.RLPImplicitConversions._
12+
import scodec.bits.{ByteVector, HexStringSyntax}
13+
import java.net.InetAddress
14+
import io.iohk.ethereum.rlp.RLPEncodeable
15+
import io.iohk.ethereum.rlp.RLPDecoder
16+
17+
class ENRCodecsSpec extends AnyFlatSpec with Matchers {
18+
19+
import RLPCodecs._
20+
21+
implicit val sigalg: SigAlg = Secp256k1SigAlg
22+
23+
val localhost = InetAddress.getByName("127.0.0.1")
24+
25+
behavior of "RLPCodecs with ENR"
26+
27+
// https://github.com/ethereum/devp2p/blob/master/enr.md#test-vectors
28+
val nodeId = hex"a448f24c6d18e575453db13171562b71999873db5b286df957af199ec94617f7"
29+
val privateKey = hex"b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291"
30+
val enrString =
31+
"enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"
32+
33+
val enrRLP = RLPList(
34+
hex"7098ad865b00a582051940cb9cf36836572411a47278783077011599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11df72ecf1145ccb9c",
35+
1L,
36+
"id",
37+
"v4",
38+
"ip",
39+
hex"7f000001",
40+
"secp256k1",
41+
hex"03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138",
42+
"udp",
43+
hex"765f"
44+
)
45+
46+
it should "encode the test ENR RLP to the expected bytes" in {
47+
// https://github.com/ethereum/devp2p/blob/master/enr.md#text-encoding
48+
val enrBytes = ByteVector.fromBase64(enrString.stripPrefix("enr:"), scodec.bits.Bases.Alphabets.Base64Url).get
49+
// Check that the RLP really serializes to that string representation.
50+
ByteVector(rlp.encode(enrRLP)) shouldBe enrBytes
51+
}
52+
53+
it should "encode a Node to the expected RLP structure" in {
54+
// Construct a Node which should give us the same results.
55+
// Unfortunately there's no option to omit the TCP port from it :(
56+
val node = Node(
57+
id = PublicKey(nodeId.toBitVector),
58+
address = Node.Address(localhost, udpPort = 30303, tcpPort = 0)
59+
)
60+
61+
val enr = EthereumNodeRecord.fromNode(node, PrivateKey(privateKey.toBitVector), seq = 1).require
62+
63+
val rlpStructureFromNode = RLPEncoder.encode(enr)
64+
65+
rlpStructureFromNode match {
66+
case list: RLPList =>
67+
def compare(a: Iterable[RLPEncodeable], b: Iterable[RLPEncodeable]) = {
68+
// .toString hack used because RLPValue has mutable arrays in it where equality doesn't work.
69+
val encoded = a.map(_.toString).toList
70+
val expected = b.map(_.toString).toList
71+
encoded should contain theSameElementsInOrderAs expected
72+
}
73+
74+
// Ignoring the signature, taking items up to where "tcp" would be.
75+
compare(list.items.drop(1).take(7), enrRLP.items.drop(1).take(7))
76+
77+
// TODO: The example is encoded differently because it uses the minimum
78+
// length for the port, whereas the one in Scalanet just converts the
79+
// Int to BigEndian bytes and includes them in the attributes.
80+
//
81+
// This should be fine from signature checking perspective as
82+
// that is still carried out on the byte content of the attribute map,
83+
// and nodes should be able to deserialize what we send.
84+
//
85+
// We might have problems if someone sends 0 for port as that
86+
// would serialize to an empty byte array in RLP. But we can't
87+
// address such nodes anyway, so it should be enough to check
88+
// that we can deal with the shorter bytes (done below in a test).
89+
compare(
90+
list.items.drop(8),
91+
RLPList(
92+
"tcp",
93+
hex"00000000",
94+
"udp",
95+
hex"0000765f"
96+
).items
97+
)
98+
99+
case other =>
100+
fail(s"Unexpected RLP structure $other")
101+
}
102+
}
103+
104+
it should "decode the address from the example ENR" in {
105+
// We need the TCP port, so let's inject one.
106+
val enrRLPWithTCP = enrRLP ++ RLPList("tcp", hex"7660")
107+
val enr = RLPDecoder.decode[EthereumNodeRecord](enrRLPWithTCP)
108+
109+
Node.Address.fromEnr(enr) match {
110+
case Some(address) =>
111+
address.ip shouldBe localhost
112+
address.udpPort shouldBe 30303
113+
address.tcpPort shouldBe 30304
114+
case None =>
115+
fail("Couldn't extract the node address")
116+
}
117+
}
118+
119+
it should "verify the signature of the example ENR" in {
120+
val publicKey = sigalg.toPublicKey(PrivateKey(privateKey.toBitVector))
121+
val enr = RLPDecoder.decode[EthereumNodeRecord](enrRLP)
122+
EthereumNodeRecord.validateSignature(enr, publicKey).require shouldBe true
123+
}
124+
}

0 commit comments

Comments
 (0)