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