This repository has been archived by the owner on Aug 1, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 83
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #8 from atoulme/enr
Add ENR - see https://eips.ethereum.org/EIPS/eip-778
- Loading branch information
Showing
3 changed files
with
237 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
162 changes: 162 additions & 0 deletions
162
devp2p/src/main/kotlin/org/apache/tuweni/devp2p/EthereumNodeRecord.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
73 changes: 73 additions & 0 deletions
73
devp2p/src/test/kotlin/org/apache/tuweni/devp2p/EthereumNodeRecordTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()) | ||
} | ||
} |