Skip to content

Commit 2ee1901

Browse files
committed
ETCM-167: Test EIP8 with Mantis discovery messages and fix the new codecs.
1 parent b423bf0 commit 2ee1901

File tree

6 files changed

+132
-42
lines changed

6 files changed

+132
-42
lines changed

src/main/scala/io/iohk/ethereum/network/discovery/codecs/RLPCodecs.scala

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import scodec.bits.{BitVector, ByteVector}
1515
import java.net.InetAddress
1616
import scala.collection.SortedMap
1717
import scala.util.Try
18+
import io.iohk.ethereum.rlp.RLPEncoder
19+
import io.iohk.ethereum.rlp.RLPDecoder
1820

1921
/** RLP codecs based on https://github.com/ethereum/devp2p/blob/master/discv4.md */
2022
object RLPCodecs {
@@ -43,7 +45,17 @@ object RLPCodecs {
4345
deriveLabelledGenericRLPCodec
4446

4547
implicit val nodeRLPCodec: RLPCodec[Node] =
46-
deriveLabelledGenericRLPCodec
48+
RLPCodec.instance[Node](
49+
{ case Node(id, address) =>
50+
RLPEncoder.encode(address).asInstanceOf[RLPList] ++ RLPList(id)
51+
},
52+
{
53+
case list @ RLPList(items @ _*) if items.length >= 4 =>
54+
val address = RLPDecoder.decode[Node.Address](list)
55+
val id = RLPDecoder.decode[PublicKey](items(3))
56+
Node(id, address)
57+
}
58+
)
4759

4860
// https://github.com/ethereum/devp2p/blob/master/enr.md#rlp-encoding
4961
// `record = [signature, seq, k, v, ...]`
@@ -99,26 +111,27 @@ object RLPCodecs {
99111
implicit def payloadCodec: Codec[Payload] =
100112
Codec[Payload](
101113
(payload: Payload) => {
102-
def pt(b: Byte): Byte = b
103-
104-
val data: Array[Byte] = payload match {
105-
case x: Payload.Ping => pt(0x01) +: rlp.encode(x)
106-
case x: Payload.Pong => pt(0x02) +: rlp.encode(x)
107-
case x: Payload.FindNode => pt(0x03) +: rlp.encode(x)
108-
case x: Payload.Neighbors => pt(0x04) +: rlp.encode(x)
109-
case x: Payload.ENRRequest => pt(0x05) +: rlp.encode(x)
110-
case x: Payload.ENRResponse => pt(0x06) +: rlp.encode(x)
111-
}
112-
113-
Attempt.successful(BitVector(data))
114+
val (packetType, packetData) =
115+
payload match {
116+
case x: Payload.Ping => 0x01 -> rlp.encode(x)
117+
case x: Payload.Pong => 0x02 -> rlp.encode(x)
118+
case x: Payload.FindNode => 0x03 -> rlp.encode(x)
119+
case x: Payload.Neighbors => 0x04 -> rlp.encode(x)
120+
case x: Payload.ENRRequest => 0x05 -> rlp.encode(x)
121+
case x: Payload.ENRResponse => 0x06 -> rlp.encode(x)
122+
}
123+
124+
Attempt.successful(BitVector(packetType.toByte +: packetData))
114125
},
115126
(bits: BitVector) => {
116127
bits.consumeThen(8)(
117128
err => Attempt.failure(Err(err)),
118-
(packetType, packetData) => {
129+
(head, tail) => {
130+
val packetType: Byte = head.toByte()
131+
val packetData: Array[Byte] = tail.toByteArray
119132

120133
val tryPayload: Try[Payload] = Try {
121-
packetType.toByte() match {
134+
packetType match {
122135
case 0x01 => rlp.decode[Payload.Ping](packetData)
123136
case 0x02 => rlp.decode[Payload.Pong](packetData)
124137
case 0x03 => rlp.decode[Payload.FindNode](packetData)

src/main/scala/io/iohk/ethereum/rlp/RLP.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ private[rlp] object RLP {
175175
case 3 => ((bytes(0) & 0xff) << 16) + ((bytes(1) & 0xff) << 8) + (bytes(2) & 0xff)
176176
case Integer.BYTES =>
177177
((bytes(0) & 0xff) << 24) + ((bytes(1) & 0xff) << 16) + ((bytes(2) & 0xff) << 8) + (bytes(3) & 0xff)
178-
case _ => throw RLPException("Bytes don't represent an int")
178+
case _ => throw new RLPException("Bytes don't represent an int")
179179
}
180180
}
181181

@@ -197,7 +197,7 @@ private[rlp] object RLP {
197197
val binaryLength: Array[Byte] = intToBytesNoLeadZeroes(length)
198198
(binaryLength.length + offset + SizeThreshold - 1).toByte +: binaryLength
199199
} else if (length < MaxItemLength && length <= 0xff) Array((1 + offset + SizeThreshold - 1).toByte, length.toByte)
200-
else throw RLPException("Input too long")
200+
else throw new RLPException("Input too long")
201201
}
202202

203203
/**
@@ -209,7 +209,7 @@ private[rlp] object RLP {
209209
* @see [[io.iohk.ethereum.rlp.ItemBounds]]
210210
*/
211211
private[rlp] def getItemBounds(data: Array[Byte], pos: Int): ItemBounds = {
212-
if (data.isEmpty) throw RLPException("Empty Data")
212+
if (data.isEmpty) throw new RLPException("Empty Data")
213213
else {
214214
val prefix: Int = data(pos) & 0xff
215215
if (prefix == OffsetShortItem) {
@@ -239,7 +239,7 @@ private[rlp] object RLP {
239239
}
240240

241241
private def decodeWithPos(data: Array[Byte], pos: Int): (RLPEncodeable, Int) =
242-
if (data.isEmpty) throw RLPException("data is too short")
242+
if (data.isEmpty) throw new RLPException("data is too short")
243243
else {
244244
getItemBounds(data, pos) match {
245245
case ItemBounds(start, end, false, isEmpty) =>

src/main/scala/io/iohk/ethereum/rlp/RLPImplicitDerivations.scala

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ object RLPImplicitDerivations {
4444
case list: RLPList =>
4545
decodeList(list.items.toList)._1
4646
case other =>
47-
throw new RuntimeException("Expected to decode an RLPList.")
47+
throw RLPException(s"Expected to decode an RLPList", other)
4848
}
4949
}
5050
object RLPListDecoder {
@@ -155,21 +155,29 @@ object RLPImplicitDerivations {
155155
(head :: tail) -> (hInfo :: tInfos)
156156

157157
case Nil =>
158-
throw new RuntimeException(s"Cannot decode optional '${fieldName}': the RLPList is empty.")
158+
throw RLPException(s"Could not decode optional field '$fieldName': RLPList is empty.")
159159

160160
case rlps =>
161161
val (tail, tInfos) = tDecoder.value.decodeList(rlps.tail)
162162
val value: H =
163163
try {
164164
if (policy.omitTrailingOptionals && tInfos.forall(_.isOptional)) {
165-
// Treat it as a value. We have a decoder for optional fields, so we have to wrap it.
166-
hDecoder.value.decode(RLPList(rlps.head))
165+
// Expect that it's a value. We have a decoder for optional fields, so we have to wrap it into a list.
166+
try {
167+
hDecoder.value.decode(RLPList(rlps.head))
168+
} catch {
169+
case NonFatal(_) =>
170+
// The trailing fields can be followed in the RLP list by additional items
171+
// and random data which we cannot decode.
172+
None
173+
}
167174
} else {
175+
// Expect that it's a list of 0 or 1 items.
168176
hDecoder.value.decode(rlps.head)
169177
}
170178
} catch {
171179
case NonFatal(ex) =>
172-
throw new RuntimeException(s"Cannot decode optional '$fieldName' from RLP value: ${ex.getMessage}")
180+
throw RLPException(s"Could not decode optional field '$fieldName': ${ex.getMessage}", rlps.head)
173181
}
174182

175183
val head: FieldType[K, H] = field[K](value)
@@ -189,15 +197,15 @@ object RLPImplicitDerivations {
189197

190198
RLPListDecoder {
191199
case Nil =>
192-
throw new RuntimeException(s"Cannot decode '${fieldName}': the RLPList is empty.")
200+
throw RLPException(s"Could not decode field '$fieldName': RLPList is empty.")
193201

194202
case rlps =>
195203
val value: H =
196204
try {
197205
hDecoder.value.decode(rlps.head)
198206
} catch {
199207
case NonFatal(ex) =>
200-
throw new RuntimeException(s"Cannot decode '$fieldName' from RLP value: ${ex.getMessage}")
208+
throw RLPException(s"Could not decode field '$fieldName': ${ex.getMessage}", rlps.head)
201209
}
202210
val head: FieldType[K, H] = field[K](value)
203211
val (tail, tInfos) = tDecoder.value.decodeList(rlps.tail)
@@ -217,7 +225,7 @@ object RLPImplicitDerivations {
217225
generic.from(recDecoder.value.decode(rlp))
218226
} catch {
219227
case NonFatal(ex) =>
220-
throw new RuntimeException(s"Could not decode ${ct.runtimeClass.getSimpleName}: ${ex.getMessage}")
228+
throw RLPException(s"Could not decode type ${ct.runtimeClass.getSimpleName}: ${ex.getMessage}", rlp)
221229
}
222230
}
223231

src/main/scala/io/iohk/ethereum/rlp/RLPImplicits.scala

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ object RLPImplicits {
1616

1717
if (len == 0) 0: Byte
1818
else if (len == 1) (bytes(0) & 0xff).toByte
19-
else throw RLPException("src doesn't represent a byte")
19+
else throw RLPException("src doesn't represent a byte", rlp)
2020

21-
case _ => throw RLPException("src is not an RLPValue")
21+
case _ => throw RLPException("src is not an RLPValue", rlp)
2222
}
2323
}
2424

@@ -32,9 +32,9 @@ object RLPImplicits {
3232
if (len == 0) 0: Short
3333
else if (len == 1) (bytes(0) & 0xff).toShort
3434
else if (len == 2) (((bytes(0) & 0xff) << 8) + (bytes(1) & 0xff)).toShort
35-
else throw RLPException("src doesn't represent a short")
35+
else throw RLPException("src doesn't represent a short", rlp)
3636

37-
case _ => throw RLPException("src is not an RLPValue")
37+
case _ => throw RLPException("src is not an RLPValue", rlp)
3838
}
3939
}
4040

@@ -43,7 +43,7 @@ object RLPImplicits {
4343

4444
override def decode(rlp: RLPEncodeable): Int = rlp match {
4545
case RLPValue(bytes) => bigEndianMinLengthToInt(bytes)
46-
case _ => throw RLPException("src is not an RLPValue")
46+
case _ => throw RLPException("src is not an RLPValue", rlp)
4747
}
4848
}
4949

@@ -57,7 +57,7 @@ object RLPImplicits {
5757
override def decode(rlp: RLPEncodeable): BigInt = rlp match {
5858
case RLPValue(bytes) =>
5959
bytes.foldLeft[BigInt](BigInt(0)) { (rec, byte) => (rec << (8: Int)) + BigInt(byte & 0xff) }
60-
case _ => throw RLPException("src is not an RLPValue")
60+
case _ => throw RLPException("src is not an RLPValue", rlp)
6161
}
6262
}
6363

@@ -67,7 +67,8 @@ object RLPImplicits {
6767

6868
override def decode(rlp: RLPEncodeable): Long = rlp match {
6969
case RLPValue(bytes) if bytes.length <= 8 => bigIntEncDec.decode(rlp).toLong
70-
case _ => throw RLPException("src is not an RLPValue")
70+
case RLPValue(bytes) => throw RLPException(s"expected max 8 bytes for Long; got ${bytes.length}", rlp)
71+
case _ => throw RLPException(s"src is not an RLPValue", rlp)
7172
}
7273
}
7374

@@ -76,7 +77,7 @@ object RLPImplicits {
7677

7778
override def decode(rlp: RLPEncodeable): String = rlp match {
7879
case RLPValue(bytes) => new String(bytes)
79-
case _ => throw RLPException("src is not an RLPValue")
80+
case _ => throw RLPException("src is not an RLPValue", rlp)
8081
}
8182
}
8283

@@ -86,7 +87,7 @@ object RLPImplicits {
8687

8788
override def decode(rlp: RLPEncodeable): Array[Byte] = rlp match {
8889
case RLPValue(bytes) => bytes
89-
case _ => throw RLPException("src is not an RLPValue")
90+
case _ => throw RLPException("src is not an RLPValue", rlp)
9091
}
9192
}
9293

@@ -105,7 +106,7 @@ object RLPImplicits {
105106

106107
override def decode(rlp: RLPEncodeable): Seq[T] = rlp match {
107108
case l: RLPList => l.items.map(dec.decode)
108-
case _ => throw RLPException("src is not a Seq")
109+
case _ => throw RLPException("src is not a Seq", rlp)
109110
}
110111
}
111112

@@ -120,7 +121,7 @@ object RLPImplicits {
120121
implicit def optionDec[T](implicit dec: RLPDecoder[T]): RLPDecoder[Option[T]] = {
121122
case RLPList(value) => Some(dec.decode(value))
122123
case RLPList() => None
123-
case rlp => throw RLPException(s"${rlp} should be a list with 1 or 0 elements")
124+
case rlp => throw RLPException(s"${rlp} should be a list with 1 or 0 elements", rlp)
124125
}
125126

126127
implicit val booleanEncDec = new RLPEncoder[Boolean] with RLPDecoder[Boolean] {
@@ -134,8 +135,60 @@ object RLPImplicits {
134135

135136
if (intRepresentation == 1) true
136137
else if (intRepresentation == 0) false
137-
else throw RLPException(s"$rlp should be 1 or 0")
138+
else throw RLPException(s"$rlp should be 1 or 0", rlp)
138139
}
139140
}
140141

142+
implicit def tuple2Codec[A: RLPCodec, B: RLPCodec]: RLPCodec[(A, B)] =
143+
RLPCodec.instance[(A, B)](
144+
{ case (a, b) =>
145+
RLPList(RLPEncoder.encode(a), RLPEncoder.encode(b))
146+
},
147+
{ case RLPList(a, b, _*) =>
148+
(RLPDecoder.decode[A](a), RLPDecoder.decode[B](b))
149+
}
150+
)
151+
152+
implicit def tuple3Codec[A: RLPCodec, B: RLPCodec, C: RLPCodec]: RLPCodec[(A, B, C)] =
153+
RLPCodec.instance[(A, B, C)](
154+
{ case (a, b, c) =>
155+
RLPList(RLPEncoder.encode(a), RLPEncoder.encode(b), RLPEncoder.encode(c))
156+
},
157+
{ case RLPList(a, b, c, _*) =>
158+
(RLPDecoder.decode[A](a), RLPDecoder.decode[B](b), RLPDecoder.decode[C](c))
159+
}
160+
)
161+
162+
implicit def tuple4Codec[A: RLPCodec, B: RLPCodec, C: RLPCodec, D: RLPCodec]: RLPCodec[(A, B, C, D)] =
163+
RLPCodec.instance[(A, B, C, D)](
164+
{ case (a, b, c, d) =>
165+
RLPList(RLPEncoder.encode(a), RLPEncoder.encode(b), RLPEncoder.encode(c), RLPEncoder.encode(d))
166+
},
167+
{ case RLPList(a, b, c, d, _*) =>
168+
(RLPDecoder.decode[A](a), RLPDecoder.decode[B](b), RLPDecoder.decode[C](c), RLPDecoder.decode[D](d))
169+
}
170+
)
171+
172+
implicit def tuple5Codec[A: RLPCodec, B: RLPCodec, C: RLPCodec, D: RLPCodec, E: RLPCodec]: RLPCodec[(A, B, C, D, E)] =
173+
RLPCodec.instance[(A, B, C, D, E)](
174+
{ case (a, b, c, d, e) =>
175+
RLPList(
176+
RLPEncoder.encode(a),
177+
RLPEncoder.encode(b),
178+
RLPEncoder.encode(c),
179+
RLPEncoder.encode(d),
180+
RLPEncoder.encode(e)
181+
)
182+
},
183+
{ case RLPList(a, b, c, d, e, _*) =>
184+
(
185+
RLPDecoder.decode[A](a),
186+
RLPDecoder.decode[B](b),
187+
RLPDecoder.decode[C](c),
188+
RLPDecoder.decode[D](d),
189+
RLPDecoder.decode[E](e)
190+
)
191+
}
192+
)
193+
141194
}

src/main/scala/io/iohk/ethereum/rlp/package.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import scala.reflect.ClassTag
66

77
package object rlp {
88

9-
case class RLPException(message: String) extends RuntimeException(message)
9+
case class RLPException(message: String, encodeable: Option[RLPEncodeable] = None) extends RuntimeException(message)
10+
object RLPException {
11+
def apply(message: String, encodeable: RLPEncodeable): RLPException =
12+
RLPException(message, Some(encodeable))
13+
}
1014

1115
sealed trait RLPEncodeable
1216

@@ -92,7 +96,7 @@ package object rlp {
9296

9397
override def decode(rlp: RLPEncodeable): T =
9498
if (dec.isDefinedAt(rlp)) dec(rlp)
95-
else throw new RuntimeException(s"Cannot decode ${ct.runtimeClass.getSimpleName} from RLP.")
99+
else throw RLPException(s"Cannot decode type ${ct.runtimeClass.getSimpleName} from RLP.", rlp)
96100
}
97101

98102
def apply[T](enc: RLPEncoder[T], dec: RLPDecoder[T]): RLPCodec[T] =

src/test/scala/io/iohk/ethereum/network/discovery/codecs/RLPCodecsSpec.scala

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,20 @@ class RLPCodecsSpec extends AnyFlatSpec {
107107
)
108108
)
109109

110+
// Test the original Mantis types to make sure we're on the right track.
111+
EIP8TestVectors.foreach { case EIP8TestVector(description, data, _) =>
112+
it should s"decode a ${description} into an original Mantis type" in {
113+
import io.iohk.ethereum.network.discovery.Packet
114+
import akka.util.ByteString
115+
val bits = BitVector.fromHex(data).get
116+
val bytes = ByteString(bits.toByteArray)
117+
val packet = Packet(bytes)
118+
val message = io.iohk.ethereum.network.discovery.extractMessage(packet).get
119+
}
120+
}
121+
110122
// Test the RLP decoders in isolation, without crypto.
111-
EIP8TestVectors.foreach { case EIP8TestVector(description, data, test) =>
123+
EIP8TestVectors.take(5).foreach { case EIP8TestVector(description, data, test) =>
112124
it should s"decode/encode a ${description}" in {
113125
val bits = BitVector.fromHex(data).get
114126
val packet = Codec[Packet].decodeValue(bits).require

0 commit comments

Comments
 (0)