You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tuweni.apache.org by to...@apache.org on 2019/04/30 06:14:17 UTC

[incubator-tuweni] branch master updated: Add ENR - see https://eips.ethereum.org/EIPS/eip-778

This is an automated email from the ASF dual-hosted git repository.

toulmean pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-tuweni.git


The following commit(s) were added to refs/heads/master by this push:
     new 680cf41  Add ENR - see https://eips.ethereum.org/EIPS/eip-778
     new 2cf77ba  Merge pull request #8 from atoulme/enr
680cf41 is described below

commit 680cf4160695aa0957e2888ad1f6b79f6d785c3a
Author: Antoine Toulme <to...@apache.org>
AuthorDate: Mon Apr 29 23:06:26 2019 -0700

    Add ENR - see https://eips.ethereum.org/EIPS/eip-778
---
 devp2p/build.gradle                                |   2 +
 .../org/apache/tuweni/devp2p/EthereumNodeRecord.kt | 162 +++++++++++++++++++++
 .../apache/tuweni/devp2p/EthereumNodeRecordTest.kt |  73 ++++++++++
 3 files changed, 237 insertions(+)

diff --git a/devp2p/build.gradle b/devp2p/build.gradle
index ef0022a..8f90068 100644
--- a/devp2p/build.gradle
+++ b/devp2p/build.gradle
@@ -12,6 +12,8 @@ dependencies {
   compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core'
   compile 'org.logl:logl-api'
 
+  compileOnly 'org.bouncycastle:bcprov-jdk15on'
+
   testCompile project(':junit')
   testCompile 'org.bouncycastle:bcprov-jdk15on'
   testCompile 'org.junit.jupiter:junit-jupiter-api'
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/EthereumNodeRecord.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/EthereumNodeRecord.kt
new file mode 100644
index 0000000..b40a9e0
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/EthereumNodeRecord.kt
@@ -0,0 +1,162 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tuweni.devp2p
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.bytes.MutableBytes
+import org.apache.tuweni.crypto.Hash
+import org.apache.tuweni.crypto.SECP256K1
+import org.apache.tuweni.rlp.RLP
+import org.apache.tuweni.rlp.RLPWriter
+import org.apache.tuweni.units.bigints.UInt256
+import java.lang.IllegalArgumentException
+import java.lang.RuntimeException
+import java.net.InetAddress
+import java.time.Instant
+
+/**
+ * Ethereum Node Record (ENR) as described in [EIP-778](https://eips.ethereum.org/EIPS/eip-778).
+ */
+class EthereumNodeRecord(val signature: Bytes, val seq: Long, val data: Map<String, Bytes>) {
+
+  companion object {
+
+    /**
+     * Creates an ENR from its serialized form as a RLP list
+     * @param rlp the serialized form of the ENR
+     * @return the ENR
+     * @throws IllegalArgumentException if the rlp bytes length is longer than 300 bytes
+     */
+    @JvmStatic
+    fun fromRLP(rlp: Bytes): EthereumNodeRecord {
+      if (rlp.size() > 300) {
+        throw IllegalArgumentException("Record too long")
+      }
+      return RLP.decodeList(rlp, {
+        val sig = it.readValue()
+
+        val seq = it.readLong()
+
+        val data = mutableMapOf<String, Bytes>()
+        while (!it.isComplete) {
+          val key = it.readString()
+          val value = it.readValue()
+          data[key] = value
+        }
+
+        EthereumNodeRecord(sig, seq, data)
+      })
+    }
+
+    private fun encode(
+      signatureKeyPair: SECP256K1.KeyPair? = null,
+      seq: Long = Instant.now().toEpochMilli(),
+      ip: InetAddress? = null,
+      tcp: Int? = null,
+      udp: Int? = null,
+      data: Map<String, Bytes>? = null,
+      writer: RLPWriter
+    ) {
+      writer.writeLong(seq)
+      val mutableData = data?.toMutableMap() ?: mutableMapOf()
+      mutableData["id"] = Bytes.wrap("v4".toByteArray())
+      signatureKeyPair?.let {
+        mutableData["secp256k1"] = Bytes.wrap(it.publicKey().asEcPoint().getEncoded(true))
+      }
+      ip?.let {
+        mutableData["ip"] = Bytes.wrap(it.address)
+      }
+      tcp?.let {
+        mutableData["tcp"] = Bytes.ofUnsignedShort(it)
+      }
+      udp?.let {
+        mutableData["udp"] = Bytes.ofUnsignedShort(it)
+        writer.writeString("udp")
+      }
+      mutableData.keys.sorted().forEach { key ->
+          mutableData[key]?.let { value ->
+            writer.writeString(key)
+            writer.writeValue(value)
+          }
+      }
+    }
+
+    /**
+     * Creates the serialized form of a ENR
+     * @param signatureKeyPair the key pair to use to sign the ENR
+     * @param seq the sequence number for the ENR. It should be higher than the previous time the ENR was generated. It defaults to the current time since epoch in milliseconds.
+     * @param data the key pairs to encode in the ENR
+     * @param ip the IP address of the host
+     * @param tcp an optional parameter to a TCP port used for the wire protocol
+     * @param udp an optional parameter to a UDP port used for discovery
+     * @return the serialized form of the ENR as a RLP-encoded list
+     */
+    @JvmOverloads
+    @JvmStatic
+    fun toRLP(
+      signatureKeyPair: SECP256K1.KeyPair,
+      seq: Long = Instant.now().toEpochMilli(),
+      data: Map<String, Bytes>? = null,
+      ip: InetAddress,
+      tcp: Int? = null,
+      udp: Int? = null
+    ): Bytes {
+      val encoded = RLP.encode { writer ->
+        encode(signatureKeyPair, seq, ip, tcp, udp, data, writer)
+      }
+      val signature = SECP256K1.sign(Hash.keccak256(encoded), signatureKeyPair)
+      val sigBytes = MutableBytes.create(64)
+      UInt256.valueOf(signature.r()).toBytes().copyTo(sigBytes, 0)
+      UInt256.valueOf(signature.s()).toBytes().copyTo(sigBytes, 32)
+
+      val completeEncoding = RLP.encodeList { writer ->
+        writer.writeValue(sigBytes)
+        encode(signatureKeyPair, seq, ip, tcp, udp, data, writer)
+      }
+      return completeEncoding
+    }
+  }
+
+  fun validate() {
+    if (Bytes.wrap("v4".toByteArray()) != data["id"]) {
+      throw InvalidNodeRecordException("id attribute is not set to v4")
+    }
+
+    val encoded = RLP.encodeList {
+      encode(data = data, seq = seq, writer = it)
+    }
+
+    val sig = SECP256K1.Signature.create(1, signature.slice(0, 32).toUnsignedBigInteger(),
+      signature.slice(32).toUnsignedBigInteger())
+
+    val pubKey = publicKey()
+
+    val recovered = SECP256K1.PublicKey.recoverFromSignature(encoded, sig)
+
+    if (pubKey != recovered) {
+      throw InvalidNodeRecordException("Public key does not match signature")
+    }
+  }
+
+  fun publicKey(): SECP256K1.PublicKey {
+    val keyBytes = data["secp256k1"] ?: throw InvalidNodeRecordException("Missing secp256k1 entry")
+    val ecPoint = SECP256K1.Parameters.CURVE.getCurve().decodePoint(keyBytes.toArrayUnsafe())
+    return SECP256K1.PublicKey.fromBytes(Bytes.wrap(ecPoint.getEncoded(false)).slice(1))
+  }
+}
+
+internal class InvalidNodeRecordException(message: String?) : RuntimeException(message)
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/EthereumNodeRecordTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/EthereumNodeRecordTest.kt
new file mode 100644
index 0000000..eeb1552
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/EthereumNodeRecordTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tuweni.devp2p
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.crypto.SECP256K1
+import org.apache.tuweni.junit.BouncyCastleExtension
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.junit.jupiter.api.extension.ExtendWith
+import java.lang.IllegalArgumentException
+import java.net.InetAddress
+
+@ExtendWith(BouncyCastleExtension::class)
+class EthereumNodeRecordTest {
+
+  @Test
+  fun tooLong() {
+    val tooLong = Bytes.random(312)
+    val exception: IllegalArgumentException = assertThrows({
+      EthereumNodeRecord.fromRLP(tooLong)
+    })
+    assertEquals("Record too long", exception.message)
+  }
+
+  @Test
+  fun readFromRLP() {
+    val enr = EthereumNodeRecord.fromRLP(Bytes.fromHexString(
+      "f884b8407098ad865b00a582051940cb9cf36836572411a4727878307701" +
+      "1599ed5cd16b76f2635f4e234738f30813a89eb9137e3e3df5266e3a1f11" +
+      "df72ecf1145ccb9c01826964827634826970847f00000189736563703235" +
+      "366b31a103ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1" +
+      "400f3258cd31388375647082765f"))
+    assertEquals(1L, enr.seq)
+    assertEquals(Bytes.wrap("v4".toByteArray()), enr.data["id"])
+    assertEquals(Bytes.fromHexString("7f000001"), enr.data["ip"])
+    assertEquals(
+      Bytes.fromHexString("03ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd3138"),
+      enr.data["secp256k1"])
+    assertEquals(Bytes.fromHexString("765f"), enr.data["udp"])
+    enr.validate()
+    System.out.println(enr.publicKey().bytes())
+  }
+
+  @Test
+  fun toRLP() {
+    val keypair = SECP256K1.KeyPair.random()
+    val rlp = EthereumNodeRecord.toRLP(keypair,
+      seq = 1L,
+      data = mutableMapOf(Pair("key", Bytes.fromHexString("deadbeef"))),
+      ip = InetAddress.getByName("127.0.0.1"))
+    val record = EthereumNodeRecord.fromRLP(rlp)
+    assertEquals(1L, record.seq)
+    assertEquals(Bytes.wrap("v4".toByteArray()), record.data["id"])
+    assertEquals(Bytes.fromHexString("7f000001"), record.data["ip"])
+    assertEquals(keypair.publicKey(), record.publicKey())
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tuweni.apache.org
For additional commands, e-mail: commits-help@tuweni.apache.org