Skip to content
This repository has been archived by the owner on Aug 1, 2023. It is now read-only.

Commit

Permalink
Merge pull request #8 from atoulme/enr
Browse files Browse the repository at this point in the history
  • Loading branch information
atoulme authored Apr 30, 2019
2 parents 0d86a5e + 680cf41 commit 2cf77ba
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 0 deletions.
2 changes: 2 additions & 0 deletions devp2p/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
162 changes: 162 additions & 0 deletions devp2p/src/main/kotlin/org/apache/tuweni/devp2p/EthereumNodeRecord.kt
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)
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())
}
}

0 comments on commit 2cf77ba

Please sign in to comment.